From d841d2b7acf6dde70032cffe7e2d319405342902 Mon Sep 17 00:00:00 2001 From: developer-boy-sdowner <75466952+developer-boy-sdowner@users.noreply.github.com> Date: Mon, 11 Jan 2021 09:27:00 +0530 Subject: [PATCH] Add files via upload --- fire_bot/modules/__init__.py | 37 + fire_bot/modules/admin.py | 409 +++++++ fire_bot/modules/afk.py | 103 ++ fire_bot/modules/antiflood.py | 170 +++ fire_bot/modules/backups.py | 82 ++ fire_bot/modules/bans.py | 351 ++++++ fire_bot/modules/blacklist.py | 187 +++ fire_bot/modules/blacklist2.py | 382 +++++++ fire_bot/modules/blacklistusers.py | 150 +++ fire_bot/modules/chatbot.py | 108 ++ fire_bot/modules/cleaner.py | 229 ++++ fire_bot/modules/combot_antispam_system.py | 349 ++++++ fire_bot/modules/connection.py | 303 +++++ fire_bot/modules/corona.py | 36 + fire_bot/modules/covid.py | 52 + fire_bot/modules/covid19.py | 121 ++ fire_bot/modules/covidindia.py | 55 + fire_bot/modules/currency_converter.py | 55 + fire_bot/modules/cust_filters.py | 287 +++++ fire_bot/modules/dbcleanup.py | 206 ++++ fire_bot/modules/dev.py | 64 ++ fire_bot/modules/devpromoter.py | 407 +++++++ fire_bot/modules/dictionary.py | 42 + fire_bot/modules/disable.py | 305 +++++ fire_bot/modules/disasters.py | 593 ++++++++++ fire_bot/modules/dogbin.py | 133 +++ fire_bot/modules/emojis.py | 389 +++++++ fire_bot/modules/eval.py | 136 +++ fire_bot/modules/feds.py | 1206 ++++++++++++++++++++ fire_bot/modules/fun.py | 157 +++ fire_bot/modules/fun_strings.py | 425 +++++++ fire_bot/modules/gettime.py | 91 ++ fire_bot/modules/github.py | 46 + fire_bot/modules/global_bans.py | 328 ++++++ fire_bot/modules/global_kick.py | 173 +++ fire_bot/modules/global_mutes.py | 337 ++++++ fire_bot/modules/google_reverse_search.py | 192 ++++ fire_bot/modules/gps.py | 53 + fire_bot/modules/grammar.py | 60 + fire_bot/modules/gtranslator.py | 131 +++ fire_bot/modules/locks.py | 355 ++++++ fire_bot/modules/log_channel.py | 204 ++++ fire_bot/modules/lyrics.py | 53 + fire_bot/modules/math.py | 140 +++ fire_bot/modules/misc.py | 473 ++++++++ fire_bot/modules/modules.py | 161 +++ fire_bot/modules/music.py | 82 ++ fire_bot/modules/muting.py | 208 ++++ fire_bot/modules/notes.py | 258 +++++ fire_bot/modules/paste.py | 42 + fire_bot/modules/ping.py | 109 ++ fire_bot/modules/police.py | 47 + fire_bot/modules/purge.py | 116 ++ fire_bot/modules/reactions.py | 235 ++++ fire_bot/modules/remote_cmds.py | 441 +++++++ fire_bot/modules/reporting.py | 160 +++ fire_bot/modules/rss.py | 244 ++++ fire_bot/modules/rules.py | 111 ++ fire_bot/modules/shout.py | 44 + fire_bot/modules/special.py | 165 +++ fire_bot/modules/speed_test.py | 56 + fire_bot/modules/stickers.py | 244 ++++ fire_bot/modules/sudo.py | 84 ++ fire_bot/modules/tts.py | 39 + fire_bot/modules/ud.py | 32 + fire_bot/modules/userinfo.py | 148 +++ fire_bot/modules/users.py | 131 +++ fire_bot/modules/warns.py | 427 +++++++ fire_bot/modules/weather.py | 71 ++ fire_bot/modules/weebify.py | 40 + fire_bot/modules/welcome.py | 714 ++++++++++++ fire_bot/modules/whois.py | 119 ++ fire_bot/modules/wiki.py | 30 + fire_bot/modules/zal.py | 35 + 74 files changed, 14758 insertions(+) create mode 100644 fire_bot/modules/__init__.py create mode 100644 fire_bot/modules/admin.py create mode 100644 fire_bot/modules/afk.py create mode 100644 fire_bot/modules/antiflood.py create mode 100644 fire_bot/modules/backups.py create mode 100644 fire_bot/modules/bans.py create mode 100644 fire_bot/modules/blacklist.py create mode 100644 fire_bot/modules/blacklist2.py create mode 100644 fire_bot/modules/blacklistusers.py create mode 100644 fire_bot/modules/chatbot.py create mode 100644 fire_bot/modules/cleaner.py create mode 100644 fire_bot/modules/combot_antispam_system.py create mode 100644 fire_bot/modules/connection.py create mode 100644 fire_bot/modules/corona.py create mode 100644 fire_bot/modules/covid.py create mode 100644 fire_bot/modules/covid19.py create mode 100644 fire_bot/modules/covidindia.py create mode 100644 fire_bot/modules/currency_converter.py create mode 100644 fire_bot/modules/cust_filters.py create mode 100644 fire_bot/modules/dbcleanup.py create mode 100644 fire_bot/modules/dev.py create mode 100644 fire_bot/modules/devpromoter.py create mode 100644 fire_bot/modules/dictionary.py create mode 100644 fire_bot/modules/disable.py create mode 100644 fire_bot/modules/disasters.py create mode 100644 fire_bot/modules/dogbin.py create mode 100644 fire_bot/modules/emojis.py create mode 100644 fire_bot/modules/eval.py create mode 100644 fire_bot/modules/feds.py create mode 100644 fire_bot/modules/fun.py create mode 100644 fire_bot/modules/fun_strings.py create mode 100644 fire_bot/modules/gettime.py create mode 100644 fire_bot/modules/github.py create mode 100644 fire_bot/modules/global_bans.py create mode 100644 fire_bot/modules/global_kick.py create mode 100644 fire_bot/modules/global_mutes.py create mode 100644 fire_bot/modules/google_reverse_search.py create mode 100644 fire_bot/modules/gps.py create mode 100644 fire_bot/modules/grammar.py create mode 100644 fire_bot/modules/gtranslator.py create mode 100644 fire_bot/modules/locks.py create mode 100644 fire_bot/modules/log_channel.py create mode 100644 fire_bot/modules/lyrics.py create mode 100644 fire_bot/modules/math.py create mode 100644 fire_bot/modules/misc.py create mode 100644 fire_bot/modules/modules.py create mode 100644 fire_bot/modules/music.py create mode 100644 fire_bot/modules/muting.py create mode 100644 fire_bot/modules/notes.py create mode 100644 fire_bot/modules/paste.py create mode 100644 fire_bot/modules/ping.py create mode 100644 fire_bot/modules/police.py create mode 100644 fire_bot/modules/purge.py create mode 100644 fire_bot/modules/reactions.py create mode 100644 fire_bot/modules/remote_cmds.py create mode 100644 fire_bot/modules/reporting.py create mode 100644 fire_bot/modules/rss.py create mode 100644 fire_bot/modules/rules.py create mode 100644 fire_bot/modules/shout.py create mode 100644 fire_bot/modules/special.py create mode 100644 fire_bot/modules/speed_test.py create mode 100644 fire_bot/modules/stickers.py create mode 100644 fire_bot/modules/sudo.py create mode 100644 fire_bot/modules/tts.py create mode 100644 fire_bot/modules/ud.py create mode 100644 fire_bot/modules/userinfo.py create mode 100644 fire_bot/modules/users.py create mode 100644 fire_bot/modules/warns.py create mode 100644 fire_bot/modules/weather.py create mode 100644 fire_bot/modules/weebify.py create mode 100644 fire_bot/modules/welcome.py create mode 100644 fire_bot/modules/whois.py create mode 100644 fire_bot/modules/wiki.py create mode 100644 fire_bot/modules/zal.py diff --git a/fire_bot/modules/__init__.py b/fire_bot/modules/__init__.py new file mode 100644 index 0000000..f929433 --- /dev/null +++ b/fire_bot/modules/__init__.py @@ -0,0 +1,37 @@ +from tg_bot import LOAD, NO_LOAD, LOGGER + + +def __list_all_modules(): + from os.path import dirname, basename, isfile + import glob + # This generates a list of modules in this folder for the * in __main__ to work. + mod_paths = glob.glob(dirname(__file__) + "/*.py") + all_modules = [basename(f)[:-3] for f in mod_paths if isfile(f) + and f.endswith(".py") + and not f.endswith('__init__.py')] + + if LOAD or NO_LOAD: + to_load = LOAD + if to_load: + if not all(any(mod == module_name for module_name in all_modules) for mod in to_load): + LOGGER.error("Invalid loadorder names. Quitting.") + quit(1) + + all_modules = sorted(set(all_modules) - set(to_load)) + to_load = list(all_modules) + to_load + + else: + to_load = all_modules + + if NO_LOAD: + LOGGER.info("Not loading: {}".format(NO_LOAD)) + return [item for item in to_load if item not in NO_LOAD] + + return to_load + + return all_modules + + +ALL_MODULES = __list_all_modules() +LOGGER.info("Modules to load: %s", str(ALL_MODULES)) +__all__ = ALL_MODULES + ["ALL_MODULES"] diff --git a/fire_bot/modules/admin.py b/fire_bot/modules/admin.py new file mode 100644 index 0000000..df042cc --- /dev/null +++ b/fire_bot/modules/admin.py @@ -0,0 +1,409 @@ +import html +import os +from typing import Optional, List + +import requests +from telegram import Message, Chat, Update, Bot, User +from telegram import ParseMode +from telegram.error import BadRequest +from telegram.ext import CommandHandler, Filters +from telegram.ext.dispatcher import run_async +from telegram.utils.helpers import escape_markdown, mention_html + +from tg_bot import dispatcher, TOKEN +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import bot_admin, can_promote, user_admin, can_pin, connection_status +from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text +from tg_bot.modules.log_channel import loggable +from tg_bot.modules.connection import connected +from tg_bot.modules.translations.strings import tld + +@run_async +@bot_admin +@user_admin +@loggable +def promote(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message # type: Optional[Message] + user = update.effective_user # type: Optional[User] + chat = update.effective_chat # type: Optional[Chat] + conn = connected(bot, update, chat, user.id) + if not conn == False: + chatD = dispatcher.bot.getChat(conn) + else: + chatD = update.effective_chat + if chat.type == "private": + exit(1) + + if not chatD.get_member(bot.id).can_promote_members: + update.effective_message.reply_text("I can't promote/demote people here! " + "Make sure I'm admin and can appoint new admins.") + exit(1) + + user_id = extract_user(message, args) + if not user_id: + message.reply_text(tld(chat.id, "You don't seem to be referring to a user.")) + return "" + + user_member = chatD.get_member(user_id) + if user_member.status == 'administrator' or user_member.status == 'creator': + message.reply_text(tld(chat.id, "How am I meant to promote someone that's already an admin?")) + return "" + + if user_id == bot.id: + message.reply_text(tld(chat.id, "I can't promote myself! Get an admin to do it for me.")) + return "" + + # set same perms as bot - bot can't assign higher perms than itself! + bot_member = chatD.get_member(bot.id) + + bot.promoteChatMember(chatD.id, user_id, + can_change_info=bot_member.can_change_info, + can_post_messages=bot_member.can_post_messages, + can_edit_messages=bot_member.can_edit_messages, + can_delete_messages=bot_member.can_delete_messages, + #can_invite_users=bot_member.can_invite_users, + can_restrict_members=bot_member.can_restrict_members, + can_pin_messages=bot_member.can_pin_messages, + can_promote_members=bot_member.can_promote_members) + + message.reply_text(tld(chat.id, f"Successfully promoted {mention_html(user_member.user.id, user_member.user.first_name)} in {html.escape(chatD.title)}!"), parse_mode=ParseMode.HTML) + return f"{html.escape(chatD.title)}:" \ + "\n#PROMOTED" \ + f"\nAdmin: {mention_html(user.id, user.first_name)}" \ + f"\nUser: {mention_html(user_member.user.id, user_member.user.first_name)}" + + +@run_async +@bot_admin +@user_admin +@loggable +def demote(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat # type: Optional[Chat] + message = update.effective_message # type: Optional[Message] + user = update.effective_user # type: Optional[User] + conn = connected(bot, update, chat, user.id) + if not conn == False: + chatD = dispatcher.bot.getChat(conn) + else: + chatD = update.effective_chat + if chat.type == "private": + exit(1) + + if not chatD.get_member(bot.id).can_promote_members: + update.effective_message.reply_text("I can't promote/demote people here! " + "Make sure I'm admin and can appoint new admins.") + exit(1) + + user_id = extract_user(message, args) + if not user_id: + message.reply_text(tld(chat.id, "You don't seem to be referring to a user.")) + return "" + + user_member = chatD.get_member(user_id) + if user_member.status == 'creator': + message.reply_text(tld(chat.id, "This person CREATED the chat, how would I demote them?")) + return "" + + if not user_member.status == 'administrator': + message.reply_text(tld(chat.id, "Can't demote what wasn't promoted!")) + return "" + + if user_id == bot.id: + message.reply_text(tld(chat.id, "I can't demote myself!")) + return "" + + try: + bot.promoteChatMember(int(chatD.id), int(user_id), + can_change_info=False, + can_post_messages=False, + can_edit_messages=False, + can_delete_messages=False, + can_invite_users=False, + can_restrict_members=False, + can_pin_messages=False, + can_promote_members=False) + message.reply_text(tld(chat.id, f"Successfully demoted in *{chatD.title}*!"), parse_mode=ParseMode.MARKDOWN) + return f"{html.escape(chatD.title)}:" \ + "\n#DEMOTED" \ + f"\nAdmin: {mention_html(user.id, user.first_name)}" \ + f"\nUser: {mention_html(user_member.user.id, user_member.user.first_name)}" + + except BadRequest: + message.reply_text( + tld(chat.id, "Could not demote. I might not be admin, or the admin status was appointed by another user, so I can't act upon them!") + ) + return "" + + + + +@run_async +@bot_admin +@can_pin +@user_admin +@loggable +def pin(bot: Bot, update: Update, args: List[str]) -> str: + user = update.effective_user # type: Optional[User] + chat = update.effective_chat # type: Optional[Chat] + + is_group = chat.type != "private" and chat.type != "channel" + + prev_message = update.effective_message.reply_to_message + + is_silent = True + if len(args) >= 1: + is_silent = not (args[0].lower() == 'notify' or args[0].lower() == 'loud' or args[0].lower() == 'violent') + + if prev_message and is_group: + try: + bot.pinChatMessage(chat.id, prev_message.message_id, disable_notification=is_silent) + except BadRequest as excp: + if excp.message == "Chat_not_modified": + pass + else: + raise + return "{}:" \ + "\n#PINNED" \ + "\nAdmin: {}".format(html.escape(chat.title), mention_html(user.id, user.first_name)) + + return "" + + +@run_async +@bot_admin +@can_pin +@user_admin +@loggable +def unpin(bot: Bot, update: Update) -> str: + chat = update.effective_chat + user = update.effective_user # type: Optional[User] + + try: + bot.unpinChatMessage(chat.id) + except BadRequest as excp: + if excp.message == "Chat_not_modified": + pass + else: + raise + + return "{}:" \ + "\n#UNPINNED" \ + "\nAdmin: {}".format(html.escape(chat.title), + mention_html(user.id, user.first_name)) + + +@run_async +@bot_admin +@user_admin +def invite(bot: Bot, update: Update): + chat = update.effective_chat # type: Optional[Chat] + if chat.username: + update.effective_message.reply_text(chat.username) + elif chat.type == chat.SUPERGROUP or chat.type == chat.CHANNEL: + bot_member = chat.get_member(bot.id) + if bot_member.can_invite_users: + invitelink = bot.exportChatInviteLink(chat.id) + update.effective_message.reply_text(invitelink) + else: + update.effective_message.reply_text("I don't have access to the invite link, try changing my permissions!") + else: + update.effective_message.reply_text("I can only give you invite links for supergroups and channels, sorry!") + +@run_async +@connection_status +@bot_admin +@can_promote +@user_admin +def set_title(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + message = update.effective_message + + user_id, title = extract_user_and_text(message, args) + try: + user_member = chat.get_member(user_id) + except: + return + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + + if user_member.status == 'creator': + message.reply_text("This person CREATED the chat, how can i set custom title for him?") + return + + if not user_member.status == 'administrator': + message.reply_text("Can't set title for non-admins!\nPromote them first to set custom title!") + return + + if user_id == bot.id: + message.reply_text("I can't set my own title myself! Get the one who made me admin to do it for me.") + return + + if not title: + message.reply_text("Setting blank title doesn't do anything!") + return + + if len(title) > 16: + message.reply_text("The title length is longer than 16 characters.\nTruncating it to 16 characters.") + + result = requests.post(f"https://api.telegram.org/bot{TOKEN}/setChatAdministratorCustomTitle" + f"?chat_id={chat.id}" + f"&user_id={user_id}" + f"&custom_title={title}") + status = result.json()["ok"] + + if status is True: + bot.sendMessage(chat.id, f"Sucessfully set title for {user_member.user.first_name or user_id} " + f"to {title[:16]}!", parse_mode=ParseMode.HTML) + else: + description = result.json()["description"] + if description == "Bad Request: not enough rights to change custom title of the user": + message.reply_text("I can't set custom title for admins that I didn't promote!") + + +@run_async +@bot_admin +@user_admin +def setchatpic(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + user = update.effective_user + + user_member = chat.get_member(user.id) + if user_member.can_change_info == False: + msg.reply_text("You are missing right to change group info!") + return + + if msg.reply_to_message: + if msg.reply_to_message.photo: + pic_id = msg.reply_to_message.photo[-1].file_id + elif msg.reply_to_message.document: + pic_id = msg.reply_to_message.document.file_id + else: + msg.reply_text("You can only set some photo as chat pic!") + return + dlmsg = msg.reply_text("Hold on...") + tpic = bot.get_file(pic_id) + tpic.download('gpic.png') + try: + with open('gpic.png', 'rb') as chatp: + bot.set_chat_photo(int(chat.id), photo=chatp) + msg.reply_text("Successfully set new chat Picture!") + except BadRequest as excp: + msg.reply_text(f"Error! {excp.message}") + finally: + dlmsg.delete() + if os.path.isfile('gpic.png'): + os.remove("gpic.png") + else: + msg.reply_text("Reply to some photo or file to set new chat pic!") + + +@run_async +@bot_admin +@user_admin +def rmchatpic(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + user = update.effective_user + + user_member = chat.get_member(user.id) + if user_member.can_change_info == False: + msg.reply_text("You don't have enough rights to delete group photo") + return + try: + bot.delete_chat_photo(int(chat.id)) + msg.reply_text("Successfully deleted chat's profile photo!") + except BadRequest as excp: + msg.reply_text(f"Error! {excp.message}.") + return + + +@run_async +def adminlist(bot: Bot, update: Update): + administrators = update.effective_chat.get_administrators() + msg = update.effective_message + text = "Admins in *{}*:".format(update.effective_chat.title or "this chat") + for admin in administrators: + user = admin.user + status = admin.status + name = "[{}](tg://user?id={})".format(user.first_name + " " + (user.last_name or ""), user.id) + if user.username: + name = name = escape_markdown("@" + user.username) + if status == "creator": + text += "\n 🔱 Creator:" + text += "\n` • `{} \n\n • *Administrators*:".format(name) + for admin in administrators: + user = admin.user + status = admin.status + chat = update.effective_chat + count = chat.get_members_count() + name = "[{}](tg://user?id={})".format(user.first_name + " " + (user.last_name or ""), user.id) + if user.username: + name = escape_markdown("@" + user.username) + + if status == "administrator": + text += "\n`👮🏻 `{}".format(name) + members = "\n\n*Members:*\n`🙍‍♂️ ` {} users".format(count) + + msg.reply_text(text + members, parse_mode=ParseMode.MARKDOWN) + + + +def __chat_settings__(chat_id, user_id): + return "You are *admin*: `{}`".format( + dispatcher.bot.get_chat_member(chat_id, user_id).status in ("administrator", "creator")) + + +__help__ = """ + - /adminlist: list of admins in the chat + +*Admin only:* + - /pin: silently pins the message replied to - add 'loud' or 'notify' to give notifs to users. + - /unpin: unpins the currently pinned message + - /invitelink: gets invitelink + - /promote: promotes the user replied to + - /demote: demotes the user replied to + - /settitle: sets a custom title for an admin that the bot promoted. + - /settitle: Sets a custom title for an admin which is promoted by bot. + - /setgpic: As a reply to file or photo to set group profile pic! + - /delgpic: Same as above but to remove group profile pic. + +""" + + +PIN_HANDLER = CommandHandler("pin", pin, pass_args=True, filters=Filters.group) +UNPIN_HANDLER = CommandHandler("unpin", unpin, filters=Filters.group) + +INVITE_HANDLER = CommandHandler("invitelink", invite, filters=Filters.group) + +PROMOTE_HANDLER = CommandHandler("promote", promote, pass_args=True, filters=Filters.group) +DEMOTE_HANDLER = CommandHandler("demote", demote, pass_args=True, filters=Filters.group) + + +SET_TITLE_HANDLER = CommandHandler("settitle", set_title, pass_args=True) +CHAT_PIC_HANDLER = CommandHandler("setgpic", setchatpic, filters=Filters.group) +DEL_CHAT_PIC_HANDLER = CommandHandler("delgpic", rmchatpic, filters=Filters.group) + + + + +ADMINLIST_HANDLER = DisableAbleCommandHandler("adminlist", adminlist, filters=Filters.group) + +dispatcher.add_handler(PIN_HANDLER) +dispatcher.add_handler(UNPIN_HANDLER) +dispatcher.add_handler(INVITE_HANDLER) +dispatcher.add_handler(PROMOTE_HANDLER) +dispatcher.add_handler(DEMOTE_HANDLER) +dispatcher.add_handler(SET_TITLE_HANDLER) +dispatcher.add_handler(CHAT_PIC_HANDLER) +dispatcher.add_handler(DEL_CHAT_PIC_HANDLER) +dispatcher.add_handler(ADMINLIST_HANDLER) + +__mod_name__ = "ADMIN" + +__command_list__ = ["adminlist", "admins", "invitelink"] + +__handlers__ = [ADMINLIST_HANDLER, PIN_HANDLER, UNPIN_HANDLER, + INVITE_HANDLER, PROMOTE_HANDLER, DEMOTE_HANDLER, SET_TITLE_HANDLER, CHAT_PIC_HANDLER, DEL_CHAT_PIC_HANDLER] diff --git a/fire_bot/modules/afk.py b/fire_bot/modules/afk.py new file mode 100644 index 0000000..ea44f0e --- /dev/null +++ b/fire_bot/modules/afk.py @@ -0,0 +1,103 @@ +import random + +from telegram import Bot, Update, MessageEntity +from telegram.ext import Filters, run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler, DisableAbleRegexHandler, DisableAbleMessageHandler +from tg_bot.modules.sql import afk_sql as sql +from tg_bot.modules.users import get_user_id + +AFK_GROUP = 7 +AFK_REPLY_GROUP = 8 + + +@run_async +def afk(bot: Bot, update: Update): + args = update.effective_message.text.split(None, 1) + reason = "" + if len(args) >= 2: + reason = args[1] + + sql.set_afk(update.effective_user.id, reason) + update.effective_message.reply_text("{} is now away!".format(update.effective_user.first_name)) + + +@run_async +def no_longer_afk(bot: Bot, update: Update): + user = update.effective_user + + if not user: + return + + res = sql.rm_afk(user.id) + if res: + options = [ + '{} is here!', + '{} is back!', + '{} is now in the chat!', + '{} is awake!', + '{} is back online!', + '{} is finally here!', + 'Welcome back!, {}', + 'Where is {}?\nIn the chat!' + ] + chosen_option = random.choice(options) + update.effective_message.reply_text(chosen_option.format(update.effective_user.first_name)) + + +@run_async +def reply_afk(bot: Bot, update: Update): + message = update.effective_message + entities = message.parse_entities([MessageEntity.TEXT_MENTION, MessageEntity.MENTION]) + + if message.entities and entities: + for ent in entities: + if ent.type == MessageEntity.TEXT_MENTION: + user_id = ent.user.id + fst_name = ent.user.first_name + + elif ent.type == MessageEntity.MENTION: + user_id = get_user_id(message.text[ent.offset:ent.offset + ent.length]) + if not user_id: + return + chat = bot.get_chat(user_id) + fst_name = chat.first_name + + else: + return + + if sql.is_afk(user_id): + valid, reason = sql.check_afk_status(user_id) + if valid: + if not reason: + res = "{} is AFK!".format(fst_name) + else: + res = "{} is AFK!\nReason:\n{}".format(fst_name, reason) + message.reply_text(res) + + +def __gdpr__(user_id): + sql.rm_afk(user_id) + + +__help__ = """ + - /afk : mark yourself as AFK(away from keyboard). + - brb : same as the afk command - but not a command. +When marked as AFK, any mentions will be replied to with a message to say you're not available! +""" + +AFK_HANDLER = DisableAbleCommandHandler("afk", afk) +AFK_REGEX_HANDLER = DisableAbleRegexHandler(r"(?i)brb", afk, friendly="afk") +NO_AFK_HANDLER = DisableAbleMessageHandler(Filters.all & Filters.group, no_longer_afk, friendly="afk") +AFK_REPLY_HANDLER = DisableAbleMessageHandler((Filters.entity(MessageEntity.MENTION) | Filters.entity(MessageEntity.TEXT_MENTION)) & Filters.group, reply_afk, friendly="afk") + +dispatcher.add_handler(AFK_HANDLER, AFK_GROUP) +dispatcher.add_handler(AFK_REGEX_HANDLER, AFK_GROUP) +dispatcher.add_handler(NO_AFK_HANDLER, AFK_GROUP) +dispatcher.add_handler(AFK_REPLY_HANDLER, AFK_REPLY_GROUP) + +__mod_name__ = "AFK" +__command_list__ = ["afk"] +__handlers__ = [(AFK_HANDLER, AFK_GROUP), (AFK_REGEX_HANDLER, AFK_GROUP), (NO_AFK_HANDLER, AFK_GROUP), + (AFK_REPLY_HANDLER, AFK_REPLY_GROUP)] diff --git a/fire_bot/modules/antiflood.py b/fire_bot/modules/antiflood.py new file mode 100644 index 0000000..badba2c --- /dev/null +++ b/fire_bot/modules/antiflood.py @@ -0,0 +1,170 @@ +import html +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.error import BadRequest +from telegram.ext import MessageHandler, CommandHandler, Filters, run_async +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, WHITELIST_USERS, TIGER_USERS +from tg_bot.modules.helper_funcs.chat_status import is_user_admin, user_admin, can_restrict, connection_status +from tg_bot.modules.log_channel import loggable +from tg_bot.modules.sql import antiflood_sql as sql + +FLOOD_GROUP = 3 + + +@run_async +@loggable +def check_flood(bot: Bot, update: Update) -> str: + user = update.effective_user + chat = update.effective_chat + msg = update.effective_message + log_message = "" + + if not user: # ignore channels + return log_message + + # ignore admins and whitelists + if (is_user_admin(chat, user.id) + or user.id in WHITELIST_USERS + or user.id in TIGER_USERS): + sql.update_flood(chat.id, None) + return log_message + + should_ban = sql.update_flood(chat.id, user.id) + if not should_ban: + return log_message + + try: + bot.restrict_chat_member(chat.id, user.id, can_send_messages=False) + msg.reply_text(f"*mutes {mention_html(user.id, user.first_name)} permanently*\nStop flooding the group!", parse_mode=ParseMode.HTML) + log_message = (f"{html.escape(chat.title)}:\n" + f"#MUTED\n" + f"User: {mention_html(user.id, user.first_name)}\n" + f"Flooded the group.\nMuted until an admin unmutes") + + return log_message + + except BadRequest: + msg.reply_text("I can't kick people here, give me permissions first! Until then, I'll disable antiflood.") + sql.set_flood(chat.id, 0) + log_message = ("{chat.title}:\n" + "#INFO\n" + "Don't have kick permissions, so automatically disabled antiflood.") + + return log_message + + +@run_async +@connection_status +@user_admin +@can_restrict +@loggable +def set_flood(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + log_message = "" + + update_chat_title = chat.title + message_chat_title = update.effective_message.chat.title + + if update_chat_title == message_chat_title: + chat_name = "" + else: + chat_name = f" in {update_chat_title}" + + if len(args) >= 1: + + val = args[0].lower() + + if val == "off" or val == "no" or val == "0": + sql.set_flood(chat.id, 0) + message.reply_text("Antiflood has been disabled{}.".format(chat_name), parse_mode=ParseMode.HTML) + + elif val.isdigit(): + amount = int(val) + if amount <= 0: + sql.set_flood(chat.id, 0) + message.reply_text("Antiflood has been disabled{}.".format(chat_name), parse_mode=ParseMode.HTML) + log_message = (f"{html.escape(chat.title)}:\n" + f"#SETFLOOD\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Disabled antiflood.") + + return log_message + elif amount < 3: + message.reply_text("Antiflood has to be either 0 (disabled), or a number bigger than 3!") + return log_message + + else: + sql.set_flood(chat.id, amount) + message.reply_text("Antiflood has been updated and set to {}{}".format(amount, chat_name), + parse_mode=ParseMode.HTML) + log_message = (f"{html.escape(chat.title)}:\n" + f"#SETFLOOD\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Set antiflood to {amount}.") + + return log_message + else: + message.reply_text("Unrecognised argument - please use a number, 'off', or 'no'.") + + return log_message + + +@run_async +@connection_status +def flood(bot: Bot, update: Update): + chat = update.effective_chat + update_chat_title = chat.title + message_chat_title = update.effective_message.chat.title + + if update_chat_title == message_chat_title: + chat_name = "" + else: + chat_name = f" in {update_chat_title}" + + limit = sql.get_flood_limit(chat.id) + + if limit == 0: + update.effective_message.reply_text(f"I'm not currently enforcing flood control{chat_name}!", + parse_mode=ParseMode.HTML) + else: + update.effective_message.reply_text(f"I'm currently muting users if they send " + f"more than {limit} consecutive messages{chat_name}.", + parse_mode=ParseMode.HTML) + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + limit = sql.get_flood_limit(chat_id) + if limit == 0: + return "*Not* currently enforcing flood control." + else: + return "Antiflood is set to `{}` messages.".format(limit) + + +__help__ = """ + - /flood: Get the current flood control setting + +*Admin only:* + - /setflood : enables or disables flood control + Example: /setflood 10 + This will mute users if they send more than 10 messages in a row, bots are ignored. +""" + +FLOOD_BAN_HANDLER = MessageHandler(Filters.all & ~Filters.status_update & Filters.group, check_flood) +SET_FLOOD_HANDLER = CommandHandler("setflood", set_flood, pass_args=True) +FLOOD_HANDLER = CommandHandler("flood", flood) + +dispatcher.add_handler(FLOOD_BAN_HANDLER, FLOOD_GROUP) +dispatcher.add_handler(SET_FLOOD_HANDLER) +dispatcher.add_handler(FLOOD_HANDLER) + +__mod_name__ = "ANTI FLOOD" +__handlers__ = [(FLOOD_BAN_HANDLER, FLOOD_GROUP), SET_FLOOD_HANDLER, FLOOD_HANDLER] diff --git a/fire_bot/modules/backups.py b/fire_bot/modules/backups.py new file mode 100644 index 0000000..0a99310 --- /dev/null +++ b/fire_bot/modules/backups.py @@ -0,0 +1,82 @@ +import json +from io import BytesIO + +from telegram import Bot, Update +from telegram.error import BadRequest +from telegram.ext import CommandHandler, run_async + +from tg_bot import dispatcher, LOGGER +from tg_bot.__main__ import DATA_IMPORT +from tg_bot.modules.helper_funcs.chat_status import user_admin + + +@run_async +@user_admin +def import_data(bot: Bot, update: Update): + msg = update.effective_message + chat = update.effective_chat + # TODO: allow uploading doc with command, not just as reply + # only work with a doc + if msg.reply_to_message and msg.reply_to_message.document: + try: + file_info = bot.get_file(msg.reply_to_message.document.file_id) + except BadRequest: + msg.reply_text("Try downloading and reuploading the file as yourself before importing - this one seems " + "to be iffy!") + return + + with BytesIO() as file: + file_info.download(out=file) + file.seek(0) + data = json.load(file) + + # only import one group + if len(data) > 1 and str(chat.id) not in data: + msg.reply_text("Theres more than one group here in this file, and none have the same chat id as this group " + "- how do I choose what to import?") + return + + # Select data source + if str(chat.id) in data: + data = data[str(chat.id)]['hashes'] + else: + data = data[list(data.keys())[0]]['hashes'] + + try: + for mod in DATA_IMPORT: + mod.__import_data__(str(chat.id), data) + except Exception: + msg.reply_text("An exception occured while restoring your data. The process may not be complete. If " + "you're having issues with this, message @OnePunchSupport with your backup file so the " + "issue can be debugged. My owners would be happy to help, and every bug " + "reported makes me better! Thanks! :)") + LOGGER.exception("Import for chatid %s with name %s failed.", str(chat.id), str(chat.title)) + return + + # TODO: some of that link logic + # NOTE: consider default permissions stuff? + msg.reply_text("Backup fully imported. Welcome back! :D") + + +@run_async +@user_admin +def export_data(bot: Bot, update: Update): + msg = update.effective_message + msg.reply_text("Doesn't work yet.") + + +__help__ = """ +*Admin only:* + - /import: reply to a group butler backup file to import as much as possible, making the transfer super simple! Note \ +that files/photos can't be imported due to telegram restrictions. + - /export: !!! This isn't a command yet, but should be coming soon! +""" + +IMPORT_HANDLER = CommandHandler("import", import_data) +EXPORT_HANDLER = CommandHandler("export", export_data) + +dispatcher.add_handler(IMPORT_HANDLER) +dispatcher.add_handler(EXPORT_HANDLER) + +__mod_name__ = "BACKUP" +__handlers__ = [IMPORT_HANDLER, EXPORT_HANDLER] diff --git a/fire_bot/modules/bans.py b/fire_bot/modules/bans.py new file mode 100644 index 0000000..d0d393d --- /dev/null +++ b/fire_bot/modules/bans.py @@ -0,0 +1,351 @@ +import html +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.error import BadRequest +from telegram.ext import CommandHandler, Filters, run_async +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, LOGGER, DEV_USERS, SUDO_USERS, TIGER_USERS +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import (bot_admin, user_admin, is_user_ban_protected, can_restrict, + is_user_admin, is_user_in_chat, connection_status) +from tg_bot.modules.helper_funcs.extraction import extract_user_and_text +from tg_bot.modules.helper_funcs.string_handling import extract_time +from tg_bot.modules.log_channel import loggable, gloggable + + +@run_async +@connection_status +@bot_admin +@can_restrict +@user_admin +@loggable +def ban(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + log_message = "" + + user_id, reason = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("I doubt that's a user.") + return log_message + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("Can't seem to find this person.") + return log_message + else: + raise + + if user_id == bot.id: + message.reply_text("Oh yeah, ban myself, noob!") + return log_message + + # dev users to bypass whitelist protection incase of abuse + if is_user_ban_protected(chat, user_id, member) and user not in DEV_USERS: + message.reply_text("This user has immunity - I can't ban them.") + return log_message + + log = (f"{html.escape(chat.title)}:\n" + f"#BANNED\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(member.user.id, member.user.first_name)}") + if reason: + log += "\nReason: {}".format(reason) + + try: + chat.kick_member(user_id) + # bot.send_sticker(chat.id, BAN_STICKER) # banhammer marie sticker + bot.sendMessage(chat.id, "Banned user {}.".format(mention_html(member.user.id, member.user.first_name)), + parse_mode=ParseMode.HTML) + return log + + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text('Banned!', quote=False) + return log + else: + LOGGER.warning(update) + LOGGER.exception("ERROR banning user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, + excp.message) + message.reply_text("Uhm...that didn't work...") + + return log_message + + +@run_async +@connection_status +@bot_admin +@can_restrict +@user_admin +@loggable +def temp_ban(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + log_message = "" + + user_id, reason = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("I doubt that's a user.") + return log_message + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user.") + return log_message + else: + raise + + if user_id == bot.id: + message.reply_text("I'm not gonna BAN myself, are you crazy?") + return log_message + + if is_user_ban_protected(chat, user_id, member): + message.reply_text("I don't feel like it.") + return log_message + + if not reason: + message.reply_text("You haven't specified a time to ban this user for!") + return log_message + + split_reason = reason.split(None, 1) + + time_val = split_reason[0].lower() + if len(split_reason) > 1: + reason = split_reason[1] + else: + reason = "" + + bantime = extract_time(message, time_val) + + if not bantime: + return log_message + + log = (f"{html.escape(chat.title)}:\n" + "#TEMP BANNED\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(member.user.id, member.user.first_name)}\n" + f"Time: {time_val}") + if reason: + log += "\nReason: {}".format(reason) + + try: + chat.kick_member(user_id, until_date=bantime) + # bot.send_sticker(chat.id, BAN_STICKER) # banhammer marie sticker + bot.sendMessage(chat.id, f"Banned! User {mention_html(member.user.id, member.user.first_name)} " + f"will be banned for {time_val}.", + parse_mode=ParseMode.HTML) + return log + + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text(f"Banned! User will be banned for {time_val}.", quote=False) + return log + else: + LOGGER.warning(update) + LOGGER.exception("ERROR banning user %s in chat %s (%s) due to %s", + user_id, chat.title, chat.id, excp.message) + message.reply_text("Well damn, I can't ban that user.") + + return log_message + + +@run_async +@connection_status +@bot_admin +@can_restrict +@user_admin +@loggable +def punch(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + log_message = "" + + user_id, reason = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("I doubt that's a user.") + return log_message + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user.") + return log_message + else: + raise + + if user_id == bot.id: + message.reply_text("Yeahhh I'm not gonna do that.") + return log_message + + if is_user_ban_protected(chat, user_id): + message.reply_text("I really wish I could punch this user....") + return log_message + + res = chat.unban_member(user_id) # unban on current user = kick + if res: + # bot.send_sticker(chat.id, BAN_STICKER) # banhammer marie sticker + bot.sendMessage(chat.id, f"One Punched! {mention_html(member.user.id, member.user.first_name)}.", + parse_mode=ParseMode.HTML) + log = (f"{html.escape(chat.title)}:\n" + f"#KICKED\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(member.user.id, member.user.first_name)}") + if reason: + log += f"\nReason: {reason}" + + return log + + else: + message.reply_text("Well damn, I can't punch that user.") + + return log_message + + +@run_async +@bot_admin +@can_restrict +def punchme(bot: Bot, update: Update): + user_id = update.effective_message.from_user.id + if is_user_admin(update.effective_chat, user_id): + update.effective_message.reply_text("I wish I could... but you're an admin.") + return + + res = update.effective_chat.unban_member(user_id) # unban on current user = kick + if res: + update.effective_message.reply_text("No problem.") + else: + update.effective_message.reply_text("Huh? I can't :/") + + +@run_async +@connection_status +@bot_admin +@can_restrict +@user_admin +@loggable +def unban(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + log_message = "" + + user_id, reason = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("I doubt that's a user.") + return log_message + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user.") + return log_message + else: + raise + + if user_id == bot.id: + message.reply_text("How would I unban myself if I wasn't here...?") + return log_message + + if is_user_in_chat(chat, user_id): + message.reply_text("Isn't this person already here??") + return log_message + + chat.unban_member(user_id) + message.reply_text("Yep, this user can join!") + + log = (f"{html.escape(chat.title)}:\n" + f"#UNBANNED\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(member.user.id, member.user.first_name)}") + if reason: + log += f"\nReason: {reason}" + + return log + + +@run_async +@connection_status +@bot_admin +@can_restrict +@gloggable +def selfunban(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + + if user.id not in SUDO_USERS or user.id not in TIGER_USERS: + return + + try: + chat_id = int(args[0]) + except: + message.reply_text("Give a valid chat ID.") + return + + chat = bot.getChat(chat_id) + + try: + member = chat.get_member(user.id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user.") + return + else: + raise + + if is_user_in_chat(chat, user.id): + message.reply_text("Aren't you already in the chat??") + return + + chat.unban_member(user.id) + message.reply_text("Yep, I have unbanned you.") + + log = (f"{html.escape(chat.title)}:\n" + f"#UNBANNED\n" + f"User: {mention_html(member.user.id, member.user.first_name)}") + + return log + + +__help__ = """ + - /punchme: punchs the user who issued the command + +*Admin only:* + - /ban : bans a user. (via handle, or reply) + - /tban x(m/h/d): bans a user for x time. (via handle, or reply). m = minutes, h = hours, d = days. + - /unban : unbans a user. (via handle, or reply) + - /punch : Punches a user out of the group, (via handle, or reply) +""" + +BAN_HANDLER = CommandHandler("ban", ban, pass_args=True) +TEMPBAN_HANDLER = CommandHandler(["tban", "tempban"], temp_ban, pass_args=True) +PUNCH_HANDLER = CommandHandler("punch", punch, pass_args=True) +UNBAN_HANDLER = CommandHandler("unban", unban, pass_args=True) +ROAR_HANDLER = CommandHandler("roar", selfunban, pass_args=True) +PUNCHME_HANDLER = DisableAbleCommandHandler("punchme", punchme, filters=Filters.group) + +dispatcher.add_handler(BAN_HANDLER) +dispatcher.add_handler(TEMPBAN_HANDLER) +dispatcher.add_handler(PUNCH_HANDLER) +dispatcher.add_handler(UNBAN_HANDLER) +dispatcher.add_handler(ROAR_HANDLER) +dispatcher.add_handler(PUNCHME_HANDLER) + +__mod_name__ = "BAN" +__handlers__ = [BAN_HANDLER, TEMPBAN_HANDLER, PUNCH_HANDLER, UNBAN_HANDLER, ROAR_HANDLER, PUNCHME_HANDLER] diff --git a/fire_bot/modules/blacklist.py b/fire_bot/modules/blacklist.py new file mode 100644 index 0000000..5eb9306 --- /dev/null +++ b/fire_bot/modules/blacklist.py @@ -0,0 +1,187 @@ +import html +import re +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.error import BadRequest +from telegram.ext import CommandHandler, MessageHandler, Filters, run_async + +import tg_bot.modules.sql.blacklist_sql as sql +from tg_bot import dispatcher, LOGGER +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import user_admin, user_not_admin, connection_status +from tg_bot.modules.helper_funcs.extraction import extract_text +from tg_bot.modules.helper_funcs.misc import split_message + +BLACKLIST_GROUP = 11 + + +@run_async +@connection_status +def blacklist(bot: Bot, update: Update, args: List[str]): + msg = update.effective_message + chat = update.effective_chat + + update_chat_title = chat.title + message_chat_title = update.effective_message.chat.title + + if update_chat_title == message_chat_title: + base_blacklist_string = "Current blacklisted words:\n" + else: + base_blacklist_string = f"Current blacklisted words in {update_chat_title}:\n" + + all_blacklisted = sql.get_chat_blacklist(chat.id) + + filter_list = base_blacklist_string + + if len(args) > 0 and args[0].lower() == 'copy': + for trigger in all_blacklisted: + filter_list += f"{html.escape(trigger)}\n" + else: + for trigger in all_blacklisted: + filter_list += f" - {html.escape(trigger)}\n" + + split_text = split_message(filter_list) + for text in split_text: + if text == base_blacklist_string: + if update_chat_title == message_chat_title: + msg.reply_text("There are no blacklisted messages here!") + else: + msg.reply_text(f"There are no blacklisted messages in {update_chat_title}!", + parse_mode=ParseMode.HTML) + return + msg.reply_text(text, parse_mode=ParseMode.HTML) + + +@run_async +@connection_status +@user_admin +def add_blacklist(bot: Bot, update: Update): + msg = update.effective_message + chat = update.effective_chat + words = msg.text.split(None, 1) + + if len(words) > 1: + text = words[1] + to_blacklist = list(set(trigger.strip() for trigger in text.split("\n") if trigger.strip())) + + for trigger in to_blacklist: + sql.add_to_blacklist(chat.id, trigger.lower()) + + if len(to_blacklist) == 1: + msg.reply_text(f"Added {html.escape(to_blacklist[0])} to the blacklist!", + parse_mode=ParseMode.HTML) + + else: + msg.reply_text(f"Added {len(to_blacklist)} triggers to the blacklist.", + parse_mode=ParseMode.HTML) + + else: + msg.reply_text("Tell me which words you would like to remove from the blacklist.") + + +@run_async +@connection_status +@user_admin +def unblacklist(bot: Bot, update: Update): + msg = update.effective_message + chat = update.effective_chat + words = msg.text.split(None, 1) + + if len(words) > 1: + text = words[1] + to_unblacklist = list(set(trigger.strip() for trigger in text.split("\n") if trigger.strip())) + successful = 0 + + for trigger in to_unblacklist: + success = sql.rm_from_blacklist(chat.id, trigger.lower()) + if success: + successful += 1 + + if len(to_unblacklist) == 1: + if successful: + msg.reply_text(f"Removed {html.escape(to_unblacklist[0])} from the blacklist!", + parse_mode=ParseMode.HTML) + else: + msg.reply_text("This isn't a blacklisted trigger...!") + + elif successful == len(to_unblacklist): + msg.reply_text(f"Removed {successful} triggers from the blacklist.", parse_mode=ParseMode.HTML) + + elif not successful: + msg.reply_text("None of these triggers exist, so they weren't removed.", parse_mode=ParseMode.HTML) + + else: + msg.reply_text(f"Removed {successful} triggers from the blacklist." + f" {len(to_unblacklist) - successful} did not exist, so were not removed.", + parse_mode=ParseMode.HTML) + else: + msg.reply_text("Tell me which words you would like to remove from the blacklist.") + + +@run_async +@connection_status +@user_not_admin +def del_blacklist(bot: Bot, update: Update): + chat = update.effective_chat + message = update.effective_message + to_match = extract_text(message) + + if not to_match: + return + + chat_filters = sql.get_chat_blacklist(chat.id) + for trigger in chat_filters: + pattern = r"( |^|[^\w])" + re.escape(trigger) + r"( |$|[^\w])" + if re.search(pattern, to_match, flags=re.IGNORECASE): + try: + message.delete() + except BadRequest as excp: + if excp.message == "Message to delete not found": + pass + else: + LOGGER.exception("Error while deleting blacklist message.") + break + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + blacklisted = sql.num_blacklist_chat_filters(chat_id) + return "There are {} blacklisted words.".format(blacklisted) + + +def __stats__(): + return "{} blacklist triggers, across {} chats.".format(sql.num_blacklist_filters(), + sql.num_blacklist_filter_chats()) + + +__help__ = """ +Blacklists are used to stop certain triggers from being said in a group. Any time the trigger is mentioned, \ +the message will immediately be deleted. A good combo is sometimes to pair this up with warn filters! + +*NOTE:* blacklists do not affect group admins. + + - /blacklist: View the current blacklisted words. + +*Admin only:* + - /addblacklist : Add a trigger to the blacklist. Each line is considered one trigger, so using different \ +lines will allow you to add multiple triggers. + - /unblacklist : Remove triggers from the blacklist. Same newline logic applies here, so you can remove \ +multiple triggers at once. + - /rmblacklist : Same as above. +""" + +BLACKLIST_HANDLER = DisableAbleCommandHandler("blacklist", blacklist, pass_args=True, admin_ok=True) +ADD_BLACKLIST_HANDLER = CommandHandler("addblacklist", add_blacklist) +UNBLACKLIST_HANDLER = CommandHandler(["unblacklist", "rmblacklist"], unblacklist) +BLACKLIST_DEL_HANDLER = MessageHandler((Filters.text | Filters.command | Filters.sticker | Filters.photo) & Filters.group, del_blacklist, edited_updates=True) +dispatcher.add_handler(BLACKLIST_HANDLER) +dispatcher.add_handler(ADD_BLACKLIST_HANDLER) +dispatcher.add_handler(UNBLACKLIST_HANDLER) +dispatcher.add_handler(BLACKLIST_DEL_HANDLER, group=BLACKLIST_GROUP) + +__mod_name__ = "WORD BLACKLIST" +__handlers__ = [BLACKLIST_HANDLER, ADD_BLACKLIST_HANDLER, UNBLACKLIST_HANDLER, (BLACKLIST_DEL_HANDLER, BLACKLIST_GROUP)] diff --git a/fire_bot/modules/blacklist2.py b/fire_bot/modules/blacklist2.py new file mode 100644 index 0000000..93acd9c --- /dev/null +++ b/fire_bot/modules/blacklist2.py @@ -0,0 +1,382 @@ +import html +from typing import Optional, List + +import telegram.ext as tg +from telegram import Message, Chat, Update, Bot, ParseMode, User, MessageEntity +from telegram import TelegramError +from telegram.error import BadRequest +from telegram.ext import CommandHandler, MessageHandler, Filters +from telegram.ext.dispatcher import run_async +from telegram.utils.helpers import mention_html, mention_markdown + +import tg_bot.modules.sql.blsticker_sql as sql +from tg_bot import dispatcher, SUDO_USERS, LOGGER, OWNER_ID +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import can_delete, is_user_admin, user_not_admin, user_admin, \ + bot_can_delete, is_bot_admin +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.helper_funcs.misc import split_message +from tg_bot.modules.warns import warn +from tg_bot.modules.log_channel import loggable +from tg_bot.modules.sql import users_sql +from tg_bot.modules.connection import connected + +from tg_bot.modules.helper_funcs.alternate import send_message + + +@run_async +def blackliststicker(bot: Bot, update: Update, args: List[str]): + msg = update.effective_message # type: Optional[Message] + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + + + conn = connected(bot, update, chat, user.id, need_admin=False) + if conn: + chat_id = conn + chat_name = dispatcher.bot.getChat(conn).title + else: + if chat.type == "private": + return + else: + chat_id = update.effective_chat.id + chat_name = chat.title + + sticker_list = "List black stickers currently in {}:\n".format(chat_name) + + all_stickerlist = sql.get_chat_stickers(chat_id) + + if len(args) > 0 and args[0].lower() == 'copy': + for trigger in all_stickerlist: + sticker_list += "{}\n".format(html.escape(trigger)) + elif len(args) == 0: + for trigger in all_stickerlist: + sticker_list += " - {}\n".format(html.escape(trigger)) + + split_text = split_message(sticker_list) + for text in split_text: + if sticker_list == "List black stickers currently in {}:\n".format(chat_name).format(chat_name): + send_message(update.effective_message, "There is no blacklist sticker in {}!".format(chat_name), parse_mode=ParseMode.HTML) + return + send_message(update.effective_message, text, parse_mode=ParseMode.HTML) + + +@run_async +@user_admin +def add_blackliststicker(bot: Bot, update: Update): + msg = update.effective_message # type: Optional[Message] + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + words = msg.text.split(None, 1) + + conn = connected(bot, update, chat, user.id) + if conn: + chat_id = conn + chat_name = dispatcher.bot.getChat(conn).title + else: + chat_id = update.effective_chat.id + if chat.type == "private": + return + else: + chat_name = chat.title + + if len(words) > 1: + text = words[1].replace('https://t.me/addstickers/', '') + to_blacklist = list(set(trigger.strip() for trigger in text.split("\n") if trigger.strip())) + added = 0 + for trigger in to_blacklist: + try: + get = bot.getStickerSet(trigger) + sql.add_to_stickers(chat_id, trigger.lower()) + added += 1 + except BadRequest: + send_message(update.effective_message, "Sticker `{}` can not be found!".format(trigger), parse_mode="markdown") + + if added == 0: + return + + if len(to_blacklist) == 1: + send_message(update.effective_message, "Sticker {} added to blacklist stickers in {}!".format(html.escape(to_blacklist[0]), chat_name), + parse_mode=ParseMode.HTML) + else: + send_message(update.effective_message, "{} stickers added to blacklist sticker in {}!".format(added, chat_name), parse_mode=ParseMode.HTML) + elif msg.reply_to_message: + added = 0 + trigger = msg.reply_to_message.sticker.set_name + if trigger == None: + send_message(update.effective_message, "Sticker is invalid!") + return + try: + get = bot.getStickerSet(trigger) + sql.add_to_stickers(chat_id, trigger.lower()) + added += 1 + except BadRequest: + send_message(update.effective_message, "Sticker `{}` can not be found!".format(trigger), parse_mode="markdown") + + if added == 0: + return + + send_message(update.effective_message, "Sticker {} added to blacklist stickers in {}!".format(trigger, chat_name), parse_mode=ParseMode.HTML) + else: + send_message(update.effective_message, "Tell me what stickers you want to add to the blacklist stickers.") + +@run_async +@user_admin +def unblackliststicker(bot: Bot, update: Update): + msg = update.effective_message # type: Optional[Message] + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + words = msg.text.split(None, 1) + + conn = connected(bot, update, chat, user.id) + if conn: + chat_id = conn + chat_name = dispatcher.bot.getChat(conn).title + else: + chat_id = update.effective_chat.id + if chat.type == "private": + return + else: + chat_name = chat.title + + + if len(words) > 1: + text = words[1].replace('https://t.me/addstickers/', '') + to_unblacklist = list(set(trigger.strip() for trigger in text.split("\n") if trigger.strip())) + successful = 0 + for trigger in to_unblacklist: + success = sql.rm_from_stickers(chat_id, trigger.lower()) + if success: + successful += 1 + + if len(to_unblacklist) == 1: + if successful: + send_message(update.effective_message, "Sticker {} deleted from blacklist in {}!".format(html.escape(to_unblacklist[0]), chat_name), + parse_mode=ParseMode.HTML) + else: + send_message(update.effective_message, "This is not on the blacklist stickers...!") + + elif successful == len(to_unblacklist): + send_message(update.effective_message, "Sticker {} deleted from blacklist in {}!".format( + successful, chat_name), parse_mode=ParseMode.HTML) + + elif not successful: + send_message(update.effective_message, "None of these stickers exist, so they cannot be removed.".format( + successful, len(to_unblacklist) - successful), parse_mode=ParseMode.HTML) + + else: + send_message(update.effective_message, "Sticker {} deleted from blacklist. {} did not exist, so it's not deleted.".format(successful, len(to_unblacklist) - successful), + parse_mode=ParseMode.HTML) + elif msg.reply_to_message: + trigger = msg.reply_to_message.sticker.set_name + if trigger == None: + send_message(update.effective_message, "Sticker is invalid!") + return + success = sql.rm_from_stickers(chat_id, trigger.lower()) + + if success: + send_message(update.effective_message, "Sticker {} deleted from blacklist in {}!".format(trigger, chat_name), + parse_mode=ParseMode.HTML) + else: + send_message(update.effective_message, "{} not found on blacklisted stickers...!".format(trigger)) + else: + send_message(update.effective_message, "Tell me what stickers you want to add to the blacklist stickers.") + +@run_async +@loggable +@user_admin +def blacklist_mode(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + msg = update.effective_message # type: Optional[Message] + + + conn = connected(bot, update, chat, user.id, need_admin=True) + if conn: + chat = dispatcher.bot.getChat(conn) + chat_id = conn + chat_name = dispatcher.bot.getChat(conn).title + else: + if update.effective_message.chat.type == "private": + send_message(update.effective_message, "You can do this command in groups, not PM") + return "" + chat = update.effective_chat + chat_id = update.effective_chat.id + chat_name = update.effective_message.chat.title + + if args: + if args[0].lower() == 'off' or args[0].lower() == 'nothing' or args[0].lower() == 'no': + settypeblacklist = 'turn off' + sql.set_blacklist_strength(chat_id, 0, "0") + elif args[0].lower() == 'del' or args[0].lower() == 'delete': + settypeblacklist = 'left, the message will be deleted' + sql.set_blacklist_strength(chat_id, 1, "0") + elif args[0].lower() == 'warn': + settypeblacklist = 'warned' + sql.set_blacklist_strength(chat_id, 2, "0") + elif args[0].lower() == 'mute': + settypeblacklist = 'muted' + sql.set_blacklist_strength(chat_id, 3, "0") + elif args[0].lower() == 'kick': + settypeblacklist = 'kicked' + sql.set_blacklist_strength(chat_id, 4, "0") + elif args[0].lower() == 'ban': + settypeblacklist = 'banned' + sql.set_blacklist_strength(chat_id, 5, "0") + elif args[0].lower() == 'tban': + if len(args) == 1: + teks = """It looks like you are trying to set a temporary value to blacklist, but has not determined the time; use `/blstickermode tban `. + Examples of time values: 4m = 4 minute, 3h = 3 hours, 6d = 6 days, 5w = 5 weeks.""" + send_message(update.effective_message, teks, parse_mode="markdown") + return + settypeblacklist = 'temporary banned for {}'.format(args[1]) + sql.set_blacklist_strength(chat_id, 6, str(args[1])) + elif args[0].lower() == 'tmute': + if len(args) == 1: + teks = """It looks like you are trying to set a temporary value to blacklist, but has not determined the time; use `/blstickermode tmute `. + Examples of time values: 4m = 4 minute, 3h = 3 hours, 6d = 6 days, 5w = 5 weeks.""" + send_message(update.effective_message, teks, parse_mode="markdown") + return + settypeblacklist = tl(update.effective_message, 'temporary muted for {}').format(args[1]) + sql.set_blacklist_strength(chat_id, 7, str(args[1])) + else: + send_message(update.effective_message, "I only understand off/del/warn/ban/kick/mute/tban/tmute!") + return + if conn: + text = "Blacklist sticker mode changed, will `{}` at *{}*!".format(settypeblacklist, chat_name) + else: + text = "Blacklist sticker mode changed, will `{}`!".format(settypeblacklist) + send_message(update.effective_message, text, parse_mode="markdown") + return "{}:\n" \ + "Admin: {}\n" \ + "Changed sticker blacklist mode. Will {}.".format(html.escape(chat.title), + mention_html(user.id, user.first_name), settypeblacklist) + else: + getmode, getvalue = sql.get_blacklist_setting(chat.id) + if getmode == 0: + settypeblacklist = "not active" + elif getmode == 1: + settypeblacklist = "hapus" + elif getmode == 2: + settypeblacklist = "warn" + elif getmode == 3: + settypeblacklist = "mute" + elif getmode == 4: + settypeblacklist = "kick" + elif getmode == 5: + settypeblacklist = "ban" + elif getmode == 6: + settypeblacklist = "temporarily banned for {}".format(getvalue) + elif getmode == 7: + settypeblacklist = "temporarily muted for {}".format(getvalue) + if conn: + text = "Blacklist sticker mode is currently set to *{}* in *{}*.".format(settypeblacklist, chat_name) + else: + text = "Blacklist sticker mode is currently set to *{}*.".format(settypeblacklist) + send_message(update.effective_message, text, parse_mode=ParseMode.MARKDOWN) + return "" + +@run_async +@user_not_admin +def del_blackliststicker(bot: Bot, update: Update): + chat = update.effective_chat # type: Optional[Chat] + message = update.effective_message # type: Optional[Message] + user = update.effective_user + to_match = message.sticker + if not to_match: + return + + getmode, value = sql.get_blacklist_setting(chat.id) + + chat_filters = sql.get_chat_stickers(chat.id) + for trigger in chat_filters: + if to_match.set_name.lower() == trigger.lower(): + try: + if getmode == 0: + return + elif getmode == 1: + message.delete() + elif getmode == 2: + message.delete() + warn(update.effective_user, chat, "Using sticker '{}' which in blacklist stickers".format(trigger), message, update.effective_user, conn=False) + return + elif getmode == 3: + message.delete() + bot.restrict_chat_member(chat.id, update.effective_user.id, can_send_messages=False) + bot.sendMessage(chat.id, "{} muted because using '{}' which in blacklist stickers".format(mention_markdown(user.id, user.first_name), trigger), parse_mode="markdown") + return + elif getmode == 4: + message.delete() + res = chat.unban_member(update.effective_user.id) + if res: + bot.sendMessage(chat.id, "{} kicked because using '{}' which in blacklist stickers".format(mention_markdown(user.id, user.first_name), trigger), parse_mode="markdown") + return + elif getmode == 5: + message.delete() + chat.kick_member(user.id) + bot.sendMessage(chat.id, "{} banned because using '{}' which in blacklist stickers".format(mention_markdown(user.id, user.first_name), trigger), parse_mode="markdown") + return + elif getmode == 6: + message.delete() + bantime = extract_time(message, value) + chat.kick_member(user.id, until_date=bantime) + bot.sendMessage(chat.id, "{} banned for {} because using '{}' which in blacklist stickers".format(mention_markdown(user.id, user.first_name), value, trigger), parse_mode="markdown") + return + elif getmode == 7: + message.delete() + mutetime = extract_time(message, value) + bot.restrict_chat_member(chat.id, user.id, until_date=mutetime, can_send_messages=False) + bot.sendMessage(chat.id, "{} muted for {} because using '{}' which in blacklist stickers".format(mention_markdown(user.id, user.first_name), value, trigger), parse_mode="markdown") + return + except BadRequest as excp: + if excp.message == "Message to delete not found": + pass + else: + LOGGER.exception("Error while deleting blacklist message.") + break + + +def __import_data__(chat_id, data): + # set chat blacklist + blacklist = data.get('sticker_blacklist', {}) + for trigger in blacklist: + sql.add_to_blacklist(chat_id, trigger) + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + blacklisted = sql.num_stickers_chat_filters(chat_id) + return "There are `{} `blacklisted stickers.".format(blacklisted) + +def __stats__(): + return "{} blacklist stickers, across {} chats.".format(sql.num_stickers_filters(), sql.num_stickers_filter_chats()) + +__help__ = """ +Blacklist sticker is used to stop certain stickers. Whenever a sticker is sent, the message will be deleted immediately. +*NOTE:* Blacklist stickers do not affect the group admin. + - /blsticker: See current blacklisted sticker. +*Only admin:* + - /addblsticker : Add the sticker trigger to the black list. Can be added via reply sticker. + - /unblsticker : Remove triggers from blacklist. The same newline logic applies here, so you can delete multiple triggers at once. + - /rmblsticker : Same as above. + - /blstickermode ban/tban/mute/tmute . +Note: + - `` can be `https://t.me/addstickers/` or just `` or reply to the sticker message. +""" + +__mod_name__ = "S BLACKLIST" + +BLACKLIST_STICKER_HANDLER = DisableAbleCommandHandler("blsticker", blackliststicker, pass_args=True, admin_ok=True) +ADDBLACKLIST_STICKER_HANDLER = DisableAbleCommandHandler("addblsticker", add_blackliststicker) +UNBLACKLIST_STICKER_HANDLER = CommandHandler(["unblsticker", "rmblsticker"], unblackliststicker) +BLACKLISTMODE_HANDLER = CommandHandler("blstickermode", blacklist_mode, pass_args=True) +BLACKLIST_STICKER_DEL_HANDLER = MessageHandler(Filters.sticker & Filters.group, del_blackliststicker) + +dispatcher.add_handler(BLACKLIST_STICKER_HANDLER) +dispatcher.add_handler(ADDBLACKLIST_STICKER_HANDLER) +dispatcher.add_handler(UNBLACKLIST_STICKER_HANDLER) +dispatcher.add_handler(BLACKLISTMODE_HANDLER) +dispatcher.add_handler(BLACKLIST_STICKER_DEL_HANDLER) diff --git a/fire_bot/modules/blacklistusers.py b/fire_bot/modules/blacklistusers.py new file mode 100644 index 0000000..3fccd98 --- /dev/null +++ b/fire_bot/modules/blacklistusers.py @@ -0,0 +1,150 @@ +# Module to blacklist users and prevent them from using commands by @TheRealPhoenix +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.error import BadRequest +from telegram.ext import CommandHandler, run_async +from telegram.utils.helpers import mention_html + +import tg_bot.modules.sql.blacklistusers_sql as sql +from tg_bot import dispatcher, OWNER_ID, DEV_USERS, SUDO_USERS, WHITELIST_USERS, SUPPORT_USERS +from tg_bot.modules.helper_funcs.chat_status import dev_plus +from tg_bot.modules.helper_funcs.extraction import extract_user_and_text, extract_user +from tg_bot.modules.log_channel import gloggable + +BLACKLISTWHITELIST = [OWNER_ID] + DEV_USERS + SUDO_USERS + WHITELIST_USERS + SUPPORT_USERS +BLABLEUSERS = [OWNER_ID] + DEV_USERS + + +@run_async +@dev_plus +@gloggable +def bl_user(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + + user_id, reason = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("I doubt that's a user.") + return "" + + if user_id == bot.id: + message.reply_text("How am I supposed to do my work if I am ignoring myself?") + return "" + + if user_id in BLACKLISTWHITELIST: + message.reply_text("No!\nNoticing Disasters is my job.") + return "" + + try: + target_user = bot.get_chat(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user.") + return "" + else: + raise + + sql.blacklist_user(user_id, reason) + message.reply_text("I shall ignore the existence of this user!") + log_message = (f"#BLACKLIST\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(target_user.id, target_user.first_name)}") + if reason: + log_message += f"\nReason: {reason}" + + return log_message + + +@run_async +@dev_plus +@gloggable +def unbl_user(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + + user_id = extract_user(message, args) + + if not user_id: + message.reply_text("I doubt that's a user.") + return "" + + if user_id == bot.id: + message.reply_text("I always notice myself.") + return "" + + try: + target_user = bot.get_chat(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user.") + return "" + else: + raise + + if sql.is_user_blacklisted(user_id): + + sql.unblacklist_user(user_id) + message.reply_text("*notices user*") + log_message = (f"#UNBLACKLIST\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(target_user.id, target_user.first_name)}") + + return log_message + + else: + message.reply_text("I am not ignoring them at all though!") + return "" + + +@run_async +@dev_plus +def bl_users(bot: Bot, update: Update): + users = [] + + for each_user in sql.BLACKLIST_USERS: + + user = bot.get_chat(each_user) + reason = sql.get_reason(each_user) + + if reason: + users.append(f"• {mention_html(user.id, user.first_name)} :- {reason}") + else: + users.append(f"• {mention_html(user.id, user.first_name)}") + + message = "Blacklisted Users\n" + if not users: + message += "Noone is being ignored as of yet." + else: + message += '\n'.join(users) + + update.effective_message.reply_text(message, parse_mode=ParseMode.HTML) + + +def __user_info__(user_id): + is_blacklisted = sql.is_user_blacklisted(user_id) + + text = "Globally Ignored: {}" + + if is_blacklisted: + text = text.format("Yes") + reason = sql.get_reason(user_id) + if reason: + text += f"\nReason: {reason}" + else: + text = text.format("No") + + return text + + +BL_HANDLER = CommandHandler("ignore", bl_user, pass_args=True) +UNBL_HANDLER = CommandHandler("notice", unbl_user, pass_args=True) +BLUSERS_HANDLER = CommandHandler("ignoredlist", bl_users) + +dispatcher.add_handler(BL_HANDLER) +dispatcher.add_handler(UNBL_HANDLER) +dispatcher.add_handler(BLUSERS_HANDLER) + +__mod_name__ = "BLACKLISTING USERS" +__handlers__ = [BL_HANDLER, UNBL_HANDLER, BLUSERS_HANDLER] diff --git a/fire_bot/modules/chatbot.py b/fire_bot/modules/chatbot.py new file mode 100644 index 0000000..5a92e8a --- /dev/null +++ b/fire_bot/modules/chatbot.py @@ -0,0 +1,108 @@ +# AI module using Intellivoid's Coffeehouse API by @TheRealPhoenix +from time import time, sleep + +from coffeehouse.lydia import LydiaAI +from coffeehouse.api import API +from coffeehouse.exception import CoffeeHouseError as CFError + +from telegram import Message, Chat, User, Update, Bot +from telegram.ext import CommandHandler, MessageHandler, Filters, run_async + +from tg_bot import dispatcher, AI_API_KEY, OWNER_ID +import tg_bot.modules.sql.chatbot_sql as sql +from tg_bot.modules.helper_funcs.filters import CustomFilters + + +CoffeeHouseAPI = API(AI_API_KEY) +api_client = LydiaAI(CoffeeHouseAPI) + + +@run_async +def add_chat(bot: Bot, update: Update): + global api_client + chat_id = update.effective_chat.id + msg = update.effective_message + is_chat = sql.is_chat(chat_id) + if not is_chat: + ses = api_client.create_session() + ses_id = str(ses.id) + expires = str(ses.expires) + sql.set_ses(chat_id, ses_id, expires) + msg.reply_text("AI successfully enabled for this chat!") + else: + msg.reply_text("AI is already enabled for this chat!") + + +@run_async +def remove_chat(bot: Bot, update: Update): + msg = update.effective_message + chat_id = update.effective_chat.id + is_chat = sql.is_chat(chat_id) + if not is_chat: + msg.reply_text("AI isn't enabled here in the first place!") + else: + sql.rem_chat(chat_id) + msg.reply_text("AI disabled successfully!") + + +def check_message(bot: Bot, message): + reply_msg = message.reply_to_message + if message.text.lower() == "saitama": + return True + if reply_msg: + if reply_msg.from_user.id == bot.get_me().id: + return True + else: + return False + + +@run_async +def chatbot(bot: Bot, update: Update): + global api_client + msg = update.effective_message + chat_id = update.effective_chat.id + is_chat = sql.is_chat(chat_id) + if not is_chat: + return + if msg.text and not msg.document: + if not check_message(bot, msg): + return + sesh, exp = sql.get_ses(chat_id) + query = msg.text + try: + if int(exp) < time(): + ses = api_client.create_session() + ses_id = str(ses.id) + expires = str(ses.expires) + sql.set_ses(chat_id, ses_id, expires) + sesh, exp = sql.get_ses(chat_id) + except ValueError: + pass + try: + bot.send_chat_action(chat_id, action='typing') + rep = api_client.think_thought(sesh, query) + sleep(0.3) + msg.reply_text(rep, timeout=60) + except CFError as e: + bot.send_message(OWNER_ID, f"Chatbot error: {e} occurred in {chat_id}!") + + +__mod_name__ = "CHAT BOT" + +__help__ = """ + +Powered by CoffeeHouse (https://coffeehouse.intellivoid.net/) from @Intellivoid + + - /addchat : Enables Chatbot mode in the chat. + - /rmchat : Disables Chatbot mode in the chat. +""" + +ADD_CHAT_HANDLER = CommandHandler("addchat", add_chat, filters=CustomFilters.dev_filter) +REMOVE_CHAT_HANDLER = CommandHandler("rmchat", remove_chat, filters=CustomFilters.dev_filter) +CHATBOT_HANDLER = MessageHandler(Filters.text & (~Filters.regex(r"^#[^\s]+") & ~Filters.regex(r"^!") + & ~Filters.regex(r"^s\/")), chatbot) +# Filters for ignoring #note messages, !commands and sed. + +dispatcher.add_handler(ADD_CHAT_HANDLER) +dispatcher.add_handler(REMOVE_CHAT_HANDLER) +dispatcher.add_handler(CHATBOT_HANDLER) diff --git a/fire_bot/modules/cleaner.py b/fire_bot/modules/cleaner.py new file mode 100644 index 0000000..7ec2adc --- /dev/null +++ b/fire_bot/modules/cleaner.py @@ -0,0 +1,229 @@ +import html + +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.ext import CommandHandler, MessageHandler, Filters, run_async + +from tg_bot import ALLOW_EXCL, dispatcher, CustomCommandHandler +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import user_admin, bot_can_delete, dev_plus, connection_status +from tg_bot.modules.sql import cleaner_sql as sql + +if ALLOW_EXCL: + CMD_STARTERS = ('/', '!') +else: + CMD_STARTERS = ('/') + +BLUE_TEXT_CLEAN_GROUP = 15 +CommandHandlerList = (CommandHandler, CustomCommandHandler, DisableAbleCommandHandler) +command_list = ["cleanblue", "ignoreblue", "unignoreblue", "listblue", "ungignoreblue", "gignoreblue" + "start", "help", "settings", "donate", "stalk", "aka", "leaderboard"] + +for handler_list in dispatcher.handlers: + for handler in dispatcher.handlers[handler_list]: + if any(isinstance(handler, cmd_handler) for cmd_handler in CommandHandlerList): + command_list += handler.command + + +@run_async +def clean_blue_text_must_click(bot: Bot, update: Update): + + chat = update.effective_chat + message = update.effective_message + + if chat.get_member(bot.id).can_delete_messages: + if sql.is_enabled(chat.id): + fst_word = message.text.strip().split(None, 1)[0] + + if len(fst_word) > 1 and any(fst_word.startswith(start) for start in CMD_STARTERS): + + command = fst_word[1:].split('@') + chat = update.effective_chat + + ignored = sql.is_command_ignored(chat.id, command[0]) + if ignored: + return + + if command[0] not in command_list: + message.delete() + + +@run_async +@connection_status +@bot_can_delete +@user_admin +def set_blue_text_must_click(bot: Bot, update: Update, args: List[str]): + + chat = update.effective_chat + message = update.effective_message + + if len(args) >= 1: + val = args[0].lower() + if val == "off" or val == "no": + sql.set_cleanbt(chat.id, False) + reply = "Bluetext cleaning has been disabled for {}".format(html.escape(chat.title)) + message.reply_text(reply, parse_mode=ParseMode.HTML) + + elif val == "yes" or val == "on": + sql.set_cleanbt(chat.id, True) + reply = "Bluetext cleaning has been enabled for {}".format(html.escape(chat.title)) + message.reply_text(reply, parse_mode=ParseMode.HTML) + + else: + reply = "Invalid argument.Accepted values are 'yes', 'on', 'no', 'off'" + message.reply_text(reply) + else: + clean_status = sql.is_enabled(chat.id) + if clean_status: + clean_status = "Enabled" + else: + clean_status = "Disabled" + reply = "Bluetext cleaning for {} : {}".format(chat.title, clean_status) + message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@user_admin +def add_bluetext_ignore(bot: Bot, update: Update, args: List[str]): + + message = update.effective_message + chat = update.effective_chat + + if len(args) >= 1: + val = args[0].lower() + added = sql.chat_ignore_command(chat.id, val) + if added: + reply = "{} has been added to bluetext cleaner ignore list.".format(args[0]) + else: + reply = "Command is already ignored." + message.reply_text(reply, parse_mode=ParseMode.HTML) + + else: + reply = "No command supplied to be ignored." + message.reply_text(reply) + + +@run_async +@user_admin +def remove_bluetext_ignore(bot: Bot, update: Update, args: List[str]): + + message = update.effective_message + chat = update.effective_chat + + if len(args) >= 1: + val = args[0].lower() + removed = sql.chat_unignore_command(chat.id, val) + if removed: + reply = "{} has been removed from bluetext cleaner ignore list.".format(args[0]) + else: + reply = "Command isn't ignored currently." + message.reply_text(reply, parse_mode=ParseMode.HTML) + + else: + reply = "No command supplied to be unignored." + message.reply_text(reply) + + +@run_async +@user_admin +def add_bluetext_ignore_global(bot: Bot, update: Update, args: List[str]): + + message = update.effective_message + + if len(args) >= 1: + val = args[0].lower() + added = sql.global_ignore_command(val) + if added: + reply = "{} has been added to global bluetext cleaner ignore list.".format(args[0]) + else: + reply = "Command is already ignored." + message.reply_text(reply, parse_mode=ParseMode.HTML) + + else: + reply = "No command supplied to be ignored." + message.reply_text(reply) + + +@run_async +@dev_plus +def remove_bluetext_ignore_global(bot: Bot, update: Update, args: List[str]): + + message = update.effective_message + + if len(args) >= 1: + val = args[0].lower() + removed = sql.global_unignore_command(val) + if removed: + reply = "{} has been removed from global bluetext cleaner ignore list.".format(args[0]) + else: + reply = "Command isn't ignored currently." + message.reply_text(reply, parse_mode=ParseMode.HTML) + + else: + reply = "No command supplied to be unignored." + message.reply_text(reply) + + +@run_async +@dev_plus +def bluetext_ignore_list(bot: Bot, update: Update): + + message = update.effective_message + chat = update.effective_chat + + global_ignored_list, local_ignore_list = sql.get_all_ignored(chat.id) + text = "" + + if global_ignored_list: + text = "The following commands are currently ignored globally from bluetext cleaning :\n" + + for x in global_ignored_list: + text += f" - {x}\n" + + if local_ignore_list: + text += "\nThe following commands are currently ignored locally from bluetext cleaning :\n" + + for x in local_ignore_list: + text += f" - {x}\n" + + if text == "": + text = "No commands are currently ignored from bluetext cleaning." + message.reply_text(text) + return + + message.reply_text(text, parse_mode=ParseMode.HTML) + return + + +__help__ = """ + - /cleanblue - clean commands after sending + - /ignoreblue - prevent auto cleaning of the command + - /unignoreblue - remove prevent auto cleaning of the command + - /listblue - list currently whitelisted commands + + Following are Disasters only commands, admins cannot use these: + - /gignoreblue - globally ignore bluetext cleaning. + - /ungignoreblue - remove said command from global cleaning list +""" + +SET_CLEAN_BLUE_TEXT_HANDLER = CommandHandler("cleanblue", set_blue_text_must_click, pass_args=True) +ADD_CLEAN_BLUE_TEXT_HANDLER = CommandHandler("ignoreblue", add_bluetext_ignore, pass_args=True) +REMOVE_CLEAN_BLUE_TEXT_HANDLER = CommandHandler("unignoreblue", remove_bluetext_ignore, pass_args=True) +ADD_CLEAN_BLUE_TEXT_GLOBAL_HANDLER = CommandHandler("gignoreblue", add_bluetext_ignore_global, pass_args=True) +REMOVE_CLEAN_BLUE_TEXT_GLOBAL_HANDLER = CommandHandler("ungignoreblue", remove_bluetext_ignore_global, pass_args=True) +LIST_CLEAN_BLUE_TEXT_HANDLER = CommandHandler("listblue", bluetext_ignore_list) +CLEAN_BLUE_TEXT_HANDLER = MessageHandler(Filters.command & Filters.group, clean_blue_text_must_click) + +dispatcher.add_handler(SET_CLEAN_BLUE_TEXT_HANDLER) +dispatcher.add_handler(ADD_CLEAN_BLUE_TEXT_HANDLER) +dispatcher.add_handler(REMOVE_CLEAN_BLUE_TEXT_HANDLER) +dispatcher.add_handler(ADD_CLEAN_BLUE_TEXT_GLOBAL_HANDLER) +dispatcher.add_handler(REMOVE_CLEAN_BLUE_TEXT_GLOBAL_HANDLER) +dispatcher.add_handler(LIST_CLEAN_BLUE_TEXT_HANDLER) +dispatcher.add_handler(CLEAN_BLUE_TEXT_HANDLER, BLUE_TEXT_CLEAN_GROUP) + +__mod_name__ = "BlUETEXT CLEANING" +__handlers__ = [SET_CLEAN_BLUE_TEXT_HANDLER, ADD_CLEAN_BLUE_TEXT_HANDLER, REMOVE_CLEAN_BLUE_TEXT_HANDLER, + ADD_CLEAN_BLUE_TEXT_GLOBAL_HANDLER, REMOVE_CLEAN_BLUE_TEXT_GLOBAL_HANDLER, + LIST_CLEAN_BLUE_TEXT_HANDLER, (CLEAN_BLUE_TEXT_HANDLER, BLUE_TEXT_CLEAN_GROUP)] diff --git a/fire_bot/modules/combot_antispam_system.py b/fire_bot/modules/combot_antispam_system.py new file mode 100644 index 0000000..617ad8b --- /dev/null +++ b/fire_bot/modules/combot_antispam_system.py @@ -0,0 +1,349 @@ +import html, time +import re +from typing import Optional, List + +import tg_bot.modules.helper_funcs.cas_api as cas + +from telegram import Message, Chat, Update, Bot, User, CallbackQuery, ChatMember, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton, MessageEntity +from telegram.error import BadRequest +from tg_bot import dispatcher, OWNER_ID, DEV_USERS, SUDO_USERS, SUPPORT_USERS, TIGER_USERS, WHITELIST_USERS, LOGGER +from telegram.ext import MessageHandler, Filters, CommandHandler, run_async, CallbackQueryHandler +from telegram.utils.helpers import mention_markdown, mention_html, escape_markdown + +import tg_bot.modules.sql.welcome_sql as sql +import tg_bot.modules.sql.global_bans_sql as gbansql +import tg_bot.modules.sql.users_sql as userssql + +from tg_bot import dispatcher, OWNER_ID, LOGGER, SUDO_USERS, SUPPORT_USERS +from tg_bot.modules.helper_funcs.chat_status import user_admin, can_delete, is_user_ban_protected +from tg_bot.modules.helper_funcs.misc import build_keyboard, revert_buttons, send_to_list +from tg_bot.modules.helper_funcs.msg_types import get_welcome_type +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.helper_funcs.string_handling import markdown_parser, escape_invalid_curly_brackets +from tg_bot.modules.log_channel import loggable + + +@run_async +@user_admin +def setcas(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + split_msg = msg.text.split(' ') + if len(split_msg)!= 2: + msg.reply_text("Invalid arguments!") + return + param = split_msg[1] + if param == "on" or param == "true": + sql.set_cas_status(chat.id, True) + msg.reply_text("Successfully updated configuration.") + return + elif param == "off" or param == "false": + sql.set_cas_status(chat.id, False) + msg.reply_text("Successfully updated configuration.") + return + else: + msg.reply_text("Invalid status to set!") #on or off ffs + return + +@run_async +@user_admin +def setban(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + split_msg = msg.text.split(' ') + if len(split_msg)!= 2: + msg.reply_text("Invalid arguments!") + return + param = split_msg[1] + if param == "on" or param == "true": + sql.set_cas_autoban(chat.id, True) + msg.reply_text("Successfully updated configuration.") + return + elif param == "off" or param == "false": + sql.set_cas_autoban(chat.id, False) + msg.reply_text("Successfully updated configuration.") + return + else: + msg.reply_text("Invalid autoban definition to set!") #on or off ffs + return + +@run_async +@user_admin +def get_current_setting(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + stats = sql.get_cas_status(chat.id) + autoban = sql.get_cas_autoban(chat.id) + rtext = "CAS Preferences\n\nCAS Checking: {}\nAutoban: {}".format(stats, autoban) + msg.reply_text(rtext, parse_mode=ParseMode.HTML) + return + +@run_async +@user_admin +def getTimeSetting(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + timeSetting = sql.getKickTime(chat.id) + text = "This group will automatically kick people in " + str(timeSetting) + " seconds." + msg.reply_text(text) + return + +@run_async +@user_admin +def setTimeSetting(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + msg = update.effective_message + if (not args) or len(args) != 1 or (not args[0].isdigit()): + msg.reply_text("Give me a valid value to set! 30 to 900 secs") + return + value = int(args[0]) + if value < 30 or value > 900: + msg.reply_text("Invalid value! Please use a value between 30 and 900 seconds (15 minutes)") + return + sql.setKickTime(str(chat.id), value) + msg.reply_text("Success! Users that don't confirm being people will be kicked after " + str(value) + " seconds.") + return + +@run_async +def get_version(bot: Bot, update: Update): + msg = update.effective_message + ver = cas.vercheck() + msg.reply_text("CAS API version: "+ver) + return + +@run_async +def caschecker(bot: Bot, update: Update, args: List[str]): + #/info logic + msg = update.effective_message # type: Optional[Message] + user_id = extract_user(update.effective_message, args) + if user_id and int(user_id) != 777000: + user = bot.get_chat(user_id) + elif user_id and int(user_id) == 777000: + msg.reply_text("This is Telegram. Unless you manually entered this reserved account's ID, it is likely a broadcast from a linked channel.") + return + 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 can't extract a user from this.") + return + else: + return + + text = "CAS Check:" \ + "\nID: {}" \ + "\nFirst Name: {}".format(user.id, html.escape(user.first_name)) + if user.last_name: + text += "\nLast Name: {}".format(html.escape(user.last_name)) + if user.username: + text += "\nUsername: @{}".format(html.escape(user.username)) + text += "\n\nCAS Banned: " + result = cas.banchecker(user.id) + text += str(result) + if result: + parsing = cas.offenses(user.id) + if parsing: + text += "\nTotal of Offenses: " + text += str(parsing) + parsing = cas.timeadded(user.id) + if parsing: + parseArray=str(parsing).split(", ") + text += "\nDay added: " + text += str(parseArray[1]) + text += "\nTime added: " + text += str(parseArray[0]) + text += "\n\nAll times are in UTC" + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + + + +#this sends direct request to combot server. Will return true if user is banned, false if +#id invalid or user not banned +@run_async +def casquery(bot: Bot, update: Update, args: List[str]): + msg = update.effective_message # type: Optional[Message] + try: + user_id = msg.text.split(' ')[1] + except: + msg.reply_text("There was a problem parsing the query.") + return + text = "Your query returned: " + result = cas.banchecker(user_id) + text += str(result) + msg.reply_text(text) + + +@run_async +def gbanChat(bot: Bot, update: Update, args: List[str]): + if args and len(args) == 1: + chat_id = str(args[0]) + del args[0] + try: + banner = update.effective_user + send_to_list(bot, SUDO_USERS, + "Chat Blacklist" \ + "\n#BLCHAT" \ + "\nStatus: Blacklisted" \ + "\nSudo Admin: {}" \ + "\nChat Name: {}" \ + "\nID: {}".format(mention_html(banner.id, banner.first_name),userssql.get_chat_name(chat_id),chat_id), html=True) + sql.blacklistChat(chat_id) + update.effective_message.reply_text("Chat has been successfully blacklisted!") + try: + bot.leave_chat(int(chat_id)) + except: + pass + except: + update.effective_message.reply_text("Error blacklisting chat!") + else: + update.effective_message.reply_text("Give me a valid chat id!") + +@run_async +def ungbanChat(bot: Bot, update: Update, args: List[str]): + if args and len(args) == 1: + chat_id = str(args[0]) + del args[0] + try: + banner = update.effective_user + send_to_list(bot, SUDO_USERS, + "Regression of Chat Blacklist" \ + "\n#UNBLCHAT" \ + "\nStatus: Un-Blacklisted" \ + "\nSudo Admin: {}" \ + "\nChat Name: {}" \ + "\nID: {}".format(mention_html(banner.id, banner.first_name),userssql.get_chat_name(chat_id),chat_id), html=True) + sql.unblacklistChat(chat_id) + update.effective_message.reply_text("Chat has been successfully un-blacklisted!") + except: + update.effective_message.reply_text("Error unblacklisting chat!") + else: + update.effective_message.reply_text("Give me a valid chat id!") + +@run_async +@user_admin +def setDefense(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + msg = update.effective_message + if len(args)!=1: + msg.reply_text("Invalid arguments!") + return + param = args[0] + if param == "on" or param == "true": + sql.setDefenseStatus(chat.id, True) + msg.reply_text("Defense mode has been turned on, this group is under attack. Every user that now joins will be auto kicked.") + return + elif param == "off" or param == "false": + sql.setDefenseStatus(chat.id, False) + msg.reply_text("Defense mode has been turned off, group is no longer under attack.") + return + else: + msg.reply_text("Invalid status to set!") #on or off ffs + return + +@run_async +@user_admin +def getDefense(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + stat = sql.getDefenseStatus(chat.id) + text = "Defense Status\n\nCurrently, this group has the defense setting set to: {}".format(stat) + msg.reply_text(text, parse_mode=ParseMode.HTML) + +# TODO: get welcome data from group butler snap +# def __import_data__(chat_id, data): +# welcome = data.get('info', {}).get('rules') +# welcome = welcome.replace('$username', '{username}') +# welcome = welcome.replace('$name', '{fullname}') +# welcome = welcome.replace('$id', '{id}') +# welcome = welcome.replace('$title', '{chatname}') +# welcome = welcome.replace('$surname', '{lastname}') +# welcome = welcome.replace('$rules', '{rules}') +# sql.set_custom_welcome(chat_id, welcome, sql.Types.TEXT) +ABOUT_CAS = "Combot Anti-Spam System (CAS)" \ + "\n\nCAS stands for Combot Anti-Spam, an automated system designed to detect spammers in Telegram groups."\ + "\nIf a user with any spam record connects to a CAS-secured group, the CAS system will ban that user immediately."\ + "\n\nCAS bans are permanent, non-negotiable, and cannot be removed by Combot community managers." \ + "\nIf a CAS ban is determined to have been issued incorrectly, it will automatically be removed." + +@run_async +def about_cas(bot: Bot, update: Update): + user = update.effective_message.from_user + chat = update.effective_chat # type: Optional[Chat] + + if chat.type == "private": + update.effective_message.reply_text(ABOUT_CAS, parse_mode=ParseMode.HTML) + + else: + try: + bot.send_message(user.id, ABOUT_CAS, parse_mode=ParseMode.HTML) + + update.effective_message.reply_text("You'll find in PM more info about CAS") + except Unauthorized: + update.effective_message.reply_text("Contact me in PM first to get CAS information.") + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + +def __chat_settings__(chat_id, user_id): + welcome_pref, _, _ = sql.get_welc_pref(chat_id) + goodbye_pref, _, _ = sql.get_gdbye_pref(chat_id) + return "This chat has it's welcome preference set to `{}`.\n" \ + "It's goodbye preference is `{}`.".format(welcome_pref, goodbye_pref) + +__help__ = """ +{} +Commands: + - /casver: Returns the API version that the bot is currently running + - /cascheck: Checks you or another user for CAS BAN +*Admin only:* + - /setcas : Enables/disables CAS Checking on welcome + - /getcas: Gets the current CAS settings + - /setban : Enables/disables autoban on CAS banned user detected. + - /setdefense : Turns on defense mode, will kick any new user automatically. + - /getdefense: gets the current defense setting + - /kicktime: gets the auto-kick time setting + - /setkicktime: sets new auto-kick time value (between 30 and 900 seconds) + - /cas: Info about CAS. (What is CAS?) +""" + +__mod_name__ = "CAS" + +SETCAS_HANDLER = CommandHandler("setcas", setcas, filters=Filters.group) +GETCAS_HANDLER = CommandHandler("getcas", get_current_setting, filters=Filters.group) +GETVER_HANDLER = DisableAbleCommandHandler("casver", get_version) +CASCHECK_HANDLER = CommandHandler("cascheck", caschecker, pass_args=True) +CASQUERY_HANDLER = CommandHandler("casquery", casquery, pass_args=True ,filters=CustomFilters.sudo_filter) +SETBAN_HANDLER = CommandHandler("setban", setban, filters=Filters.group) +GBANCHAT_HANDLER = CommandHandler("blchat", gbanChat, pass_args=True, filters=CustomFilters.sudo_filter) +UNGBANCHAT_HANDLER = CommandHandler("unblchat", ungbanChat, pass_args=True, filters=CustomFilters.sudo_filter) +DEFENSE_HANDLER = CommandHandler("setdefense", setDefense, pass_args=True) +GETDEF_HANDLER = CommandHandler("defense", getDefense) +GETTIMESET_HANDLER = CommandHandler("kicktime", getTimeSetting) +SETTIMER_HANDLER = CommandHandler("setkicktime", setTimeSetting, pass_args=True) +ABOUT_CAS_HANDLER = CommandHandler("cas", about_cas) + + + + + +dispatcher.add_handler(SETCAS_HANDLER) +dispatcher.add_handler(GETCAS_HANDLER) +dispatcher.add_handler(GETVER_HANDLER) +dispatcher.add_handler(CASCHECK_HANDLER) +dispatcher.add_handler(CASQUERY_HANDLER) +dispatcher.add_handler(SETBAN_HANDLER) +dispatcher.add_handler(GBANCHAT_HANDLER) +dispatcher.add_handler(UNGBANCHAT_HANDLER) +dispatcher.add_handler(DEFENSE_HANDLER) +dispatcher.add_handler(GETDEF_HANDLER) +dispatcher.add_handler(GETTIMESET_HANDLER) +dispatcher.add_handler(SETTIMER_HANDLER) +dispatcher.add_handler(ABOUT_CAS_HANDLER) + + + + diff --git a/fire_bot/modules/connection.py b/fire_bot/modules/connection.py new file mode 100644 index 0000000..66e41b9 --- /dev/null +++ b/fire_bot/modules/connection.py @@ -0,0 +1,303 @@ +import re +import time +from typing import List + +from telegram import Bot, Update, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.error import BadRequest, Unauthorized +from telegram.ext import CommandHandler, CallbackQueryHandler, run_async + +import tg_bot.modules.sql.connection_sql as sql +from tg_bot import dispatcher, SUDO_USERS, DEV_USERS +from tg_bot.modules.helper_funcs import chat_status +from tg_bot.modules.helper_funcs.alternate import send_message + +user_admin = chat_status.user_admin + +ADMIN_STATUS = ('administrator', 'creator') +MEMBER_STAUS = ('member',) + + +@user_admin +@run_async +def allow_connections(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + + if chat.type != chat.PRIVATE: + if len(args) >= 1: + var = args[0] + if var == "no": + sql.set_allow_connect_to_chat(chat.id, False) + send_message(update.effective_message, "Connection has been disabled for this chat") + elif var == "yes": + sql.set_allow_connect_to_chat(chat.id, True) + send_message(update.effective_message, "Connection has been enabled for this chat") + else: + send_message(update.effective_message, "Please enter `yes` or `no`!", parse_mode=ParseMode.MARKDOWN) + else: + get_settings = sql.allow_connect_to_chat(chat.id) + if get_settings: + send_message(update.effective_message, "Connections to this group are *Allowed* for members!", + parse_mode=ParseMode.MARKDOWN) + else: + send_message(update.effective_message, "Connection to this group are *Not Allowed* for members!", + parse_mode=ParseMode.MARKDOWN) + else: + send_message(update.effective_message, "This command is for group only. Not in PM!") + + +@run_async +def connection_chat(bot: Bot, update: Update): + chat = update.effective_chat + user = update.effective_user + msg = update.effective_message + + conn = connected(bot, update, chat, user.id, need_admin=True) + + if conn: + chat_name = dispatcher.bot.getChat(conn).title + else: + if msg.chat.type != "private": + return + chat_name = chat.title + + if conn: + message = "You are currently connected with {}.\n".format(chat_name) + else: + message = "You are currently not connected in any group.\n" + send_message(msg, message, parse_mode="markdown") + + +@run_async +def connect_chat(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + user = update.effective_user + msg = update.effective_message + + if chat.type == 'private': + if len(args) >= 1: + try: + connect_chat = int(args[0]) + getstatusadmin = bot.get_chat_member(connect_chat, msg.from_user.id) + except ValueError: + try: + connect_chat = str(args[0]) + get_chat = bot.getChat(connect_chat) + connect_chat = get_chat.id + getstatusadmin = bot.get_chat_member(connect_chat, msg.from_user.id) + except BadRequest: + send_message(msg, "Invalid Chat ID!") + return + except BadRequest: + send_message(msg, "Invalid Chat ID!") + return + + isadmin = getstatusadmin.status in ADMIN_STATUS + ismember = getstatusadmin.status in MEMBER_STAUS + isallow = sql.allow_connect_to_chat(connect_chat) + + if isadmin or (isallow and ismember) or (user.id in SUDO_USERS) or (user.id in DEV_USERS): + connection_status = sql.connect(msg.from_user.id, connect_chat) + if connection_status: + conn_chat = dispatcher.bot.getChat(connected(bot, update, chat, user.id, need_admin=False)) + chat_name = conn_chat.title + send_message(msg, "Successfully connected to *{chat_name}*." + " Use /connection for see current available commands.", + parse_mode=ParseMode.MARKDOWN) + sql.add_history_conn(user.id, str(conn_chat.id), chat_name) + else: + send_message(msg, "Connection failed!") + else: + send_message(msg, "Connection to this chat is not allowed!") + else: + gethistory = sql.get_history_conn(user.id) + if gethistory: + buttons = [ + InlineKeyboardButton(text="❎ Close button", callback_data="connect_close"), + InlineKeyboardButton(text="🧹 Clear history", callback_data="connect_clear") + ] + else: + buttons = [] + conn = connected(bot, update, chat, user.id, need_admin=False) + if conn: + connectedchat = dispatcher.bot.getChat(conn) + text = "You are connected to *{}* (`{}`)".format(connectedchat.title, conn) + buttons.append(InlineKeyboardButton(text="🔌 Disconnect", callback_data="connect_disconnect")) + else: + text = "Write the chat ID or tag to connect!" + if gethistory: + text += "\n\n*Connection history:*\n" + text += "╒═══「 *Info* 」\n" + text += "│ Sorted: `Newest`\n" + text += "│\n" + buttons = [buttons] + for x in sorted(gethistory.keys(), reverse=True): + htime = time.strftime("%d/%m/%Y", time.localtime(x)) + text += "╞═「 *{}* 」\n│ `{}`\n│ `{}`\n".format(gethistory[x]['chat_name'], + gethistory[x]['chat_id'], htime) + text += "│\n" + buttons.append([InlineKeyboardButton(text=gethistory[x]['chat_name'], + callback_data="connect({})".format(gethistory[x]['chat_id']))]) + text += "╘══「 Total {} Chats 」".format( + str(len(gethistory)) + " (max)" if len(gethistory) == 5 else str(len(gethistory))) + conn_hist = InlineKeyboardMarkup(buttons) + elif buttons: + conn_hist = InlineKeyboardMarkup([buttons]) + else: + conn_hist = None + send_message(msg, text, parse_mode="markdown", reply_markup=conn_hist) + + else: + getstatusadmin = bot.get_chat_member(chat.id, msg.from_user.id) + isadmin = getstatusadmin.status in ADMIN_STATUS + ismember = getstatusadmin.status in MEMBER_STAUS + isallow = sql.allow_connect_to_chat(chat.id) + if isadmin or (isallow and ismember) or (user.id in SUDO_USERS) or (user.id in DEV_USERS): + connection_status = sql.connect(msg.from_user.id, chat.id) + if connection_status: + chat_name = dispatcher.bot.getChat(chat.id).title + send_message(msg, "Successfully connected to *{}*.".format(chat_name), + parse_mode=ParseMode.MARKDOWN) + try: + sql.add_history_conn(user.id, str(chat.id), chat_name) + bot.send_message(msg.from_user.id, f"You have connected with *{chat_name}*." + f" Use /connection for see current available commands.", + parse_mode="markdown") + except BadRequest: + pass + except Unauthorized: + pass + else: + send_message(msg, "Connection failed!") + else: + send_message(msg, "Connection to this chat is not allowed!") + + +def disconnect_chat(bot: Bot, update: Update): + chat = update.effective_chat + msg = update.effective_message + + if chat.type == 'private': + disconnection_status = sql.disconnect(msg.from_user.id) + if disconnection_status: + sql.disconnected_chat = send_message(msg, "Disconnected from chat!") + else: + send_message(msg, "You're not connected!") + else: + send_message(msg, "This command is only available in PM.") + + +def connected(bot, update, chat, user_id, need_admin=True): + user = update.effective_user + msg = update.effective_message + + if chat.type == chat.PRIVATE and sql.get_connected_chat(user_id): + + conn_id = sql.get_connected_chat(user_id).chat_id + getstatusadmin = bot.get_chat_member(conn_id, msg.from_user.id) + isadmin = getstatusadmin.status in ADMIN_STATUS + ismember = getstatusadmin.status in MEMBER_STAUS + isallow = sql.allow_connect_to_chat(conn_id) + + if isadmin or (isallow and ismember) or (user.id in SUDO_USERS) or (user.id in DEV_USERS): + if need_admin is True: + if getstatusadmin.status in ADMIN_STATUS or user_id in SUDO_USERS or user.id in DEV_USERS: + return conn_id + else: + send_message(msg, "You must be an admin in the connected group!") + raise Exception("Not admin!") + else: + return conn_id + else: + send_message(msg, "The group changed the connection rights or you are no longer an admin.\n" + "I've disconnected you.") + disconnect_chat(bot, update) + raise Exception("Not admin!") + else: + return False + + +@run_async +def help_connect_chat(bot: Bot, update: Update): + msg = update.effective_message + + if msg.chat.type != "private": + send_message(msg, "PM me with that command to get help.") + return + else: + send_message(msg, "All commands", parse_mode="markdown") + + +@run_async +def connect_button(bot: Bot, update: Update): + query = update.callback_query + chat = update.effective_chat + user = update.effective_user + + connect_match = re.match(r"connect\((.+?)\)", query.data) + disconnect_match = query.data == "connect_disconnect" + clear_match = query.data == "connect_clear" + connect_close = query.data == "connect_close" + + if connect_match: + target_chat = connect_match.group(1) + getstatusadmin = bot.get_chat_member(target_chat, query.from_user.id) + isadmin = getstatusadmin.status in ADMIN_STATUS + ismember = getstatusadmin.status in MEMBER_STAUS + isallow = sql.allow_connect_to_chat(target_chat) + + if isadmin or (isallow and ismember) or (user.id in SUDO_USERS) or (user.id in DEV_USERS): + connection_status = sql.connect(query.from_user.id, target_chat) + + if connection_status: + conn_chat = dispatcher.bot.getChat(connected(bot, update, chat, user.id, need_admin=False)) + chat_name = conn_chat.title + query.message.edit_text(f"Successfully connected to *{chat_name}*." + f" Use /connection for see current available commands.", + parse_mode=ParseMode.MARKDOWN) + sql.add_history_conn(user.id, str(conn_chat.id), chat_name) + else: + query.message.edit_text("Connection failed!") + else: + bot.answer_callback_query(query.id, "Connection to this chat is not allowed!", show_alert=True) + elif disconnect_match: + disconnection_status = sql.disconnect(query.from_user.id) + if disconnection_status: + sql.disconnected_chat = query.message.edit_text("Disconnected from chat!") + else: + bot.answer_callback_query(query.id, "You're not connected!", show_alert=True) + elif clear_match: + sql.clear_history_conn(query.from_user.id) + query.message.edit_text("History connected has been cleared!") + elif connect_close: + query.message.edit_text("Closed.\nTo open again, type /connect") + else: + connect_chat(bot, update, []) + + +__help__ = """ + - /connect: connect a chat (Can be done in a group by /connect or /connect in PM) + - /connection: list connected chats + - /disconnect: disconnect from a chat + - /helpconnect: list available commands that can be done remotely + +*Admin only:* + - /allowconnect : allow a user to connect to a chat +""" + +CONNECT_CHAT_HANDLER = CommandHandler("connect", connect_chat, pass_args=True) +CONNECTION_CHAT_HANDLER = CommandHandler("connection", connection_chat) +DISCONNECT_CHAT_HANDLER = CommandHandler("disconnect", disconnect_chat) +ALLOW_CONNECTIONS_HANDLER = CommandHandler("allowconnect", allow_connections, pass_args=True) +HELP_CONNECT_CHAT_HANDLER = CommandHandler("helpconnect", help_connect_chat) +CONNECT_BTN_HANDLER = CallbackQueryHandler(connect_button, pattern=r"connect") + +dispatcher.add_handler(CONNECT_CHAT_HANDLER) +dispatcher.add_handler(CONNECTION_CHAT_HANDLER) +dispatcher.add_handler(DISCONNECT_CHAT_HANDLER) +dispatcher.add_handler(ALLOW_CONNECTIONS_HANDLER) +dispatcher.add_handler(HELP_CONNECT_CHAT_HANDLER) +dispatcher.add_handler(CONNECT_BTN_HANDLER) + +__mod_name__ = "CONNECTION" +__handlers__ = [CONNECT_CHAT_HANDLER, CONNECTION_CHAT_HANDLER, DISCONNECT_CHAT_HANDLER, ALLOW_CONNECTIONS_HANDLER, + HELP_CONNECT_CHAT_HANDLER, CONNECT_BTN_HANDLER] diff --git a/fire_bot/modules/corona.py b/fire_bot/modules/corona.py new file mode 100644 index 0000000..2137d22 --- /dev/null +++ b/fire_bot/modules/corona.py @@ -0,0 +1,36 @@ +import random +from telegram.ext import run_async, Filters +from telegram import Message, Chat, Update, Bot, MessageEntity +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + +SFW_STRINGS = ( + "HOW CAN I PROTECT MYSELF FROM CORONAVIRUS?", + "🧼WASH YOUR HANDS FREQUENTLY", + "🚴‍♂️EXCERCISE AND PROPER SLEEP🛌 WILL BOLSTER THE IMMUNE SYSTEM", + "🛀MAINTAIN GOOD HYGIENE HABHITS AT ALL TIMES", + "👬AVOID CONTACT WITH OTHERS", + "😷WEAR A FACE MASK WHEN DEALING WITH INFECTED PATIENT'S", + "🧻USE TISSUES WHEN COUGHING OR BLOWING NOSE", + "🍎WASH AND PREPARE FOODS CAREFULLY", + "STAY HOME STAY SAFE", + ) + +@run_async +def corona(bot: Bot, update: Update): + bot.sendChatAction(update.effective_chat.id, "typing") # Bot typing before send messages + message = update.effective_message + if message.reply_to_message: + message.reply_to_message.reply_text(random.choice(SFW_STRINGS)) + else: + message.reply_text(random.choice(SFW_STRINGS)) + +__help__ = """ +- /corona 😷. +""" + +__mod_name__ = "BREAK THE CHAIN" + +CRNA_HANDLER = DisableAbleCommandHandler("corona", corona) + +dispatcher.add_handler(CRNA_HANDLER) diff --git a/fire_bot/modules/covid.py b/fire_bot/modules/covid.py new file mode 100644 index 0000000..e634333 --- /dev/null +++ b/fire_bot/modules/covid.py @@ -0,0 +1,52 @@ +from telegram import ParseMode, Update, Bot, Chat +from telegram.ext import CommandHandler, MessageHandler, BaseFilter, run_async + +from tg_bot import dispatcher + +from requests import get + +import json +from urllib.request import urlopen + + + + +@run_async +def covid(bot: Bot, update: Update): + message = update.effective_message + device = message.text[len('/covid '):] + fetch = get(f'https://coronavirus-tracker-api.herokuapp.com/all') + + if fetch.status_code == 200: + usr = fetch.json() + data = fetch.text + parsed = json.loads(data) + total_confirmed_global = parsed["latest"]["confirmed"] + total_deaths_global = parsed["latest"]["deaths"] + total_recovered_global = parsed["latest"]["recovered"] + active_cases_covid19 = total_confirmed_global - total_deaths_global - total_recovered_global + reply_text = ("*Corona Stats🦠:*\n" + "Total Confirmed: `" + str(total_confirmed_global) + "`\n" + "Total Deaths: `" + str(total_deaths_global) + "`\n" + "Total Recovered: `" + str(total_recovered_global) +"`\n" + "Active Cases: `"+ str(active_cases_covid19) + "`") + message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + + return + + elif fetch.status_code == 404: + reply_text = "The API is currently down." + message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + + +__help__ = """ + + - /covid get worldwide corona status +""" + +__mod_name__ = 'COVID-19' + +COVID_HANDLER = CommandHandler("covid", covid, admin_ok=True) +dispatcher.add_handler(COVID_HANDLER) + + diff --git a/fire_bot/modules/covid19.py b/fire_bot/modules/covid19.py new file mode 100644 index 0000000..e088704 --- /dev/null +++ b/fire_bot/modules/covid19.py @@ -0,0 +1,121 @@ +from telegram import ParseMode, Update, Bot, Chat +from telegram.ext import CommandHandler, MessageHandler, BaseFilter, run_async + +from tg_bot import dispatcher + +import os +import json +import requests +from tabulate import tabulate +from urllib.request import urlopen + +def sign_delta(delta_var): + if delta_var < 0: + delta_var = str(format(delta_var, ',d')) + else: + delta_var = '+' + str(format(delta_var, ',d')) + return delta_var + +@run_async +def cov(bot: Bot, update: Update): + message = update.effective_message + confirmed = 0 + confirmed_delta = 0 + deceased = 0 + deceased_delta = 0 + active = 0 + active_delta = 0 + recovered = 0 + recovered_delta = 0 + mortality_rate = 0 + recovery_rate = 0 + country_input = '' + state_input = '' + district_input = '' + + loc_input = message.text.split(',') + if len(loc_input) > 2: + district_input = loc_input[2].strip() + if len(loc_input) > 1: + state_input = loc_input[1].strip() + if len(loc_input) > 0: + country_input = loc_input[0][4:].strip() + + try: + url_global = "https://bing.com/covid/data" + json_response = requests.get(url_global) + global_dict = json.loads(json_response.text) + + target = {} + + if country_input: + for country in global_dict['areas']: + if country['displayName'].lower() == country_input.lower(): + if state_input: + for state in country['areas']: + if state['displayName'].lower() == state_input.lower(): + if district_input: + for district in state['areas']: + if district['displayName'].lower() == district_input.lower(): + target = district + else: + target = state + else: + target = country + else: + target = global_dict + + if not target: + bot.send_message( + message.chat.id, + 'Data unavailable for %s!' % (message.text[4:].strip()) + ) + return + + confirmed = int(target['totalConfirmed'] or 0) + confirmed_delta = int(target['totalConfirmedDelta'] or 0) + deceased = int(target['totalDeaths'] or 0) + deceased_delta = int(target['totalDeathsDelta'] or 0) + recovered = int(target['totalRecovered'] or 0) + recovered_delta = int(target['totalRecoveredDelta'] or 0) + active = confirmed - deceased - recovered + active_delta = confirmed_delta - deceased_delta - recovered_delta + + mortality_rate = (deceased / confirmed) * 100 + recovery_rate = (recovered / confirmed) * 100 + + location = target['displayName'].upper() + + bot.send_message( + message.chat.id, + '`COVID-19 Tracker:` *%s*\n\n' % location.upper() + + '*Confirmed:* %s _(%s)_\n' % (format(confirmed, ',d'), sign_delta(confirmed_delta)) + + '*Active:* %s _(%s)_\n' % (format(active, ',d'), sign_delta(active_delta)) + + '*Deceased:* %s _(%s)_\n' % (format(deceased, ',d'), sign_delta(deceased_delta)) + + '*Recovered:* %s _(%s)_\n\n' % (format(recovered, ',d'), sign_delta(recovered_delta)) + + '*Mortality rate:* %s%%\n' % round(mortality_rate, 2) + + '*Recovery rate:* %s%%\n\n' % round(recovery_rate, 2) + + '[Powered by Bing.](https://bing.com/covid)', + parse_mode = ParseMode.MARKDOWN, + disable_web_page_preview = True + ) + return + + except: + bot.send_message( + message.chat.id, + 'Unable to contact the Bing COVID-19 Data API. Try again in a while.' + ) + return + + +__help__ = """ + - /cov : Get real time COVID-19 stats for the input location. + - /cov top : Get the top n countries with the highest confirmed cases. +""" + +__mod_name__ = 'COVID-19 TRACKER' + +COV_HANDLER = CommandHandler('cov', cov) + +dispatcher.add_handler(COV_HANDLER) diff --git a/fire_bot/modules/covidindia.py b/fire_bot/modules/covidindia.py new file mode 100644 index 0000000..eeb65eb --- /dev/null +++ b/fire_bot/modules/covidindia.py @@ -0,0 +1,55 @@ +from telegram import ParseMode, Update, Bot, Chat +from telegram.ext import CommandHandler, MessageHandler, BaseFilter, run_async + +from tg_bot import dispatcher + +import requests + +import json +from urllib.request import urlopen + + +def covindia(bot: Bot, update: Update): + message = update.effective_message + state = '' + confirmed = 0 + deceased = 0 + recovered = 0 + state_input = ''.join([message.text.split(' ')[i] + ' ' for i in range(1, len(message.text.split(' ')))]).strip() + if state_input: + url_india = 'https://api.covid19india.org/data.json' + json_url = urlopen(url_india) + state_dict = json.loads(json_url.read()) + for sdict in state_dict['statewise']: + if sdict['state'].lower() == state_input.lower(): + confirmed = sdict['confirmed'] + deceased = sdict['deaths'] + recovered = sdict['recovered'] + state = sdict['state'] + break + + if state: + bot.send_message( + message.chat.id, + '`COVID-19 Tracker`\n*Number of confirmed cases in %s:* %s\n*Deceased:* %s\n*Recovered:* %s\n\n_Source:_ covid19india.org' % (state, confirmed, deceased, recovered), + parse_mode = ParseMode.MARKDOWN, + disable_web_page_preview = True + ) + else: + bot.send_message( + message.chat.id, + 'You need to specify a valid Indian state!', + parse_mode = ParseMode.MARKDOWN, + disable_web_page_preview = True + ) + +__help__ = """ + + - /covindia : Get real time COVID-19 stats for the input Indian state +""" + +__mod_name__ = 'COVID-19 VIRUS' + +COV_INDIA_HANDLER = CommandHandler('covindia', covindia) + +dispatcher.add_handler(COV_INDIA_HANDLER) diff --git a/fire_bot/modules/currency_converter.py b/fire_bot/modules/currency_converter.py new file mode 100644 index 0000000..3008973 --- /dev/null +++ b/fire_bot/modules/currency_converter.py @@ -0,0 +1,55 @@ +import requests +from telegram import Bot, Update +from telegram.ext import CommandHandler, run_async + +from tg_bot import dispatcher, CASH_API_KEY + + +@run_async +def convert(bot: Bot, update: Update): + args = update.effective_message.text.split(" ", 3) + if len(args) > 1: + + orig_cur_amount = float(args[1]) + + try: + orig_cur = args[2].upper() + except IndexError: + update.effective_message.reply_text("You forgot to mention the currency code.") + return + + try: + new_cur = args[3].upper() + except IndexError: + update.effective_message.reply_text("You forgot to mention the currency code to convert into.") + return + + 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(f"Currency Not Supported.") + return + new_cur_amount = round(orig_cur_amount * current_rate, 5) + update.effective_message.reply_text(f"{orig_cur_amount} {orig_cur} = {new_cur_amount} {new_cur}") + else: + update.effective_message.reply_text(__help__) + + +__help__ = """ + - /cash : currency converter + example syntax: /cash 1 USD INR +""" + +CONVERTER_HANDLER = CommandHandler('cash', convert) + +dispatcher.add_handler(CONVERTER_HANDLER) + +__mod_name__ = "CURRENCY CONVERTER" +__command_list__ = ["cash"] +__handlers__ = [CONVERTER_HANDLER] diff --git a/fire_bot/modules/cust_filters.py b/fire_bot/modules/cust_filters.py new file mode 100644 index 0000000..88cdf65 --- /dev/null +++ b/fire_bot/modules/cust_filters.py @@ -0,0 +1,287 @@ +import re +from typing import Optional + +import telegram +from telegram import ParseMode, InlineKeyboardMarkup, Message, Chat +from telegram import Update, Bot +from telegram.error import BadRequest +from telegram.ext import CommandHandler, MessageHandler, DispatcherHandlerStop, run_async +from telegram.utils.helpers import escape_markdown + +from tg_bot import dispatcher, LOGGER +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import user_admin +from tg_bot.modules.helper_funcs.extraction import extract_text +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.helper_funcs.misc import build_keyboard +from tg_bot.modules.helper_funcs.string_handling import split_quotes, button_markdown_parser +from tg_bot.modules.sql import cust_filters_sql as sql + +from tg_bot.modules.connection import connected + +HANDLER_GROUP = 10 +BASIC_FILTER_STRING = "*Filters in this chat:*\n" + + +@run_async +def list_handlers(bot: Bot, update: Update): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + + conn = connected(bot, update, chat, user.id, need_admin=False) + if not conn == False: + chat_id = conn + chat_name = dispatcher.bot.getChat(conn).title + filter_list = "*Filters in {}:*\n" + else: + chat_id = update.effective_chat.id + if chat.type == "private": + chat_name = "local filters" + filter_list = "*local filters:*\n" + else: + chat_name = chat.title + filter_list = "*Filters in {}*:\n".format(chat_name) + + + all_handlers = sql.get_chat_triggers(chat_id) + + if not all_handlers: + update.effective_message.reply_text("No filters in *{}*!".format(chat_name), parse_mode=telegram.ParseMode.MARKDOWN) + return + + for keyword in all_handlers: + entry = " - {}\n".format(escape_markdown(keyword)) + if len(entry) + len(filter_list) > telegram.MAX_MESSAGE_LENGTH: + update.effective_message.reply_text(filter_list, parse_mode=telegram.ParseMode.MARKDOWN) + filter_list = entry + else: + filter_list += entry + + if not filter_list == BASIC_FILTER_STRING: + update.effective_message.reply_text(filter_list, parse_mode=telegram.ParseMode.MARKDOWN) + + +# NOT ASYNC BECAUSE DISPATCHER HANDLER RAISED +@user_admin +def filters(bot: Bot, update: Update): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + msg = update.effective_message # type: Optional[Message] + args = msg.text.split(None, 1) # use python's maxsplit to separate Cmd, keyword, and reply_text + + conn = connected(bot, update, chat, user.id) + if not conn == False: + chat_id = conn + chat_name = dispatcher.bot.getChat(conn).title + else: + chat_id = update.effective_chat.id + if chat.type == "private": + chat_name = "local filters" + else: + chat_name = chat.title + + if len(args) < 2: + return + + extracted = split_quotes(args[1]) + if len(extracted) < 1: + return + # set trigger -> lower, so as to avoid adding duplicate filters with different cases + keyword = extracted[0].lower() + + is_sticker = False + is_document = False + is_image = False + is_voice = False + is_audio = False + is_video = False + buttons = [] + + # determine what the contents of the filter are - text, image, sticker, etc + if len(extracted) >= 2: + offset = len(extracted[1]) - len(msg.text) # set correct offset relative to command + notename + content, buttons = button_markdown_parser(extracted[1], entities=msg.parse_entities(), offset=offset) + content = content.strip() + if not content: + msg.reply_text("There is no note message - You can't JUST have buttons, you need a message to go with it!") + return + + elif msg.reply_to_message and msg.reply_to_message.sticker: + content = msg.reply_to_message.sticker.file_id + is_sticker = True + + elif msg.reply_to_message and msg.reply_to_message.document: + content = msg.reply_to_message.document.file_id + is_document = True + + elif msg.reply_to_message and msg.reply_to_message.photo: + offset = len(msg.reply_to_message.caption) + ignore_underscore_case, buttons = button_markdown_parser(msg.reply_to_message.caption, entities=msg.reply_to_message.parse_entities(), offset=offset) + content = msg.reply_to_message.photo[-1].file_id # last elem = best quality + is_image = True + + elif msg.reply_to_message and msg.reply_to_message.audio: + content = msg.reply_to_message.audio.file_id + is_audio = True + + elif msg.reply_to_message and msg.reply_to_message.voice: + content = msg.reply_to_message.voice.file_id + is_voice = True + + elif msg.reply_to_message and msg.reply_to_message.video: + content = msg.reply_to_message.video.file_id + is_video = True + + else: + msg.reply_text("You didn't specify what to reply with!") + return + + # Add the filter + # Note: perhaps handlers can be removed somehow using sql.get_chat_filters + for handler in dispatcher.handlers.get(HANDLER_GROUP, []): + if handler.filters == (keyword, chat.id): + dispatcher.remove_handler(handler, HANDLER_GROUP) + + sql.add_filter(chat_id, keyword, content, is_sticker, is_document, is_image, is_audio, is_voice, is_video, + buttons) + + msg.reply_text("Handler '{}' added in *{}*!".format(keyword, chat_name), parse_mode=telegram.ParseMode.MARKDOWN) + raise DispatcherHandlerStop + + +# NOT ASYNC BECAUSE DISPATCHER HANDLER RAISED +@user_admin +def stop_filter(bot: Bot, update: Update): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + args = update.effective_message.text.split(None, 1) + + conn = connected(bot, update, chat, user.id) + if not conn == False: + chat_id = conn + chat_name = dispatcher.bot.getChat(conn).title + else: + chat_id = chat.id + if chat.type == "private": + chat_name = "local notes" + else: + chat_name = chat.title + + if len(args) < 2: + return + + chat_filters = sql.get_chat_triggers(chat_id) + + if not chat_filters: + update.effective_message.reply_text("No filters are active in *{}*!".format(chat_name), parse_mode=telegram.ParseMode.MARKDOWN) + return + + for keyword in chat_filters: + if keyword == args[1]: + sql.remove_filter(chat_id, args[1]) + update.effective_message.reply_text("Yep, I'll stop replying to that in *{}*.".format(chat_name), parse_mode=telegram.ParseMode.MARKDOWN) + raise DispatcherHandlerStop + + update.effective_message.reply_text("That's not a current filter - run /filters for all active filters.") + + +@run_async +def reply_filter(bot: Bot, update: Update): + chat = update.effective_chat # type: Optional[Chat] + message = update.effective_message # type: Optional[Message] + to_match = extract_text(message) + if not to_match: + return + + if message.reply_to_message: + message = message.reply_to_message + + + chat_filters = sql.get_chat_triggers(chat.id) + for keyword in chat_filters: + pattern = r"( |^|[^\w])" + re.escape(keyword) + r"( |$|[^\w])" + if re.search(pattern, to_match, flags=re.IGNORECASE): + filt = sql.get_filter(chat.id, keyword) + buttons = sql.get_buttons(chat.id, filt.keyword) + if len(buttons) > 0: + keyb = build_keyboard(buttons) + keyboard = InlineKeyboardMarkup(keyb) + if filt.is_sticker: + message.reply_sticker(filt.reply) + elif filt.is_document: + message.reply_document(filt.reply) + elif filt.is_image: + message.reply_photo(filt.reply, reply_markup=keyboard) + elif filt.is_audio: + message.reply_audio(filt.reply) + elif filt.is_voice: + message.reply_voice(filt.reply) + elif filt.is_video: + message.reply_video(filt.reply) + elif filt.has_markdown: + keyb = build_keyboard(buttons) + keyboard = InlineKeyboardMarkup(keyb) + + should_preview_disabled = True + if "telegra.ph" in filt.reply or "youtu.be" in filt.reply: + should_preview_disabled = False + + try: + message.reply_text(filt.reply, parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=should_preview_disabled, + reply_markup=keyboard) + except BadRequest as excp: + if excp.message == "Unsupported url protocol": + message.reply_text("You seem to be trying to use an unsupported url protocol. Telegram " + "doesn't support buttons for some protocols, such as tg://. Please try " + "again help.") + elif excp.message == "Reply message not found": + bot.send_message(chat.id, filt.reply, parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True, + reply_markup=keyboard) + else: + message.reply_text("This note could not be sent, as it is incorrectly formatted.") + LOGGER.warning("Message %s could not be parsed", str(filt.reply)) + LOGGER.exception("Could not parse filter %s in chat %s", str(filt.keyword), str(chat.id)) + + else: + # LEGACY - all new filters will have has_markdown set to True. + message.reply_text(filt.reply) + break + + +def __stats__(): + return "{} filters, across {} chats.".format(sql.num_filters(), sql.num_chats()) + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + cust_filters = sql.get_chat_triggers(chat_id) + return "There are `{}` custom filters here.".format(len(cust_filters)) + + +__help__ = """ + - /filters: list all active filters in this chat. + +*Admin only:* + - /filter : add a filter to this chat. The bot will now reply that message whenever 'keyword'\ +is mentioned. If you reply to a sticker with a keyword, the bot will reply with that sticker. NOTE: all filter \ +keywords are in lowercase. If you want your keyword to be a sentence, use quotes. eg: /filter "hey there" How you \ +doin? + - /stop : stop that filter. +""" + +__mod_name__ = "FILTERS" + +FILTER_HANDLER = CommandHandler("filter", filters) +STOP_HANDLER = CommandHandler("stop", stop_filter) +LIST_HANDLER = DisableAbleCommandHandler("filters", list_handlers, admin_ok=True) +CUST_FILTER_HANDLER = MessageHandler(CustomFilters.has_text, reply_filter) + +dispatcher.add_handler(FILTER_HANDLER) +dispatcher.add_handler(STOP_HANDLER) +dispatcher.add_handler(LIST_HANDLER) +dispatcher.add_handler(CUST_FILTER_HANDLER, HANDLER_GROUP) diff --git a/fire_bot/modules/dbcleanup.py b/fire_bot/modules/dbcleanup.py new file mode 100644 index 0000000..759bf19 --- /dev/null +++ b/fire_bot/modules/dbcleanup.py @@ -0,0 +1,206 @@ +from time import sleep + +from telegram import Bot, Update, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.error import BadRequest, Unauthorized +from telegram.ext import CommandHandler, CallbackQueryHandler, run_async + +import tg_bot.modules.sql.global_bans_sql as gban_sql +import tg_bot.modules.sql.users_sql as user_sql +from tg_bot import dispatcher, OWNER_ID, DEV_USERS +from tg_bot.modules.helper_funcs.chat_status import dev_plus + + +def get_invalid_chats(bot: Bot, update: Update, remove: bool = False): + chat_id = update.effective_chat.id + chats = user_sql.get_all_chats() + kicked_chats, progress = 0, 0 + chat_list = [] + progress_message = None + + for chat in chats: + + if ((100 * chats.index(chat)) / len(chats)) > progress: + progress_bar = f"{progress}% completed in getting invalid chats." + if progress_message: + try: + bot.editMessageText(progress_bar, chat_id, progress_message.message_id) + except: + pass + else: + progress_message = bot.sendMessage(chat_id, progress_bar) + progress += 5 + + cid = chat.chat_id + sleep(0.1) + try: + bot.get_chat(cid, timeout=60) + except (BadRequest, Unauthorized): + kicked_chats += 1 + chat_list.append(cid) + except: + pass + + try: + progress_message.delete() + except: + pass + + if not remove: + return kicked_chats + else: + for muted_chat in chat_list: + sleep(0.1) + user_sql.rem_chat(muted_chat) + return kicked_chats + + +def get_invalid_gban(bot: Bot, update: Update, remove: bool = False): + banned = gban_sql.get_gban_list() + ungbanned_users = 0 + ungban_list = [] + + for user in banned: + user_id = user["user_id"] + sleep(0.1) + try: + bot.get_chat(user_id) + except BadRequest: + ungbanned_users += 1 + ungban_list.append(user_id) + except: + pass + + if not remove: + return ungbanned_users + else: + for user_id in ungban_list: + sleep(0.1) + gban_sql.ungban_user(user_id) + return ungbanned_users + + +@run_async +@dev_plus +def dbcleanup(bot: Bot, update: Update): + msg = update.effective_message + + msg.reply_text("Getting invalid chat count ...") + invalid_chat_count = get_invalid_chats(bot, update) + + msg.reply_text("Getting invalid gbanned count ...") + invalid_gban_count = get_invalid_gban(bot, update) + + reply = f"Total invalid chats - {invalid_chat_count}\n" + reply += f"Total invalid gbanned users - {invalid_gban_count}" + + buttons = [ + [InlineKeyboardButton("Cleanup DB", callback_data=f"db_cleanup")] + ] + + update.effective_message.reply_text(reply, reply_markup=InlineKeyboardMarkup(buttons)) + + +def get_muted_chats(bot: Bot, update: Update, leave: bool = False): + chat_id = update.effective_chat.id + chats = user_sql.get_all_chats() + muted_chats, progress = 0, 0 + chat_list = [] + progress_message = None + + for chat in chats: + + if ((100 * chats.index(chat)) / len(chats)) > progress: + progress_bar = f"{progress}% completed in getting muted chats." + if progress_message: + try: + bot.editMessageText(progress_bar, chat_id, progress_message.message_id) + except: + pass + else: + progress_message = bot.sendMessage(chat_id, progress_bar) + progress += 5 + + cid = chat.chat_id + sleep(0.1) + + try: + bot.send_chat_action(cid, "TYPING", timeout=60) + except (BadRequest, Unauthorized): + muted_chats += +1 + chat_list.append(cid) + except: + pass + + try: + progress_message.delete() + except: + pass + + if not leave: + return muted_chats + else: + for muted_chat in chat_list: + sleep(0.1) + try: + bot.leaveChat(muted_chat, timeout=60) + except: + pass + user_sql.rem_chat(muted_chat) + return muted_chats + + +@run_async +@dev_plus +def leave_muted_chats(bot: Bot, update: Update): + message = update.effective_message + progress_message = message.reply_text("Getting chat count ...") + muted_chats = get_muted_chats(bot, update) + + buttons = [ + [InlineKeyboardButton("Leave chats", callback_data=f"db_leave_chat")] + ] + + update.effective_message.reply_text(f"I am muted in {muted_chats} chats.", + reply_markup=InlineKeyboardMarkup(buttons)) + progress_message.delete() + + +@run_async +def callback_button(bot: Bot, update: Update): + query = update.callback_query + message = query.message + chat_id = update.effective_chat.id + query_type = query.data + + admin_list = [OWNER_ID] + DEV_USERS + + bot.answer_callback_query(query.id) + + if query_type == "db_leave_chat": + if query.from_user.id in admin_list: + bot.editMessageText("Leaving chats ...", chat_id, message.message_id) + chat_count = get_muted_chats(bot, update, True) + bot.sendMessage(chat_id, f"Left {chat_count} chats.") + else: + query.answer("You are not allowed to use this.") + elif query_type == "db_cleanup": + if query.from_user.id in admin_list: + bot.editMessageText("Cleaning up DB ...", chat_id, message.message_id) + invalid_chat_count = get_invalid_chats(bot, update, True) + invalid_gban_count = get_invalid_gban(bot, update, True) + reply = "Cleaned up {} chats and {} gbanned users from db.".format(invalid_chat_count, invalid_gban_count) + bot.sendMessage(chat_id, reply) + else: + query.answer("You are not allowed to use this.") + + +DB_CLEANUP_HANDLER = CommandHandler("dbcleanup", dbcleanup) +LEAVE_MUTED_CHATS_HANDLER = CommandHandler("leavemutedchats", leave_muted_chats) +BUTTON_HANDLER = CallbackQueryHandler(callback_button, pattern='db_.*') + +dispatcher.add_handler(DB_CLEANUP_HANDLER) +dispatcher.add_handler(LEAVE_MUTED_CHATS_HANDLER) +dispatcher.add_handler(BUTTON_HANDLER) + +__mod_name__ = "DB CLEANUP" +__handlers__ = [DB_CLEANUP_HANDLER, LEAVE_MUTED_CHATS_HANDLER, BUTTON_HANDLER] diff --git a/fire_bot/modules/dev.py b/fire_bot/modules/dev.py new file mode 100644 index 0000000..7eaf795 --- /dev/null +++ b/fire_bot/modules/dev.py @@ -0,0 +1,64 @@ +import os +import subprocess +import sys +from time import sleep +from typing import List + +from telegram import Bot, Update, TelegramError +from telegram.ext import CommandHandler, run_async + +from tg_bot import dispatcher +from tg_bot.modules.helper_funcs.chat_status import dev_plus + + +@run_async +@dev_plus +def leave(bot: Bot, update: Update, args: List[str]): + if args: + chat_id = str(args[0]) + try: + bot.leave_chat(int(chat_id)) + update.effective_message.reply_text("Beep boop, I left that soup!.") + except TelegramError: + update.effective_message.reply_text("Beep boop, I could not leave that group(dunno why tho).") + else: + update.effective_message.reply_text("Send a valid chat ID") + + +@run_async +@dev_plus +def gitpull(bot: Bot, update: Update): + sent_msg = update.effective_message.reply_text("Pulling all changes from remote and then attempting to restart.") + subprocess.Popen('git pull', stdout=subprocess.PIPE, shell=True) + + sent_msg_text = sent_msg.text + "\n\nChanges pulled...I guess.. Restarting in " + + for i in reversed(range(5)): + sent_msg.edit_text(sent_msg_text + str(i + 1)) + sleep(1) + + sent_msg.edit_text("Restarted.") + + os.system('restart.bat') + os.execv('start.bat', sys.argv) + + +@run_async +@dev_plus +def restart(bot: Bot, update: Update): + update.effective_message.reply_text("Starting a new instance and shutting down this one") + + os.system('restart.bat') + os.execv('start.bat', sys.argv) + + +LEAVE_HANDLER = CommandHandler("leave", leave, pass_args=True) +GITPULL_HANDLER = CommandHandler("gitpull", gitpull) +RESTART_HANDLER = CommandHandler("reboot", restart) + +dispatcher.add_handler(LEAVE_HANDLER) +dispatcher.add_handler(GITPULL_HANDLER) +dispatcher.add_handler(RESTART_HANDLER) + +__mod_name__ = "DEV" +__handlers__ = [LEAVE_HANDLER, GITPULL_HANDLER, RESTART_HANDLER] diff --git a/fire_bot/modules/devpromoter.py b/fire_bot/modules/devpromoter.py new file mode 100644 index 0000000..073e017 --- /dev/null +++ b/fire_bot/modules/devpromoter.py @@ -0,0 +1,407 @@ +import html +import json +import html +import os +from typing import List, Optional + +from telegram import Bot, Update, ParseMode, TelegramError +from telegram.ext import CommandHandler, run_async +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, WHITELIST_USERS, SUPPORT_USERS, SUDO_USERS, DEV_USERS, OWNER_ID +from tg_bot.modules.helper_funcs.chat_status import whitelist_plus, dev_plus +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.log_channel import gloggable + +ELEVATED_USERS_FILE = os.path.join(os.getcwd(), 'tg_bot/elevated_users.json') + + +def check_user_id(user_id: int, bot: Bot) -> Optional[str]: + if not user_id: + reply = "That...is a chat!" + + elif user_id == bot.id: + reply = "This does not work that way." + + else: + reply = None + return reply + + +@run_async +@dev_plus +@gloggable +def addsudo(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + message.reply_text("This member is already my SUDO.") + return "" + + if user_id in SUPPORT_USERS: + rt += "This user is already a SUPPORT USER." + data['supports'].remove(user_id) + SUPPORT_USERS.remove(user_id) + + if user_id in WHITELIST_USERS: + rt += "This user is already a WHITELIST USER." + data['whitelists'].remove(user_id) + WHITELIST_USERS.remove(user_id) + + data['sudos'].append(user_id) + SUDO_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text( + rt + "\nSuccessfully added this user {} to sudo!".format(user_member.first_name)) + + log_message = (f"#SUDO\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@dev_plus +@gloggable +def addsupport(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + rt += "Demoting status of this SUDO to SUPPORT" + data['sudos'].remove(user_id) + SUDO_USERS.remove(user_id) + + if user_id in SUPPORT_USERS: + message.reply_text("This user is already a SUDO.") + return "" + + if user_id in WHITELIST_USERS: + rt += "Promoting Disaster level from WHITELIST USER to SUPPORT USER" + data['whitelists'].remove(user_id) + WHITELIST_USERS.remove(user_id) + + data['supports'].append(user_id) + SUPPORT_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text(rt + f"\n{user_member.first_name} was added as a Demon Disaster!") + + log_message = (f"#SUPPORT\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = "{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@dev_plus +@gloggable +def addwhitelist(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + rt += "This member is a SUDO, Demoting to SUDO." + data['sudos'].remove(user_id) + SUDO_USERS.remove(user_id) + + if user_id in SUPPORT_USERS: + rt += "This user is already a SUPPORT, Demoting to SUPPORT" + data['supports'].remove(user_id) + SUPPORT_USERS.remove(user_id) + + if user_id in WHITELIST_USERS: + message.reply_text("This user is already a WHITELIST USER.") + return "" + + data['whitelists'].append(user_id) + WHITELIST_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text( + rt + f"\nSuccessfully promoted {user_member.first_name} to a whhitelist user!") + + log_message = (f"#WHITELIST\n" + f"Admin: {mention_html(user.id, user.first_name)} \n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@dev_plus +@gloggable +def removesudo(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + message.reply_text("Demoting to normal user") + SUDO_USERS.remove(user_id) + data['sudos'].remove(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + log_message = (f"#UNSUDO\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = "{}:\n".format(html.escape(chat.title)) + log_message + + return log_message + + else: + message.reply_text("This user is not a sudo!") + return "" + + +@run_async +@dev_plus +@gloggable +def removesupport(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUPPORT_USERS: + message.reply_text("Demoting to Civilian") + SUPPORT_USERS.remove(user_id) + data['supports'].remove(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + log_message = (f"#UNSUPPORT\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + else: + message.reply_text("This user is not a support!") + return "" + + +@run_async +@dev_plus +@gloggable +def removewhitelist(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in WHITELIST_USERS: + message.reply_text("Demoting to normal user") + WHITELIST_USERS.remove(user_id) + data['whitelists'].remove(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + log_message = (f"#UNWHITELIST\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + else: + message.reply_text("This user is not a whitelist!") + return "" + + +@run_async +@whitelist_plus +def whitelistlist(bot: Bot, update: Update): + reply = "Whitelist user :\n" + for each_user in WHITELIST_USERS: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@whitelist_plus +def supportlist(bot: Bot, update: Update): + reply = "Support List :\n" + for each_user in SUPPORT_USERS: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@whitelist_plus +def sudolist(bot: Bot, update: Update): + true_sudo = list(set(SUDO_USERS) - set(DEV_USERS)) + reply = "Sudo list:\n" + for each_user in true_sudo: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@whitelist_plus +def devlist(bot: Bot, update: Update): + true_dev = list(set(DEV_USERS) - {OWNER_ID}) + reply = "My developer list:\n" + for each_user in true_dev: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +__help__ = """ + - /addsudo - add sudo users + - /addsupport - add support users + - /addwhitelist - add whitelist users + - /whitelistlist - List whitelisted users. + - /supportlist - List support users. + - /sudolist - List sudo users. + - /devlist - List dev users. +""" + +SUDO_HANDLER = CommandHandler(("addsudo"), addsudo, pass_args=True) +SUPPORT_HANDLER = CommandHandler(("addsupport"), addsupport, pass_args=True) +WHITELIST_HANDLER = CommandHandler(("addwhitelist"), addwhitelist, pass_args=True) +UNSUDO_HANDLER = CommandHandler(("removesudo"), removesudo, pass_args=True) +UNSUPPORT_HANDLER = CommandHandler(("removesupport"), removesupport, pass_args=True) +UNWHITELIST_HANDLER = CommandHandler(("removewhitelist"), removewhitelist, pass_args=True) + +WHITELISTLIST_HANDLER = CommandHandler(["whitelistlist"], whitelistlist) +SUPPORTLIST_HANDLER = CommandHandler(["supportlist"], supportlist) +SUDOLIST_HANDLER = CommandHandler(["sudolist"], sudolist) +DEVLIST_HANDLER = CommandHandler(["devlist"], devlist) + +dispatcher.add_handler(SUDO_HANDLER) +dispatcher.add_handler(SUPPORT_HANDLER) +dispatcher.add_handler(WHITELIST_HANDLER) +dispatcher.add_handler(UNSUDO_HANDLER) +dispatcher.add_handler(UNSUPPORT_HANDLER) +dispatcher.add_handler(UNWHITELIST_HANDLER) + +dispatcher.add_handler(WHITELISTLIST_HANDLER) +dispatcher.add_handler(SUPPORTLIST_HANDLER) +dispatcher.add_handler(SUDOLIST_HANDLER) +dispatcher.add_handler(DEVLIST_HANDLER) + +__mod_name__ = "DEV PROMOTER" +__handlers__ = [SUDO_HANDLER, SUPPORT_HANDLER, WHITELIST_HANDLER, + UNSUDO_HANDLER, UNSUPPORT_HANDLER, UNWHITELIST_HANDLER, + WHITELISTLIST_HANDLER, SUPPORTLIST_HANDLER, SUDOLIST_HANDLER, DEVLIST_HANDLER] diff --git a/fire_bot/modules/dictionary.py b/fire_bot/modules/dictionary.py new file mode 100644 index 0000000..28277b5 --- /dev/null +++ b/fire_bot/modules/dictionary.py @@ -0,0 +1,42 @@ +import requests +from telegram import Bot, Message, Update, ParseMode +from telegram.ext import CommandHandler, run_async + +from tg_bot import dispatcher + + +@run_async +def define(bot: Bot, update: Update, args): + msg = update.effective_message + word = " ".join(args) + res = requests.get(f"https://googledictionaryapi.eu-gb.mybluemix.net/?define={word}") + if res.status_code == 200: + info = res.json()[0].get("meaning") + if info: + meaning = "" + for count, (key, value) in enumerate(info.items(), start=1): + meaning += f"{count}. {word} ({key})\n" + for i in value: + defs = i.get("definition") + meaning += f"• {defs}\n" + msg.reply_text(meaning, parse_mode=ParseMode.HTML) + else: + return + else: + msg.reply_text("No results found!") + + +__help__ = """ +Ever stumbled upon a word that you didn't know of and wanted to look it up? +With this module, you can find the definitions of words without having to leave the app! + +*Available commands:* + - /define : returns the definition of the word. + """ + +__mod_name__ = "DICTIONARY" + + +DEFINE_HANDLER = CommandHandler("define", define, pass_args=True) + +dispatcher.add_handler(DEFINE_HANDLER) diff --git a/fire_bot/modules/disable.py b/fire_bot/modules/disable.py new file mode 100644 index 0000000..bfa2b62 --- /dev/null +++ b/fire_bot/modules/disable.py @@ -0,0 +1,305 @@ +import importlib +from typing import Union, List + +from future.utils import string_types +from telegram import Bot, Update, ParseMode +from telegram.ext import CommandHandler, RegexHandler, MessageHandler +from telegram.utils.helpers import escape_markdown + +from tg_bot import dispatcher +from tg_bot.modules.helper_funcs.handlers import CMD_STARTERS, CustomCommandHandler +from tg_bot.modules.helper_funcs.misc import is_module_loaded + +FILENAME = __name__.rsplit(".", 1)[-1] + +# If module is due to be loaded, then setup all the magical handlers +if is_module_loaded(FILENAME): + + from telegram.ext.dispatcher import run_async + + from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_admin, connection_status + from tg_bot.modules.sql import disable_sql as sql + + DISABLE_CMDS = [] + DISABLE_OTHER = [] + ADMIN_CMDS = [] + + + class DisableAbleCommandHandler(CustomCommandHandler): + + def __init__(self, command, callback, admin_ok=False, filters=None, **kwargs): + super().__init__(command, callback, **kwargs) + self.admin_ok = admin_ok + self.filters = filters + if isinstance(command, string_types): + DISABLE_CMDS.append(command) + if admin_ok: + ADMIN_CMDS.append(command) + else: + DISABLE_CMDS.extend(command) + if admin_ok: + ADMIN_CMDS.extend(command) + + def check_update(self, update): + chat = update.effective_chat + user = update.effective_user + + if super().check_update(update): + + # Should be safe since check_update passed. + command = update.effective_message.text_html.split(None, 1)[0][1:].split('@')[0] + + # disabled, admincmd, user admin + if sql.is_command_disabled(chat.id, command): + if command in ADMIN_CMDS and is_user_admin(chat, user.id): + return True + + # not disabled + else: + return True + + + class DisableAbleMessageHandler(MessageHandler): + def __init__(self, filters, callback, friendly, **kwargs): + super().__init__(filters, callback, **kwargs) + DISABLE_OTHER.append(friendly) + self.friendly = friendly + self.filters = filters + + def check_update(self, update): + + chat = update.effective_chat + if super().check_update(update): + if sql.is_command_disabled(chat.id, self.friendly): + return False + else: + return True + + + class DisableAbleRegexHandler(RegexHandler): + def __init__(self, pattern, callback, friendly="", filters=None, **kwargs): + super().__init__(pattern, callback, filters, **kwargs) + DISABLE_OTHER.append(friendly) + self.friendly = friendly + + def check_update(self, update): + chat = update.effective_chat + if super().check_update(update): + if sql.is_command_disabled(chat.id, self.friendly): + return False + else: + return True + + + @run_async + @connection_status + @user_admin + def disable(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + if len(args) >= 1: + disable_cmd = args[0] + if disable_cmd.startswith(CMD_STARTERS): + disable_cmd = disable_cmd[1:] + + if disable_cmd in set(DISABLE_CMDS + DISABLE_OTHER): + sql.disable_command(chat.id, str(disable_cmd).lower()) + update.effective_message.reply_text(f"Disabled the use of `{disable_cmd}`", + parse_mode=ParseMode.MARKDOWN) + else: + update.effective_message.reply_text("That command can't be disabled") + + else: + update.effective_message.reply_text("What should I disable?") + + + @run_async + @connection_status + @user_admin + def disable_module(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + if len(args) >= 1: + disable_module = "tg_bot.modules." + args[0].rsplit(".", 1)[0] + + try: + module = importlib.import_module(disable_module) + except: + update.effective_message.reply_text("Does that module even exist?") + return + + try: + command_list = module.__command_list__ + except: + update.effective_message.reply_text("Module does not contain command list!") + return + + disabled_cmds = [] + failed_disabled_cmds = [] + + for disable_cmd in command_list: + if disable_cmd.startswith(CMD_STARTERS): + disable_cmd = disable_cmd[1:] + + if disable_cmd in set(DISABLE_CMDS + DISABLE_OTHER): + sql.disable_command(chat.id, str(disable_cmd).lower()) + disabled_cmds.append(disable_cmd) + else: + failed_disabled_cmds.append(disable_cmd) + + if disabled_cmds: + disabled_cmds_string = ", ".join(disabled_cmds) + update.effective_message.reply_text(f"Disabled the uses of `{disabled_cmds_string}`", + parse_mode=ParseMode.MARKDOWN) + + if failed_disabled_cmds: + failed_disabled_cmds_string = ", ".join(failed_disabled_cmds) + update.effective_message.reply_text(f"Commands `{failed_disabled_cmds_string}` can't be disabled", + parse_mode=ParseMode.MARKDOWN) + + else: + update.effective_message.reply_text("What should I disable?") + + + @run_async + @connection_status + @user_admin + def enable(bot: Bot, update: Update, args: List[str]): + + chat = update.effective_chat + if len(args) >= 1: + enable_cmd = args[0] + if enable_cmd.startswith(CMD_STARTERS): + enable_cmd = enable_cmd[1:] + + if sql.enable_command(chat.id, enable_cmd): + update.effective_message.reply_text(f"Enabled the use of `{enable_cmd}`", + parse_mode=ParseMode.MARKDOWN) + else: + update.effective_message.reply_text("Is that even disabled?") + + else: + update.effective_message.reply_text("What should I enable?") + + + @run_async + @connection_status + @user_admin + def enable_module(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + + if len(args) >= 1: + enable_module = "tg_bot.modules." + args[0].rsplit(".", 1)[0] + + try: + module = importlib.import_module(enable_module) + except: + update.effective_message.reply_text("Does that module even exist?") + return + + try: + command_list = module.__command_list__ + except: + update.effective_message.reply_text("Module does not contain command list!") + return + + enabled_cmds = [] + failed_enabled_cmds = [] + + for enable_cmd in command_list: + if enable_cmd.startswith(CMD_STARTERS): + enable_cmd = enable_cmd[1:] + + if sql.enable_command(chat.id, enable_cmd): + enabled_cmds.append(enable_cmd) + else: + failed_enabled_cmds.append(enable_cmd) + + if enabled_cmds: + enabled_cmds_string = ", ".join(enabled_cmds) + update.effective_message.reply_text(f"Enabled the uses of `{enabled_cmds_string}`", + parse_mode=ParseMode.MARKDOWN) + + if failed_enabled_cmds: + failed_enabled_cmds_string = ", ".join(failed_enabled_cmds) + update.effective_message.reply_text(f"Are the commands `{failed_enabled_cmds_string}` even disabled?", + parse_mode=ParseMode.MARKDOWN) + + else: + update.effective_message.reply_text("What should I enable?") + + + @run_async + @connection_status + @user_admin + def list_cmds(bot: Bot, update: Update): + if DISABLE_CMDS + DISABLE_OTHER: + result = "" + for cmd in set(DISABLE_CMDS + DISABLE_OTHER): + result += f" - `{escape_markdown(cmd)}`\n" + update.effective_message.reply_text(f"The following commands are toggleable:\n{result}", + parse_mode=ParseMode.MARKDOWN) + else: + update.effective_message.reply_text("No commands can be disabled.") + + + # do not async + def build_curr_disabled(chat_id: Union[str, int]) -> str: + disabled = sql.get_all_disabled(chat_id) + if not disabled: + return "No commands are disabled!" + + result = "" + for cmd in disabled: + result += " - `{}`\n".format(escape_markdown(cmd)) + return "The following commands are currently restricted:\n{}".format(result) + + + @run_async + @connection_status + def commands(bot: Bot, update: Update): + chat = update.effective_chat + update.effective_message.reply_text(build_curr_disabled(chat.id), parse_mode=ParseMode.MARKDOWN) + + + def __stats__(): + return f"{sql.num_disabled()} disabled items, across {sql.num_chats()} chats." + + + def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + + def __chat_settings__(chat_id, user_id): + return build_curr_disabled(chat_id) + + + DISABLE_HANDLER = CommandHandler("disable", disable, pass_args=True) + DISABLE_MODULE_HANDLER = CommandHandler("disablemodule", disable_module, pass_args=True) + ENABLE_HANDLER = CommandHandler("enable", enable, pass_args=True) + ENABLE_MODULE_HANDLER = CommandHandler("enablemodule", enable_module, pass_args=True) + COMMANDS_HANDLER = CommandHandler(["cmds", "disabled"], commands) + TOGGLE_HANDLER = CommandHandler("listcmds", list_cmds) + + dispatcher.add_handler(DISABLE_HANDLER) + dispatcher.add_handler(DISABLE_MODULE_HANDLER) + dispatcher.add_handler(ENABLE_HANDLER) + dispatcher.add_handler(ENABLE_MODULE_HANDLER) + dispatcher.add_handler(COMMANDS_HANDLER) + dispatcher.add_handler(TOGGLE_HANDLER) + + __help__ = """ + - /cmds: check the current status of disabled commands + + *Admin only:* + - /enable : enable that command + - /disable : disable that command + - /enablemodule : enable all commands in that module + - /disablemodule : disable all commands in that module + - /listcmds: list all possible toggleable commands + """ + + __mod_name__ = "COMMAND DISABLING" + +else: + DisableAbleCommandHandler = CommandHandler + DisableAbleRegexHandler = RegexHandler + DisableAbleMessageHandler = MessageHandler diff --git a/fire_bot/modules/disasters.py b/fire_bot/modules/disasters.py new file mode 100644 index 0000000..8f5e82f --- /dev/null +++ b/fire_bot/modules/disasters.py @@ -0,0 +1,593 @@ +import html +import json +import html +import os +from typing import List, Optional + +from telegram import Bot, Update, ParseMode, TelegramError +from telegram.ext import CommandHandler, run_async +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, WHITELIST_USERS, TIGER_USERS, SUPPORT_USERS, SUDO_USERS, DEV_USERS, OWNER_ID +from tg_bot.modules.helper_funcs.chat_status import whitelist_plus, dev_plus, sudo_plus +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.log_channel import gloggable + +ELEVATED_USERS_FILE = os.path.join(os.getcwd(), 'tg_bot/elevated_users.json') + + +def check_user_id(user_id: int, bot: Bot) -> Optional[str]: + if not user_id: + reply = "That...is a chat! baka ka omae?" + + elif user_id == bot.id: + reply = "This does not work that way." + + else: + reply = None + return reply + +#I added extra new lines +disasters = """ *"Disaster Levels"* +\n*Heroes Association* - Devs who can access the bots server and can execute, edit, modify bot code. Can also manage other Disasters +\n*LEGEND* - Only one exists, bot owner. +\n*Dragons* - Have super user access, can gban, manage disasters lower than them and are admins in phantom. +\n*HACKER* - Have access go globally ban users across this bot +\n*Tigers* - Same as wolves but can unban themselves if banned. +\n*Wolves* - Cannot be banned, muted flood kicked but can be manually banned by admins. +""" +# do not async, not a handler +def send_disasters(update): + update.effective_message.reply_text(disasters, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + +@run_async +@dev_plus +@gloggable +def addsudo(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + message.reply_text("This member is already a Sudo user") + return "" + + if user_id in SUPPORT_USERS: + rt += "Requested HA to promote a Hacker Disaster to Dragon." + data['supports'].remove(user_id) + SUPPORT_USERS.remove(user_id) + + if user_id in WHITELIST_USERS: + rt += "Requested HA to promote a Wolf Disaster to Dragon." + data['whitelists'].remove(user_id) + WHITELIST_USERS.remove(user_id) + + data['sudos'].append(user_id) + SUDO_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text( + rt + "\nSuccessfully set Disaster level of {} to Dragon!".format(user_member.first_name)) + + log_message = (f"#SUDO\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@sudo_plus +@gloggable +def addsupport(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + rt += "Requested HA to deomote this Dragon to Demon" + data['sudos'].remove(user_id) + SUDO_USERS.remove(user_id) + + if user_id in SUPPORT_USERS: + message.reply_text("This user is already a Support User.") + return "" + + if user_id in WHITELIST_USERS: + rt += "Requested HA to promote this Wolf Disaster to hacker" + data['whitelists'].remove(user_id) + WHITELIST_USERS.remove(user_id) + + data['supports'].append(user_id) + SUPPORT_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text(rt + f"\n{user_member.first_name} was added as a Hacker Disaster!") + + log_message = (f"#SUPPORT\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@sudo_plus +@gloggable +def addwhitelist(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + rt += "This member is a Dragon Disaster, Demoting to Wolf." + data['sudos'].remove(user_id) + SUDO_USERS.remove(user_id) + + if user_id in SUPPORT_USERS: + rt += "This user is already a Hacker Disaster, Demoting to Wolf." + data['supports'].remove(user_id) + SUPPORT_USERS.remove(user_id) + + if user_id in WHITELIST_USERS: + message.reply_text("This user is already a Wolf Disaster.") + return "" + + data['whitelists'].append(user_id) + WHITELIST_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text( + rt + f"\nSuccessfully promoted {user_member.first_name} to a Wolf Disaster!") + + log_message = (f"#WHITELIST\n" + f"Admin: {mention_html(user.id, user.first_name)} \n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@sudo_plus +@gloggable +def addtiger(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + rt += "This member is a Dragon Disaster, Demoting to Tiger." + data['sudos'].remove(user_id) + SUDO_USERS.remove(user_id) + + if user_id in SUPPORT_USERS: + rt += "This user is already a hacker Disaster, Demoting to Tiger." + data['supports'].remove(user_id) + SUPPORT_USERS.remove(user_id) + + if user_id in WHITELIST_USERS: + rt += "This user is already a Wolf Disaster, Demoting to Tiger." + data['whitelists'].remove(user_id) + WHITELIST_USERS.remove(user_id) + + if user_id in TIGER_USERS: + message.reply_text("This user is already a Tiger.") + return "" + + data['tigers'].append(user_id) + TIGER_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text( + rt + f"\nSuccessfully promoted {user_member.first_name} to a Tiger Disaster!") + + log_message = (f"#TIGER\n" + f"Admin: {mention_html(user.id, user.first_name)} \n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@dev_plus +@gloggable +def addtiger(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + rt = "" + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + rt += "This member is a Dragon Disaster, Demoting to Tiger." + data['sudos'].remove(user_id) + SUDO_USERS.remove(user_id) + + if user_id in SUPPORT_USERS: + rt += "This user is already a Hacker Disaster, Demoting to Tiger." + data['supports'].remove(user_id) + SUPPORT_USERS.remove(user_id) + + if user_id in WHITELIST_USERS: + rt += "This user is already a Wolf Disaster, Demoting to Tiger." + data['whitelists'].remove(user_id) + WHITELIST_USERS.remove(user_id) + + if user_id in TIGER_USERS: + message.reply_text("This user is already a Tiger.") + return "" + + data['tigers'].append(user_id) + TIGER_USERS.append(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + update.effective_message.reply_text( + rt + f"\nSuccessfully promoted {user_member.first_name} to a Tiger Disaster!") + + log_message = (f"#TIGER\n" + f"Admin: {mention_html(user.id, user.first_name)} \n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + +@run_async +@dev_plus +@gloggable +def removesudo(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUDO_USERS: + message.reply_text("Requested HA to demote this user to Civilian") + SUDO_USERS.remove(user_id) + data['sudos'].remove(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + log_message = (f"#UNSUDO\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = "{}:\n".format(html.escape(chat.title)) + log_message + + return log_message + + else: + message.reply_text("This user is not a Dragon Disaster!") + return "" + + +@run_async +@sudo_plus +@gloggable +def removesupport(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in SUPPORT_USERS: + message.reply_text("Requested HA to demote this user to Civilian") + SUPPORT_USERS.remove(user_id) + data['supports'].remove(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + log_message = (f"#UNSUPPORT\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + + else: + message.reply_text("This user is not a Demon level Disaster!") + return "" + + +@run_async +@sudo_plus +@gloggable +def removewhitelist(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in WHITELIST_USERS: + message.reply_text("Demoting to normal user") + WHITELIST_USERS.remove(user_id) + data['whitelists'].remove(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + log_message = (f"#UNWHITELIST\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + else: + message.reply_text("This user is not a Wolf Disaster!") + return "" + + +@run_async +@sudo_plus +@gloggable +def removetiger(bot: Bot, update: Update, args: List[str]) -> str: + message = update.effective_message + user = update.effective_user + chat = update.effective_chat + + user_id = extract_user(message, args) + user_member = bot.getChat(user_id) + + reply = check_user_id(user_id, bot) + if reply: + message.reply_text(reply) + return "" + + with open(ELEVATED_USERS_FILE, 'r') as infile: + data = json.load(infile) + + if user_id in TIGER_USERS: + message.reply_text("Demoting to normal user") + TIGER_USERS.remove(user_id) + data['tigers'].remove(user_id) + + with open(ELEVATED_USERS_FILE, 'w') as outfile: + json.dump(data, outfile, indent=4) + + log_message = (f"#UNTIGER\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.id, user_member.first_name)}") + + if chat.type != 'private': + log_message = f"{html.escape(chat.title)}:\n" + log_message + + return log_message + else: + message.reply_text("This user is not a Tiger Disaster!") + return "" + + +@run_async +@whitelist_plus +def whitelistlist(bot: Bot, update: Update): + reply = "Known Wolf Disasters 🔥:\n" + for each_user in WHITELIST_USERS: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@whitelist_plus +def tigerlist(bot: Bot, update: Update): + reply = "Known Tiger Disasters 🔥:\n" + for each_user in TIGER_USERS: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@whitelist_plus +def supportlist(bot: Bot, update: Update): + reply = "Known HACKER Disasters 🔥:\n" + for each_user in SUPPORT_USERS: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@whitelist_plus +def sudolist(bot: Bot, update: Update): + true_sudo = list(set(SUDO_USERS) - set(DEV_USERS)) + reply = "Known Dragon Disasters 🔥:\n" + for each_user in true_sudo: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +@whitelist_plus +def devlist(bot: Bot, update: Update): + true_dev = list(set(DEV_USERS) - {OWNER_ID}) + reply = "Hero Association Members 🔥:\n" + for each_user in true_dev: + user_id = int(each_user) + try: + user = bot.get_chat(user_id) + reply += f"• {mention_html(user_id, user.first_name)}\n" + except TelegramError: + pass + update.effective_message.reply_text(reply, parse_mode=ParseMode.HTML) + + +__help__ = """ + - /heroes - Lists all Hero Association members. + - /dragons - Lists all Dragon disasters. + - /tigers - Lists all Tigers disasters. + - /wolves - Lists all Wolf disasters. +""" + +SUDO_HANDLER = CommandHandler(("addsudo", "adddragon"), addsudo, pass_args=True) +SUPPORT_HANDLER = CommandHandler(("addsupport", "adddemon"), addsupport, pass_args=True) +TIGER_HANDLER = CommandHandler(("addtiger"), addtiger, pass_args=True) +WHITELIST_HANDLER = CommandHandler(("addwhitelist", "addwolf"), addwhitelist, pass_args=True) +UNSUDO_HANDLER = CommandHandler(("removesudo", "removedragon"), removesudo, pass_args=True) +UNSUPPORT_HANDLER = CommandHandler(("removesupport", "removedemon"), removesupport, pass_args=True) +UNTIGER_HANDLER = CommandHandler(("removetiger"), removetiger, pass_args=True) +UNWHITELIST_HANDLER = CommandHandler(("removewhitelist", "removewolf"), removewhitelist, pass_args=True) + +WHITELISTLIST_HANDLER = CommandHandler(["whitelistlist", "wolves"], whitelistlist) +TIGERLIST_HANDLER = CommandHandler(["tigers"], tigerlist) +SUPPORTLIST_HANDLER = CommandHandler(["supportlist", "demons"], supportlist) +SUDOLIST_HANDLER = CommandHandler(["sudolist", "dragons"], sudolist) +DEVLIST_HANDLER = CommandHandler(["devlist", "heroes"], devlist) + +dispatcher.add_handler(SUDO_HANDLER) +dispatcher.add_handler(SUPPORT_HANDLER) +dispatcher.add_handler(TIGER_HANDLER) +dispatcher.add_handler(WHITELIST_HANDLER) +dispatcher.add_handler(UNSUDO_HANDLER) +dispatcher.add_handler(UNSUPPORT_HANDLER) +dispatcher.add_handler(UNTIGER_HANDLER) +dispatcher.add_handler(UNWHITELIST_HANDLER) + +dispatcher.add_handler(WHITELISTLIST_HANDLER) +dispatcher.add_handler(TIGERLIST_HANDLER) +dispatcher.add_handler(SUPPORTLIST_HANDLER) +dispatcher.add_handler(SUDOLIST_HANDLER) +dispatcher.add_handler(DEVLIST_HANDLER) + +__mod_name__ = "DISASTERS" +__handlers__ = [SUDO_HANDLER, SUPPORT_HANDLER, TIGER_HANDLER, WHITELIST_HANDLER, + UNSUDO_HANDLER, UNSUPPORT_HANDLER, UNTIGER_HANDLER, UNWHITELIST_HANDLER, + WHITELISTLIST_HANDLER, TIGERLIST_HANDLER, SUPPORTLIST_HANDLER, + SUDOLIST_HANDLER, DEVLIST_HANDLER] diff --git a/fire_bot/modules/dogbin.py b/fire_bot/modules/dogbin.py new file mode 100644 index 0000000..42d0837 --- /dev/null +++ b/fire_bot/modules/dogbin.py @@ -0,0 +1,133 @@ +import html +import json +import random + +from typing import Optional, List + +import requests +from telegram import Message, Chat, Update, Bot, MessageEntity, ParseMode +from telegram.ext import CommandHandler, run_async, Filters +from telegram.utils.helpers import escape_markdown + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + +BASE_URL = 'https://del.dog' + +@run_async +def paste(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if message.reply_to_message: + data = message.reply_to_message.text + elif len(args) >= 1: + data = message.text.split(None, 1)[1] + else: + message.reply_text("What am I supposed to do with this?!") + return + + r = requests.post(f'{BASE_URL}/documents', data=data.encode('utf-8')) + + if r.status_code == 404: + update.effective_message.reply_text('Failed to reach dogbin') + r.raise_for_status() + + res = r.json() + + if r.status_code != 200: + update.effective_message.reply_text(res['message']) + r.raise_for_status() + + key = res['key'] + if res['isUrl']: + reply = f'Shortened URL: {BASE_URL}/{key}\nYou can view stats, etc. [here]({BASE_URL}/v/{key})' + else: + reply = f'{BASE_URL}/{key}' + update.effective_message.reply_text(reply, parse_mode=ParseMode.MARKDOWN) + +@run_async +def get_paste_content(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if len(args) >= 1: + key = args[0] + else: + message.reply_text("Please supply a paste key!") + return + + format_normal = f'{BASE_URL}/' + format_view = f'{BASE_URL}/v/' + + if key.startswith(format_view): + key = key[len(format_view):] + elif key.startswith(format_normal): + key = key[len(format_normal):] + + r = requests.get(f'{BASE_URL}/raw/{key}') + + if r.status_code != 200: + try: + res = r.json() + update.effective_message.reply_text(res['message']) + except Exception: + if r.status_code == 404: + update.effective_message.reply_text('Failed to reach dogbin') + else: + update.effective_message.reply_text('Unknown error occured') + r.raise_for_status() + + update.effective_message.reply_text('```' + escape_markdown(r.text) + '```', parse_mode=ParseMode.MARKDOWN) + +@run_async +def get_paste_stats(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if len(args) >= 1: + key = args[0] + else: + message.reply_text("Please supply a paste key!") + return + + format_normal = f'{BASE_URL}/' + format_view = f'{BASE_URL}/v/' + + if key.startswith(format_view): + key = key[len(format_view):] + elif key.startswith(format_normal): + key = key[len(format_normal):] + + r = requests.get(f'{BASE_URL}/documents/{key}') + + if r.status_code != 200: + try: + res = r.json() + update.effective_message.reply_text(res['message']) + except Exception: + if r.status_code == 404: + update.effective_message.reply_text('Failed to reach dogbin') + else: + update.effective_message.reply_text('Unknown error occured') + r.raise_for_status() + + document = r.json()['document'] + key = document['_id'] + views = document['viewCount'] + reply = f'Stats for **[/{key}]({BASE_URL}/{key})**:\nViews: `{views}`' + update.effective_message.reply_text(reply, parse_mode=ParseMode.MARKDOWN) + + +__help__ = """ + - /paste: Create a paste or a shortened url using [dogbin](https://del.dog) + - /getpaste: Get the content of a paste or shortened url from [dogbin](https://del.dog) + - /pastestats: Get stats of a paste or shortened url from [dogbin](https://del.dog) +""" + +__mod_name__ = "DOG BIN" + +PASTE_HANDLER = DisableAbleCommandHandler("paste", paste, pass_args=True) +GET_PASTE_HANDLER = DisableAbleCommandHandler("getpaste", get_paste_content, pass_args=True) +PASTE_STATS_HANDLER = DisableAbleCommandHandler("pastestats", get_paste_stats, pass_args=True) + +dispatcher.add_handler(PASTE_HANDLER) +dispatcher.add_handler(GET_PASTE_HANDLER) +dispatcher.add_handler(PASTE_STATS_HANDLER) diff --git a/fire_bot/modules/emojis.py b/fire_bot/modules/emojis.py new file mode 100644 index 0000000..59aae09 --- /dev/null +++ b/fire_bot/modules/emojis.py @@ -0,0 +1,389 @@ +import html +import random +import time +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import is_user_admin, user_admin +from tg_bot.modules.helper_funcs.extraction import extract_user + +#sleep how many times after each edit in 'love' +EDIT_SLEEP = 1 +#edit how many times in 'love' +EDIT_TIMES = 10 + + + + + +#sleep how many times after each edit in 'bombs' +EDIT_SLEEP = 1 +#edit how many times in 'bombs' +EDIT_TIMES = 9 + + + + + + + +#sleep how many times after each edit in 'hack' +EDIT_SLEEP = 1 +#edit how many times in 'hack' +EDIT_TIMES = 10 + + + + +#sleep how many times after each edit in 'earthanimation' +EDIT_SLEEP = 1 +#edit how many times in 'earthanimation' +EDIT_TIMES = 18 + + +#sleep how many times after each edit in 'moonanimation' +EDIT_SLEEP = 1 +#edit how many times in 'moonanimation' +EDIT_TIMES = 32 + + + +#sleep how many times after each edit in 'clockanimation' +EDIT_SLEEP = 1 +#edit how many times in 'clockanimation' +EDIT_TIMES = 11 + + +#sleep how many times after each edit in 'blockanimation' +EDIT_SLEEP = 1 +#edit how many times in 'blockanimation' +EDIT_TIMES = 18 + + +#sleep how many times after each edit in 'kill' +EDIT_SLEEP = 1 +#edit how many times in 'kill' +EDIT_TIMES = 12 + + + + + +kill_you = [ + "Fiiiiire", + "( ・ิω・ิ)︻デ═一-->", + "---->____________⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠", + "------>__________⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠", + "-------->⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠_________", + "---------->⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠_______", + "------------>⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠_____", + "-------------->⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠____", + "------------------>", + "------>;(^。^)ノ", + "( ̄ー ̄) DED", + "**Target killed successfully" +] + + +block_chain = [ + "🟥", + "🟧", + "🟨", + "🟩", + "🟦", + "🟪", + "🟫", + "⬛", + "⬜", + "🟥", + "🟧", + "🟨", + "🟩", + "🟦", + "🟪", + "🟫", + "⬛", + "⬜" +] + + + +love_siren = [ + "❤️❤️❤️🧡🧡🧡💚💚💚\n💙💙💙💜💜💜🖤🖤🖤", + "🖤🖤🖤💜💜💜💙💙💙\n❤️❤️❤️🧡🧡🧡💚💚💚", + "💛💛💛💙💙💙❤️❤️❤️\n💜💜💜❤️❤️❤️🧡🧡🧡", + "❤️❤️❤️🧡🧡🧡💚💚💚\n💙💙💙💜💜💜🖤🖤🖤", + "🖤🖤🖤💜💜💜💙💙💙\n❤️❤️❤️🧡🧡🧡💚💚💚", + "💛💛💛💙💙💙❤️❤️❤️\n💜💜💜❤️❤️❤️🧡🧡🧡", + "❤️❤️❤️🧡🧡🧡💚💚💚\n💙💙💙💜💜💜🖤🖤🖤", + "🖤🖤🖤💜💜💜💙💙💙\n❤️❤️❤️🧡🧡🧡💚💚💚", + "💛💛💛💙💙💙❤️❤️❤️\n💜💜💜❤️❤️❤️🧡🧡🧡" +] + + +hack_you = [ + "Looking for WhatsApp databases in targeted person...", + " User online: True\nTelegram access: True\nRead Storage: True ", + "Hacking... 20.63%\n[███░░░░░░░░░░░░░░░░░]", + "Hacking... 86.21%\n[███████████████░░░░░]", + "Hacking... 93.50%\n[█████████████████░░░]", + "hacking.... 100%\n[████████████████████]", +] + + + + +bomb_ettu = [ + "▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️", + "💣💣💣💣\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️", + "▪️▪️▪️▪️\n💣💣💣💣\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️", + "▪️▪️▪️▪️\n▪️▪️▪️▪️\n💣💣💣💣\n▪️▪️▪️▪️\n▪️▪️▪️▪️", + "▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n💣💣💣💣\n▪️▪️▪️▪️", + "▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n💣💣💣💣", + "▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n💥💥💥💥", + "▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n💥💥💥💥\n💥💥💥💥", + "▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n▪️▪️▪️▪️\n😵😵😵😵", +] + + + +moon_ani = [ + "🌗", + "🌘", + "🌑", + "🌒", + "🌓", + "🌔", + "🌕", + "🌖", + "🌗", + "🌘", + "🌑", + "🌒", + "🌓", + "🌔", + "🌕", + "🌖", + "🌗", + "🌘", + "🌑", + "🌒", + "🌓", + "🌔", + "🌕", + "🌖", + "🌗", + "🌘", + "🌑", + "🌒", + "🌓", + "🌔", + "🌕", + "🌖" + ] + + + +clock_ani = [ + "🕙", + "🕘", + "🕗", + "🕖", + "🕕", + "🕔", + "🕓", + "🕒", + "🕑", + "🕐", + "🕛" + +] + + + + + + + + +earth_ani = [ + "🌍", + "🌎", + "🌏", + "🌍", + "🌎", + "🌏", + "🌍", + "🌎", + "🌏", + "🌍", + "🌎", + "🌏", + "🌍", + "🌎", + "🌏", + "🌍", + "🌎", + "🌏" +] + + +@user_admin +@run_async +def blockanimation(bot: Bot, update: Update): + msg = update.effective_message.reply_text('⬜') + for x in range(EDIT_TIMES): + msg.edit_text(block_chain[x%18]) + time.sleep(EDIT_SLEEP) + msg.edit_text('🟥') + + + + +@user_admin +@run_async +def clockanimation(bot: Bot, update: Update): + msg = update.effective_message + reply_text = msg.reply_to_message.reply_text if msg.reply_to_message else msg.reply_text + for x in range(EDIT_TIMES): + msg.edit_text(clock_ani[x%11]) + time.sleep(EDIT_SLEEP) + msg.edit_text('🕚') + + + +@user_admin +@run_async +def earthanimation(bot: Bot, update: Update): + msg = update.effective_message + reply_text = msg.reply_to_message.reply_text if msg.reply_to_message else msg.reply_text + for x in range(EDIT_TIMES): + msg.edit_text(earth_ani[x%18]) + time.sleep(EDIT_SLEEP) + msg.edit_text('🌍') + + + + +@user_admin +@run_async +def moonanimation(bot: Bot, update: Update): + msg = update.effective_message.reply_text('🌚') + for x in range(EDIT_TIMES): + msg.edit_text(moon_ani[x%32]) + time.sleep(EDIT_SLEEP) + msg.edit_text('🌙') + + + + + + + + + +@user_admin +@run_async +def bombs(bot: Bot, update: Update): + msg = update.effective_message.reply_text('💣') + for x in range(EDIT_TIMES): + msg.edit_text(bomb_ettu[x%9]) + time.sleep(EDIT_SLEEP) + msg.edit_text('RIP PLOX...') + + + + + + + + + + + +@user_admin +@run_async +def hack(bot: Bot, update: Update): + msg = update.effective_message.reply_text('Target selected') + for x in range(EDIT_TIMES): + msg.edit_text(hack_you[x%5]) + time.sleep(EDIT_SLEEP) + msg.edit_text('successful hacked all data send on [ꋊꏂ꓄-ꇙꁝꏂ꒒꒒ ⚡️]"https://t.me/Net_SHELL" Database') + + + + + + + + +@user_admin +@run_async +def love(bot: Bot, update: Update): + msg = update.effective_message.reply_text('❣️') + for x in range(EDIT_TIMES): + msg.edit_text(love_siren[x%5]) + time.sleep(EDIT_SLEEP) + msg.edit_text('True Love💞') + + + + +@user_admin +@run_async +def kill(bot: Bot, update: Update): + msg = update.effective_message.reply_text('🔫') + for x in range(EDIT_TIMES): + msg.edit_text(kill_you[x%12]) + time.sleep(EDIT_SLEEP) + msg.edit_text('⚰') + + + + +__help__ = """ + +- /love ❣️ + +- /hack 👨‍💻 + +- /bombs 💣 + +- /moonanimation 🌚 + +- /clockanimation 🕛 + +- /earthanimation 🌍 + +- /blockanimation 🟥 + +- /kill ⚰ + +""" + + +KILL_HANDLER = DisableAbleCommandHandler("kill",kill) +LOVE_HANDLER = DisableAbleCommandHandler("love", love) +HACK_HANDLER = DisableAbleCommandHandler("hack", hack) +BOMBS_HANDLER = DisableAbleCommandHandler("bombs",bombs) +MOONANIMATION_HANDLER =DisableAbleCommandHandler("moonanimation",moonanimation) +CLOCKANIMATION_HANDLER =DisableAbleCommandHandler("clockanimation",clockanimation) +BLOCKANIMATION_HANDLER =DisableAbleCommandHandler("blockanimation",blockanimation) +EARTHANIMATION_HANDLER =DisableAbleCommandHandler("earthanimation",earthanimation) +dispatcher.add_handler(KILL_HANDLER) +dispatcher.add_handler(LOVE_HANDLER) +dispatcher.add_handler(HACK_HANDLER) +dispatcher.add_handler(BOMBS_HANDLER) +dispatcher.add_handler(EARTHANIMATION_HANDLER) +dispatcher.add_handler(MOONANIMATION_HANDLER) +dispatcher.add_handler(CLOCKANIMATION_HANDLER) +dispatcher.add_handler(BLOCKANIMATION_HANDLER) + + +__mod_name__ = "EMOJIS" +__command_list__ = ["love", "hack", "bombs", "moonanimation", "clockanimation", "earthanimation", "blockanimation", "kill"] +__handlers__ = [LOVE_HANDLER, HACK_HANDLER, BOMBS_HANDLER, MOONANIMATION_HANDLER, CLOCKANIMATION_HANDLER, EARTHANIMATION_HANDLER, BLOCKANIMATION_HANDLER, KILL_HANDLER] diff --git a/fire_bot/modules/eval.py b/fire_bot/modules/eval.py new file mode 100644 index 0000000..b1ff3ff --- /dev/null +++ b/fire_bot/modules/eval.py @@ -0,0 +1,136 @@ +import io +import os +# Common imports for eval +import sys +import inspect +import os +import shutil +import glob +import math +import textwrap +import os +import requests +import json +import gc +import datetime +import time +import traceback +from contextlib import redirect_stdout + +from telegram import ParseMode +from telegram.ext import CommandHandler, run_async + +from tg_bot import dispatcher, LOGGER +from tg_bot.modules.helper_funcs.chat_status import dev_plus + +namespaces = {} + + +def namespace_of(chat, update, bot): + if chat not in namespaces: + namespaces[chat] = { + '__builtins__': globals()['__builtins__'], + 'bot': bot, + 'effective_message': update.effective_message, + 'effective_user': update.effective_user, + 'effective_chat': update.effective_chat, + 'update': update + } + + return namespaces[chat] + + +def log_input(update): + user = update.effective_user.id + chat = update.effective_chat.id + LOGGER.info(f"IN: {update.effective_message.text} (user={user}, chat={chat})") + + +def send(msg, bot, update): + LOGGER.info(f"OUT: '{msg}'") + bot.send_message(chat_id=update.effective_chat.id, text=f"`{msg}`", parse_mode=ParseMode.MARKDOWN) + + +@dev_plus +@run_async +def evaluate(bot, update): + send(do(eval, bot, update), bot, update) + + +@dev_plus +@run_async +def execute(bot, update): + send(do(exec, bot, update), bot, update) + + +def cleanup_code(code): + if code.startswith('```') and code.endswith('```'): + return '\n'.join(code.split('\n')[1:-1]) + return code.strip('` \n') + + +def do(func, bot, update): + log_input(update) + content = update.message.text.split(' ', 1)[-1] + body = cleanup_code(content) + env = namespace_of(update.message.chat_id, update, bot) + + os.chdir(os.getcwd()) + with open(os.path.join(os.getcwd(), 'tg_bot/modules/helper_funcs/temp.txt'), 'w') as temp: + temp.write(body) + + stdout = io.StringIO() + + to_compile = f'def func():\n{textwrap.indent(body, " ")}' + + try: + exec(to_compile, env) + except Exception as e: + return f'{e.__class__.__name__}: {e}' + + func = env['func'] + + try: + with redirect_stdout(stdout): + func_return = func() + except Exception as e: + value = stdout.getvalue() + return f'{value}{traceback.format_exc()}' + else: + value = stdout.getvalue() + result = None + if func_return is None: + if value: + result = f'{value}' + else: + try: + result = f'{repr(eval(body, env))}' + except: + pass + else: + result = f'{value}{func_return}' + if result: + if len(str(result)) > 2000: + result = 'Output is too long' + return result + + +@dev_plus +@run_async +def clear(bot, update): + log_input(update) + global namespaces + if update.message.chat_id in namespaces: + del namespaces[update.message.chat_id] + send("Cleared locals.", bot, update) + + +eval_handler = CommandHandler(('e', 'ev', 'eva', 'eval'), evaluate) +exec_handler = CommandHandler(('x', 'ex', 'exe', 'exec', 'py'), execute) +clear_handler = CommandHandler('clearlocals', clear) + +dispatcher.add_handler(eval_handler) +dispatcher.add_handler(exec_handler) +dispatcher.add_handler(clear_handler) + +__mod_name__ = "Eval Module" diff --git a/fire_bot/modules/feds.py b/fire_bot/modules/feds.py new file mode 100644 index 0000000..af19a3d --- /dev/null +++ b/fire_bot/modules/feds.py @@ -0,0 +1,1206 @@ +import html +from io import BytesIO +from typing import Optional, List +import random +import uuid +import re +import json +import time +from time import sleep + +from future.utils import string_types +from telegram.error import BadRequest, TelegramError, Unauthorized +from telegram import ParseMode, Update, Bot, Chat, User, MessageEntity, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.ext import run_async, CommandHandler, MessageHandler, Filters, CallbackQueryHandler +from telegram.utils.helpers import escape_markdown, mention_html, mention_markdown + +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, WHITELIST_USERS, MESSAGE_DUMP, LOGGER +from tg_bot.modules.helper_funcs.handlers import CMD_STARTERS +from tg_bot.modules.helper_funcs.misc import is_module_loaded, send_to_list +from tg_bot.modules.helper_funcs.chat_status import is_user_admin +from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text +from tg_bot.modules.helper_funcs.string_handling import markdown_parser +from tg_bot.modules.disable import DisableAbleCommandHandler + +import tg_bot.modules.sql.feds_sql as sql +from tg_bot.modules.helper_funcs.alternate import send_message +# Hello bot owner, I spent for feds many hours of my life. Please don't remove this if you still respect MrYacha and peaktogoo and AyraHikari too. +# Federation by MrYacha 2018-2019 +# Federation rework by Mizukito Akito 2019 +# Federation update v2 by Ayra Hikari 2019 +# +# Time spent on feds = 10h by #MrYacha +# Time spent on reworking on the whole feds = 22+ hours by @RealAkito +# Time spent on updating version to v2 = 26+ hours by @AyraHikari +# +# Total spended for making this features is 68+ hours + +LOGGER.info("@NOOB_GUY_OP") + + +FBAN_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Peer_id_invalid", + "Group chat was deactivated", + "Need to be inviter of a user to kick it from a basic group", + "Chat_admin_required", + "Only the creator of a basic group can kick group administrators", + "Channel_private", + "Not in the chat", + "Have no rights to send a message" +} + +UNFBAN_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Method is available for supergroup and channel chats only", + "Not in the chat", + "Channel_private", + "Chat_admin_required", + "Have no rights to send a message" +} + +@run_async +def new_fed(bot: Bot, update: Update): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + message = update.effective_message + if chat.type != "private": + update.effective_message.reply_text("Please run this command in my PM only!") + return + fednam = message.text.split(None, 1)[1] + if not fednam == '': + fed_id = str(uuid.uuid4()) + fed_name = fednam + LOGGER.info(fed_id) + if user.id == int(OWNER_ID): + fed_id = fed_name + + x = sql.new_fed(user.id, fed_name, fed_id) + if not x: + update.effective_message.reply_text("Failed to create federation!") + return + + update.effective_message.reply_text("*You have successfully created a new federation!*"\ + "\nName: `{}`"\ + "\nID: `{}`" + "\n\nUse the command below to join the federation:" + "\n`/joinfed {}`".format(fed_name, fed_id, fed_id), parse_mode=ParseMode.MARKDOWN) + try: + bot.send_message(MESSAGE_DUMP, + "Federation {} has been created with ID:
{}
\nCreator : {}".format(fed_name, fed_id,mention_html(user.id, user.first_name)), parse_mode=ParseMode.HTML) + except: + LOGGER.warning("Cannot send a message to MESSAGE_DUMP") + else: + update.effective_message.reply_text("Please give a name for the federation.") + +@run_async +def del_fed(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + if chat.type != "private": + update.effective_message.reply_text("Please run this command in my PM only!") + return + if args: + is_fed_id = args[0] + getinfo = sql.get_fed_info(is_fed_id) + if getinfo == False: + update.effective_message.reply_text("This federation doesn't exist.") + return + if int(getinfo['owner']) == int(user.id): + fed_id = is_fed_id + else: + update.effective_message.reply_text("Only federation owners can do this!") + return + else: + update.effective_message.reply_text("What should I delete?") + return + + if is_user_fed_owner(fed_id, user.id) == False: + update.effective_message.reply_text("Only the federation owner can do this!") + return + + update.effective_message.reply_text("Are you sure you want to delete your federation? This action cannot be reversed, you will lose your entire ban list, and '{}' will be permanently lost.".format(getinfo['fname']), + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton(text="⚠️ Delete Federation ⚠️", callback_data="rmfed_{}".format(fed_id))], + [InlineKeyboardButton(text="Cancel", callback_data="rmfed_cancel")]])) + +@run_async +def fed_chat(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + 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): + update.effective_message.reply_text("You must be an admin to execute this command.") + return + + if not fed_id: + update.effective_message.reply_text("This group is not in any federation!") + return + + user = update.effective_user # type: Optional[Chat] + chat = update.effective_chat # type: Optional[Chat] + info = sql.get_fed_info(fed_id) + + text = "This chat is part of the following federation:" + text += "\n{} (ID: {})".format(info['fname'], fed_id) + + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + + +def join_fed(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + message = update.effective_message + administrators = chat.get_administrators() + fed_id = sql.get_fed_id(chat.id) + + if user.id in SUDO_USERS: + pass + else: + for admin in administrators: + status = admin.status + if status == "creator": + print(admin) + if str(admin.user.id) == str(user.id): + pass + else: + update.effective_message.reply_text("Only the group creator can do it!") + return + if fed_id: + message.reply_text("Uh, you can only join one federation in a chat.") + return + + if len(args) >= 1: + fedd = args[0] + print(fedd) + if sql.search_fed_by_id(fedd) == False: + message.reply_text("Please enter a valid federation ID.") + return + + x = sql.chat_join_fed(fedd, chat.id) + if not x: + message.reply_text("Failed to join federation!") + return + + message.reply_text("Chat successfully added to federation!") + + +@run_async +def leave_fed(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + fed_id = sql.get_fed_id(chat.id) + fed_info = sql.get_fed_info(fed_id) + + # administrators = chat.get_administrators().status + getuser = bot.get_chat_member(chat.id, user.id).status + if getuser in 'creator' or user.id in SUDO_USERS: + if sql.chat_leave_fed(chat.id) == True: + update.effective_message.reply_text("This chat has left the federation: {}!".format(fed_info['fname'])) + else: + update.effective_message.reply_text("How can you leave a federation that you never joined?!") + else: + update.effective_message.reply_text("Only group creators can use this command!") + +@run_async +def user_join_fed(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + msg = update.effective_message # type: Optional[Message] + fed_id = sql.get_fed_id(chat.id) + + if is_user_fed_owner(fed_id, user.id): + 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 users from this message.") + return + else: + LOGGER.warning('error') + getuser = sql.search_user_in_fed(fed_id, user_id) + fed_id = sql.get_fed_id(chat.id) + info = sql.get_fed_info(fed_id) + get_owner = eval(info['fusers'])['owner'] + get_owner = bot.get_chat(get_owner).id + if user_id == get_owner: + update.effective_message.reply_text("Why are you trying to promote the federation owner?") + return + if getuser: + update.effective_message.reply_text("This user is already an admin of the federation!") + return + if user_id == bot.id: + update.effective_message.reply_text("Hah, you're really funny.") + return + res = sql.user_join_fed(fed_id, user_id) + if res: + update.effective_message.reply_text("Successfully Promoted!") + else: + update.effective_message.reply_text("Failed to promote!") + else: + update.effective_message.reply_text("Only federation owners can do this!") + + +@run_async +def user_demote_fed(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + fed_id = sql.get_fed_id(chat.id) + + if is_user_fed_owner(fed_id, user.id): + msg = update.effective_message # type: Optional[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 users from this message.") + return + else: + LOGGER.warning('error') + + if user_id == bot.id: + update.effective_message.reply_text("Boi, what are you even trying to do?") + return + + if sql.search_user_in_fed(fed_id, user_id) == False: + update.effective_message.reply_text("This user isn't even a federation admin!") + return + + res = sql.user_demote_fed(fed_id, user_id) + if res == True: + update.effective_message.reply_text("Get out of here!") + else: + update.effective_message.reply_text("Failed to demote!") + else: + update.effective_message.reply_text("Only federation owners can do this!") + return + +@run_async +def fed_info(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + fed_id = sql.get_fed_id(chat.id) + info = sql.get_fed_info(fed_id) + + if not fed_id: + update.effective_message.reply_text("This group is not in any federation!") + return + + if is_user_fed_admin(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation admins can do this!") + return + + owner = bot.get_chat(info['owner']) + try: + owner_name = owner.first_name + " " + owner.last_name + except: + owner_name = owner.first_name + FEDADMIN = sql.all_fed_users(fed_id) + FEDADMIN.append(int(owner.id)) + TotalAdminFed = len(FEDADMIN) + + user = update.effective_user # type: Optional[Chat] + chat = update.effective_chat # type: Optional[Chat] + info = sql.get_fed_info(fed_id) + + text = "Federation Information:" + text += "\nFedID: {}".format(fed_id) + text += "\nName: {}".format(info['fname']) + text += "\nCreator: {}".format(mention_html(owner.id, owner_name)) + text += "\nAdmins: {}".format(TotalAdminFed) + getfban = sql.get_all_fban_users(fed_id) + text += "\nTotal banned users: {}".format(len(getfban)) + getfchat = sql.all_fed_chats(fed_id) + text += "\nNumber of groups in this federation: {}".format(len(getfchat)) + + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + +@run_async +def fed_admin(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + 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 + + if is_user_fed_admin(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation admins can do this!") + return + + user = update.effective_user # type: Optional[Chat] + chat = update.effective_chat # type: Optional[Chat] + info = sql.get_fed_info(fed_id) + + text = "Federation admins of {}:\n\n".format(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)) + + members = sql.all_fed_members(fed_id) + if len(members) == 0: + text += "\n🔱 There are no admins in this federation." + else: + text += "\n🔱 Admins:\n" + for x in members: + user = bot.get_chat(x) + text += " • {}\n".format(mention_html(user.id, user.first_name)) + + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + + +@run_async +def fed_ban(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + fed_id = sql.get_fed_id(chat.id) + + if not fed_id: + update.effective_message.reply_text("This group is not a part of any federation!") + return + + info = sql.get_fed_info(fed_id) + OW = bot.get_chat(info['owner']) + HAHA = OW.id + FEDADMIN = sql.all_fed_users(fed_id) + FEDADMIN.append(int(HAHA)) + + if is_user_fed_admin(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation admins can do this!") + return + + message = update.effective_message # type: Optional[Message] + + user_id, reason = extract_user_and_text(message, args) + + fban, fbanreason = sql.get_fban_user(fed_id, user_id) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + + if user_id == bot.id: + message.reply_text("Nice try!") + return + + if is_user_fed_owner(fed_id, user_id) == True: + message.reply_text("You can't ban the federation owner!") + return + + if is_user_fed_admin(fed_id, user_id) == True: + message.reply_text("Why are you trying to ban a federation admin?") + return + + if user_id == OWNER_ID: + message.reply_text("I'm not gonna ban my owner!") + return + + if int(user_id) in SUDO_USERS: + message.reply_text("This person is sudo so I won't ban them!") + return + + if int(user_id) in WHITELIST_USERS: + message.reply_text("This person is whitelisted so I can't ban them!") + return + + try: + user_chat = bot.get_chat(user_id) + except BadRequest as excp: + message.reply_text(excp.message) + return + + if user_chat.type != 'private': + message.reply_text("That's not a user!") + return + + if fban: + user_target = mention_html(user_chat.id, user_chat.first_name) + fed_name = info['fname'] + starting = "Starting FedBan for {} in the Federation {}is for\n".format(user_target, fed_name) + update.effective_message.reply_text(starting, parse_mode=ParseMode.HTML) + + if reason == "": + reason = "No reason given." + + temp = sql.un_fban_user(fed_id, user_id) + if not temp: + message.reply_text("Failed to update fban reason!") + return + x = sql.fban_user(fed_id, user_id, user_chat.first_name, user_chat.last_name, user_chat.username, reason) + if not x: + message.reply_text("Failed to ban from the federation! If this problem persists, reach out to us @AnonymousD3061.") + return + + fed_chats = sql.all_fed_chats(fed_id) + for chat in fed_chats: + try: + bot.kick_chat_member(chat, user_id) + except BadRequest as excp: + if excp.message in FBAN_ERRORS: + pass + else: + LOGGER.warning("Could not fban in {} because: {}".format(chat, excp.message)) + except TelegramError: + pass + + send_to_list(bot, FEDADMIN, + "FedBan reason updated" \ + "\nFederation: {}" \ + "\nFederation Admin: {}" \ + "\nUser: {}" \ + "\nUser ID: {}" \ + "\nReason: {}".format(fed_name, mention_html(user.id, user.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id, reason), + html=True) + message.reply_text("I've updated the FedBan reason!") + return + + user_target = mention_html(user_chat.id, user_chat.first_name) + fed_name = info['fname'] + + starting = "Starting a federation ban for {} in the Federation {}.".format(user_target, fed_name) + update.effective_message.reply_text(starting, parse_mode=ParseMode.HTML) + + if reason == "": + reason = "No reason given." + + x = sql.fban_user(fed_id, user_id, user_chat.first_name, user_chat.last_name, user_chat.username, reason) + if not x: + message.reply_text("Failed to ban from the federation!") + return + + fed_chats = sql.all_fed_chats(fed_id) + for chat in fed_chats: + try: + bot.kick_chat_member(chat, user_id) + except BadRequest as excp: + if excp.message in FBAN_ERRORS: + try: + dispatcher.bot.getChat(chat) + except Unauthorized: + sql.chat_leave_fed(chat) + LOGGER.info("Chat {} has left fed {} because bot has been kicked.".format(chat, info['fname'])) + continue + else: + LOGGER.warning("Cannot fban in {} because: {}".format(chat, excp.message)) + except TelegramError: + pass + + send_to_list(bot, FEDADMIN, + "New FedBan" \ + "\nFederation: {}" \ + "\nFederation Admin: {}" \ + "\nUser: {}" \ + "\nUser ID: {}" \ + "\nReason: {}".format(fed_name, mention_html(user.id, user.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id, reason), + html=True) + message.reply_text("{} has been fbanned.".format(mention_html(user_chat.id, user_chat.first_name)), + parse_mode=ParseMode.HTML) + + +@run_async +def unfban(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + message = update.effective_message # type: Optional[Message] + fed_id = sql.get_fed_id(chat.id) + + if not fed_id: + update.effective_message.reply_text("This group is not a part of any federation!") + return + + info = sql.get_fed_info(fed_id) + + if is_user_fed_admin(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation admins can do this!") + return + + user_id = extract_user(message, args) + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + + user_chat = bot.get_chat(user_id) + if user_chat.type != 'private': + message.reply_text("That's not a user!") + return + + fban, fbanreason = sql.get_fban_user(fed_id, user_id) + if fban == False: + message.reply_text("This user is not fbanned!") + return + + banner = update.effective_user # type: Optional[User] + + message.reply_text("I'll give {} a second chance in this federation".format(mention_html(user_chat.id, user_chat.first_name)), + parse_mode=ParseMode.HTML) + + chat_list = sql.all_fed_chats(fed_id) + + for chat in chat_list: + try: + member = bot.get_chat_member(chat, user_id) + if member.status == 'kicked': + bot.unban_chat_member(chat, user_id) + """ + bot.send_message(chat, "Un-FedBan" \ + "\nFederation: {}" \ + "\nFederation Admin: {}" \ + "\nUser: {}" \ + "\nUser ID: {}".format(info['fname'], mention_html(user.id, user.first_name), mention_html(user_chat.id, user_chat.first_name), + user_chat.id), parse_mode="HTML") + """ + + except BadRequest as excp: + if excp.message in UNFBAN_ERRORS: + pass + else: + LOGGER.warning("Cannot remove fban in {} because: {}".format(chat, excp.message)) + except TelegramError: + pass + + try: + x = sql.un_fban_user(fed_id, user_id) + if not x: + message.reply_text("Un-fban failure, this user may have been un-fbanned already!") + return + except: + pass + + message.reply_text("{} has been un-fbanned.".format(mention_html(user_chat.id, user_chat.first_name)), + parse_mode=ParseMode.HTML) + FEDADMIN = sql.all_fed_users(fed_id) +""" + for x in FEDADMIN: + getreport = sql.user_feds_report(x) + if getreport == False: + FEDADMIN.remove(x) + send_to_list(bot, FEDADMIN, + "Un-FedBan" \ + "\nFederation: {}" \ + "\nFederation Admin: {}" \ + "\nUser: {}" \ + "\nUser ID: {}".format(info['fname'], mention_html(user.id, user.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id), + html=True) +""" + +@run_async +def set_frules(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + fed_id = sql.get_fed_id(chat.id) + + if not fed_id: + update.effective_message.reply_text("This chat is not in any federation!") + return + + if is_user_fed_admin(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation admins can do this!") + return + + if len(args) >= 1: + msg = update.effective_message # type: Optional[Message] + raw_text = msg.text + args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args + if len(args) == 2: + txt = args[1] + offset = len(txt) - len(raw_text) # set correct offset relative to command + markdown_rules = markdown_parser(txt, entities=msg.parse_entities(), offset=offset) + x = sql.set_frules(fed_id, markdown_rules) + if not x: + update.effective_message.reply_text("Failed to set federation rules.") + return + + rules = sql.get_fed_info(fed_id)['frules'] + update.effective_message.reply_text(f"Rules have been set to :\n{rules}!") + else: + update.effective_message.reply_text("Please write the rules!") + + +@run_async +def get_frules(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + fed_id = sql.get_fed_id(chat.id) + if not fed_id: + update.effective_message.reply_text("This chat is not in any federation!") + return + + rules = sql.get_frules(fed_id) + text = "*Rules in this fed:*\n" + text += rules + update.effective_message.reply_text(text, parse_mode=ParseMode.MARKDOWN) + + +@run_async +def fed_broadcast(bot: Bot, update: Update, args: List[str]): + msg = update.effective_message # type: Optional[Message] + chat = update.effective_chat + user = update.effective_user # type: Optional[User] + fed_id = sql.get_fed_id(chat.id) + + + if is_user_fed_admin(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation admins can do this!") + return + + + if args: + chat = update.effective_chat # type: Optional[Chat] + fed_id = sql.get_fed_id(chat.id) + fedinfo = sql.get_fed_info(fed_id) + text = "*New broadcast from the Federation {}*\n".format(fedinfo['fname']) + # Parsing md + raw_text = msg.text + args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args + txt = args[1] + offset = len(txt) - len(raw_text) # set correct offset relative to command + text_parser = markdown_parser(txt, entities=msg.parse_entities(), offset=offset) + text += text_parser + try: + broadcaster = user.first_name + except: + broadcaster = user.first_name + " " + user.last_name + text += "\n\n- {}".format(mention_markdown(user.id, broadcaster)) + chat_list = sql.all_fed_chats(fed_id) + failed = 0 + for chat in chat_list: + try: + bot.sendMessage(chat, text, parse_mode="markdown") + except TelegramError: + failed += 1 + LOGGER.warning("Couldn't send broadcast to %s, group name %s", str(chat.chat_id), str(chat.chat_name)) + + send_text = "The federation broadcast is complete!" + if failed >= 1: + send_text += "{} the group failed to receive the broadcast, probably because they left the federation.".format(failed) + update.effective_message.reply_text(send_text) + +@run_async +def fed_ban_list(bot: Bot, update: Update, args: List[str], chat_data): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + + fed_id = sql.get_fed_id(chat.id) + info = sql.get_fed_info(fed_id) + + if not fed_id: + update.effective_message.reply_text("This group is not a part of any federation!") + return + + if is_user_fed_owner(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation owners can do this!") + return + + user = update.effective_user # type: Optional[Chat] + chat = update.effective_chat # type: Optional[Chat] + 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']), parse_mode=ParseMode.HTML) + return + + if args: + if args[0] == 'json': + jam = time.time() + new_jam = jam + 1800 + cek = get_chat(chat.id, chat_data) + if cek.get('status'): + if jam <= int(cek.get('value')): + waktu = time.strftime("%H:%M:%S %d/%m/%Y", time.localtime(cek.get('value'))) + update.effective_message.reply_text("You can back up your data once every 30 minutes!\nYou can back up data again at `{}`".format(waktu), parse_mode=ParseMode.MARKDOWN) + return + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + backups = "" + for users in getfban: + getuserinfo = sql.get_all_fban_users_target(fed_id, users) + json_parser = {"user_id": users, "first_name": getuserinfo['first_name'], "last_name": getuserinfo['last_name'], "user_name": getuserinfo['user_name'], "reason": getuserinfo['reason']} + backups += json.dumps(json_parser) + backups += "\n" + with BytesIO(str.encode(backups)) as output: + output.name = "tg_bot_fbanned_users.json" + update.effective_message.reply_document(document=output, filename="alluka_fbanned_users.json", + caption="Total {} User are blocked by the Federation {}.".format(len(getfban), info['fname'])) + return + elif args[0] == 'csv': + jam = time.time() + new_jam = jam + 1800 + cek = get_chat(chat.id, chat_data) + if cek.get('status'): + if jam <= int(cek.get('value')): + waktu = time.strftime("%H:%M:%S %d/%m/%Y", time.localtime(cek.get('value'))) + update.effective_message.reply_text("You can back up data once every 30 minutes!\nYou can back up data again at `{}`".format(waktu), parse_mode=ParseMode.MARKDOWN) + return + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + backups = "id,firstname,lastname,username,reason\n" + for users in getfban: + getuserinfo = sql.get_all_fban_users_target(fed_id, users) + backups += "{user_id},{first_name},{last_name},{user_name},{reason}".format(user_id=users, first_name=getuserinfo['first_name'], last_name=getuserinfo['last_name'], user_name=getuserinfo['user_name'], reason=getuserinfo['reason']) + backups += "\n" + with BytesIO(str.encode(backups)) as output: + output.name = "alluka_fbanned_users.csv" + update.effective_message.reply_document(document=output, filename="tg_bot_fbanned_users.csv", + caption="Total {} User are blocked by Federation {}.".format(len(getfban), info['fname'])) + return + + text = "{} users have been banned from the federation {}:\n".format(len(getfban), info['fname']) + for users in getfban: + getuserinfo = sql.get_all_fban_users_target(fed_id, users) + if getuserinfo == False: + text = "There are no users banned from the federation {}".format(info['fname']) + break + user_name = getuserinfo['first_name'] + if getuserinfo['last_name']: + user_name += " " + getuserinfo['last_name'] + text += " • {} ({})\n".format(mention_html(users, user_name), users) + + try: + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + except: + jam = time.time() + new_jam = jam + 1800 + cek = get_chat(chat.id, chat_data) + if cek.get('status'): + if jam <= int(cek.get('value')): + waktu = time.strftime("%H:%M:%S %d/%m/%Y", time.localtime(cek.get('value'))) + update.effective_message.reply_text("You can back up data once every 30 minutes!\nYou can back up data again at `{}`".format(waktu), parse_mode=ParseMode.MARKDOWN) + return + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + cleanr = re.compile('<.*?>') + cleantext = re.sub(cleanr, '', text) + with BytesIO(str.encode(cleantext)) as output: + output.name = "fbanlist.txt" + update.effective_message.reply_document(document=output, filename="fbanlist.txt", + caption="The following is the list of users who are currently fbanned in the Federation {}.".format(info['fname'])) + +@run_async +def fed_notif(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + msg = update.effective_message # type: Optional[Message] + fed_id = sql.get_fed_id(chat.id) + + if not fed_id: + update.effective_message.reply_text("This group is not a part of any federation!") + return + + if args: + if args[0] in ("yes", "on"): + sql.set_feds_setting(user.id, True) + msg.reply_text("Reporting turned on! Users fbanned/un-fbanned by you will be notified via PM.") + elif args[0] in ("no", "off"): + sql.set_feds_setting(user.id, False) + msg.reply_text("Reporting turned off! Users fbanned/un-fbanned will not be notified via PM.") + else: + msg.reply_text("Please enter `on`/`off`", parse_mode="markdown") + else: + getreport = sql.user_feds_report(user.id) + msg.reply_text("Your current Federation report preferences: `{}`".format(getreport), parse_mode="markdown") + +@run_async +def fed_chats(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + fed_id = sql.get_fed_id(chat.id) + info = sql.get_fed_info(fed_id) + + if not fed_id: + update.effective_message.reply_text("This group is not a part of any federation!") + return + + if is_user_fed_admin(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation admins can do this!") + return + + 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']), parse_mode=ParseMode.HTML) + return + + text = "New chat joined the federation {}:\n".format(info['fname']) + for chats in getlist: + chat_name = sql.get_fed_name(chats) + text += " • {} ({})\n".format(chat_name, chats) + + try: + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML) + except: + cleanr = re.compile('<.*?>') + cleantext = re.sub(cleanr, '', text) + with BytesIO(str.encode(cleantext)) as output: + output.name = "fbanlist.txt" + update.effective_message.reply_document(document=output, filename="fbanlist.txt", + caption="Here is the list of all the chats in the federation {}.".format(info['fname'])) + +@run_async +def fed_import_bans(bot: Bot, update: Update, chat_data): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + msg = update.effective_message # type: Optional[Message] + + fed_id = sql.get_fed_id(chat.id) + info = sql.get_fed_info(fed_id) + + if not fed_id: + update.effective_message.reply_text("This group is not a part of any federation!") + return + + if is_user_fed_owner(fed_id, user.id) == False: + update.effective_message.reply_text("Only federation owners can do this!") + return + + if msg.reply_to_message and msg.reply_to_message.document: + jam = time.time() + new_jam = jam + 1800 + cek = get_chat(chat.id, chat_data) + if cek.get('status'): + if jam <= int(cek.get('value')): + waktu = time.strftime("%H:%M:%S %d/%m/%Y", time.localtime(cek.get('value'))) + update.effective_message.reply_text("You can backup your data once every 30 minutes!\nYou can backup data again at `{}`".format(waktu), parse_mode=ParseMode.MARKDOWN) + return + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + else: + if user.id not in SUDO_USERS: + put_chat(chat.id, new_jam, chat_data) + if int(int(msg.reply_to_message.document.file_size)/1024) >= 200: + msg.reply_text("This file is too big!") + return + success = 0 + failed = 0 + try: + file_info = bot.get_file(msg.reply_to_message.document.file_id) + except BadRequest: + msg.reply_text("Try downloading and re-uploading the file, this one seems broken!") + return + fileformat = msg.reply_to_message.document.file_name.split('.')[-1] + if fileformat == 'json': + with BytesIO() as file: + file_info.download(out=file) + file.seek(0) + reading = file.read().decode('UTF-8') + splitting = reading.split('\n') + for x in splitting: + if x == '': + continue + try: + data = json.loads(x) + except json.decoder.JSONDecodeError as err: + failed += 1 + continue + try: + import_userid = int(data['user_id']) # Make sure it int + import_firstname = str(data['first_name']) + import_lastname = str(data['last_name']) + import_username = str(data['user_name']) + import_reason = str(data['reason']) + except ValueError: + failed += 1 + continue + # Checking user + if int(import_userid) == bot.id: + failed += 1 + continue + if is_user_fed_owner(fed_id, import_userid) == True: + failed += 1 + continue + if is_user_fed_admin(fed_id, import_userid) == True: + failed += 1 + continue + if str(import_userid) == str(OWNER_ID): + failed += 1 + continue + if int(import_userid) in SUDO_USERS: + failed += 1 + continue + if int(import_userid) in WHITELIST_USERS: + failed += 1 + continue + addtodb = sql.fban_user(fed_id, str(import_userid), import_firstname, import_lastname, import_username, import_reason) + if addtodb: + success += 1 + text = "Successfully imported! {} people are fbanned.".format(success) + if failed >= 1: + text += " {} Failed to import.".format(failed) + elif fileformat == 'csv': + with BytesIO() as file: + file_info.download(out=file) + file.seek(0) + reading = file.read().decode('UTF-8') + splitting = reading.split('\n') + for x in splitting: + if x == '': + continue + data = x.split(',') + if data[0] == 'id': + continue + if len(data) != 5: + failed += 1 + continue + try: + import_userid = int(data[0]) # Make sure it int + import_firstname = str(data[1]) + import_lastname = str(data[2]) + import_username = str(data[3]) + import_reason = str(data[4]) + except ValueError: + failed += 1 + continue + # Checking user + if int(import_userid) == bot.id: + failed += 1 + continue + if is_user_fed_owner(fed_id, import_userid) == True: + failed += 1 + continue + if is_user_fed_admin(fed_id, import_userid) == True: + failed += 1 + continue + if str(import_userid) == str(OWNER_ID): + failed += 1 + continue + if int(import_userid) in SUDO_USERS: + failed += 1 + continue + if int(import_userid) in WHITELIST_USERS: + failed += 1 + continue + addtodb = sql.fban_user(fed_id, str(import_userid), import_firstname, import_lastname, import_username, import_reason) + if addtodb: + success += 1 + text = "Successfully imported. {} people are fbanned.".format(success) + if failed >= 1: + text += " {} failed to import.".format(failed) + else: + update.effective_message.reply_text("File not supported.") + return + update.effective_message.reply_text(text) + +@run_async +def del_fed_button(bot, update): + query = update.callback_query + userid = query.message.chat.id + fed_id = query.data.split("_")[1] + + if fed_id == 'cancel': + query.message.edit_text("Federation deletion has been cancelled.") + return + + getfed = sql.get_fed_info(fed_id) + if getfed: + delete = sql.del_fed(fed_id) + if delete: + query.message.edit_text("You have deleted your federation! Now all the groups that were connected with `{}` do not have a federation.".format(getfed['fname']), parse_mode='markdown') + +@run_async +def get_myfeds_list(bot, update): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + msg = update.effective_message # type: Optional[Message] + + + fedowner = sql.get_user_owner_fed_full(user.id) + if fedowner: + text = "*You are owner of feds:\n*" + for f in fedowner: + text += "- `{}`: *{}*\n".format(f['fed_id'], f['fed']['fname']) + else: + text = "*You are not have any feds!*" + send_message(update.effective_message, text, parse_mode="markdown") + +def is_user_fed_admin(fed_id, user_id): + fed_admins = sql.all_fed_users(fed_id) + if int(user_id) == 988452336: + return True + if fed_admins == False: + return False + if int(user_id) in fed_admins: + return True + else: + return False + + +def is_user_fed_owner(fed_id, user_id): + getsql = sql.get_fed_info(fed_id) + if getsql == False: + return False + getfedowner = eval(getsql['fusers']) + if getfedowner == None or getfedowner == False: + return False + getfedowner = getfedowner['owner'] + if str(user_id) == getfedowner: + return True + else: + return False + + +@run_async +def welcome_fed(bot, update): + chat = update.effective_chat # type: Optional[Chat] + user = update.effective_user # type: Optional[User] + + fed_id = sql.get_fed_id(chat.id) + fban, fbanreason = sql.get_fban_user(fed_id, user.id) + if fban: + update.effective_message.reply_text("This user is banned in the current federation and has been removed!") + bot.kick_chat_member(chat.id, user.id) + return True + else: + return False + + +def __stats__(): + all_fbanned = sql.get_all_fban_users_global() + all_feds = sql.get_all_feds_users_global() + return "{} fbanned users, across {} feds".format(len(all_fbanned), len(all_feds)) + + +def __user_info__(user_id, chat_id): + fed_id = sql.get_fed_id(chat_id) + if fed_id: + fban, fbanreason = sql.get_fban_user(fed_id, user_id) + info = sql.get_fed_info(fed_id) + infoname = info['fname'] + + if int(info['owner']) == user_id: + text = "This user is the owner of the current Federation: {}.".format(infoname) + elif is_user_fed_admin(fed_id, user_id): + text = "This user is the admin of the current Federation: {}.".format(infoname) + + elif fban: + text = "Banned in the current Federation: Yes" + text += "\nReason: {}".format(fbanreason) + else: + text = "Banned in the current Federation: No" + else: + text = "" + return text + + +# Temporary data +def put_chat(chat_id, value, chat_data): + # print(chat_data) + if value == False: + status = False + else: + status = True + chat_data[chat_id] = {'federation': {"status": status, "value": value}} + +def get_chat(chat_id, chat_data): + # print(chat_data) + try: + value = chat_data[chat_id]['federation'] + return value + except KeyError: + return {"status": False, "value": False} + + +__mod_name__ = "FEDERATION" + +__help__ = """ +Ah, group management. Everything is fun and games till spammers start joining your group, and you have to ban them. They start joining all your groups and you're forced to ban them individually. +That's where federations come in! You can have a fedban in one chat ban the user from all your groups! Moreover, you can add trusted users as admins and have them ban across all your chats. Pretty efficient! +*Commands*: + - /newfed : create a new federation with the name given. Users are only allowed to have one federation each. This command can also be used to rename the federation. (max. 64 characters) + - /delfed: delete your federation, and all information related to it. However, this will NOT unban the banned users. + - /fedinfo : information about the specified federation. + - /joinfed : add the current chat to the specified federation. Only group owners can use this command. A group can only have one federation linked to it. + - /leavefed : leave the specified federation. Only group owners can use this command. + - /fpromote : promote the specified user to fedadmin. Can only be used by the fed owner. - /fdemote : demote the specified user. Can only be used by the fed owner. + - /fban : ban users from all federations you have control over. + - /unfban : unbans users from all federations you have control over. + - /setfrules: set the rules for the federation. + - /frules: get the rules of the federation. + - /chatfed: shows the federation the chat is linked to. + - /fedadmins: gives list of federation admins. + - /fbanlist: gives the list of currently fedbanned users. + - /fedchats: get all the chats linked to the federation. + - /importfbans: Reply to the federation backup message file to import the banned list to the federation. + - /myfeds: To know Your fed +""" + +NEW_FED_HANDLER = CommandHandler("newfed", new_fed) +DEL_FED_HANDLER = CommandHandler("delfed", del_fed, pass_args=True) +JOIN_FED_HANDLER = CommandHandler("joinfed", join_fed, pass_args=True) +LEAVE_FED_HANDLER = CommandHandler("leavefed", leave_fed, pass_args=True) +PROMOTE_FED_HANDLER = CommandHandler("fpromote", user_join_fed, pass_args=True) +DEMOTE_FED_HANDLER = CommandHandler("fdemote", user_demote_fed, pass_args=True) +INFO_FED_HANDLER = CommandHandler("fedinfo", fed_info, pass_args=True) +BAN_FED_HANDLER = DisableAbleCommandHandler(["fban", "fedban"], fed_ban, pass_args=True) +UN_BAN_FED_HANDLER = CommandHandler("unfban", unfban, pass_args=True) +FED_BROADCAST_HANDLER = CommandHandler("fbroadcast", fed_broadcast, pass_args=True) +FED_SET_RULES_HANDLER = CommandHandler("setfrules", set_frules, pass_args=True) +FED_GET_RULES_HANDLER = CommandHandler("frules", get_frules, pass_args=True) +FED_CHAT_HANDLER = CommandHandler("chatfed", fed_chat, pass_args=True) +FED_ADMIN_HANDLER = CommandHandler("fedadmins", fed_admin, pass_args=True) +FED_USERBAN_HANDLER = CommandHandler("fbanlist", fed_ban_list, pass_args=True, pass_chat_data=True) +FED_NOTIF_HANDLER = CommandHandler("fednotif", fed_notif, pass_args=True) +FED_CHATLIST_HANDLER = CommandHandler("fedchats", fed_chats, pass_args=True) +FED_IMPORTBAN_HANDLER = CommandHandler("importfbans", fed_import_bans, pass_chat_data=True) +MY_FEDS_LIST = CommandHandler("myfeds", get_myfeds_list) + +DELETEBTN_FED_HANDLER = CallbackQueryHandler(del_fed_button, pattern=r"rmfed_") + +dispatcher.add_handler(NEW_FED_HANDLER) +dispatcher.add_handler(DEL_FED_HANDLER) +dispatcher.add_handler(JOIN_FED_HANDLER) +dispatcher.add_handler(LEAVE_FED_HANDLER) +dispatcher.add_handler(PROMOTE_FED_HANDLER) +dispatcher.add_handler(DEMOTE_FED_HANDLER) +dispatcher.add_handler(INFO_FED_HANDLER) +dispatcher.add_handler(BAN_FED_HANDLER) +dispatcher.add_handler(UN_BAN_FED_HANDLER) +dispatcher.add_handler(FED_BROADCAST_HANDLER) +dispatcher.add_handler(FED_SET_RULES_HANDLER) +dispatcher.add_handler(FED_GET_RULES_HANDLER) +dispatcher.add_handler(FED_CHAT_HANDLER) +dispatcher.add_handler(FED_ADMIN_HANDLER) +dispatcher.add_handler(FED_USERBAN_HANDLER) +dispatcher.add_handler(FED_NOTIF_HANDLER) +dispatcher.add_handler(FED_CHATLIST_HANDLER) +dispatcher.add_handler(FED_IMPORTBAN_HANDLER) +dispatcher.add_handler(MY_FEDS_LIST) + +dispatcher.add_handler(DELETEBTN_FED_HANDLER) diff --git a/fire_bot/modules/fun.py b/fire_bot/modules/fun.py new file mode 100644 index 0000000..49d9722 --- /dev/null +++ b/fire_bot/modules/fun.py @@ -0,0 +1,157 @@ +import html +import random +import time +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.ext import run_async + +import tg_bot.modules.fun_strings as fun_strings +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import is_user_admin +from tg_bot.modules.helper_funcs.extraction import extract_user + + +@run_async +def runs(bot: Bot, update: Update): + update.effective_message.reply_text(random.choice(fun_strings.RUN_STRINGS)) + + +@run_async +def slap(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + chat = update.effective_chat + + reply_text = message.reply_to_message.reply_text if message.reply_to_message else message.reply_text + + curr_user = html.escape(message.from_user.first_name) + user_id = extract_user(message, args) + + if user_id == bot.id: + temp = random.choice(fun_strings.SLAP_SAITAMA_TEMPLATES) + + if isinstance(temp, list): + if temp[2] == "tmute": + if is_user_admin(chat, message.from_user.id): + reply_text(temp[1]) + return + + mutetime = int(time.time() + 60) + bot.restrict_chat_member(chat.id, message.from_user.id, until_date=mutetime, can_send_messages=False) + reply_text(temp[0]) + else: + reply_text(temp) + return + + if user_id: + + slapped_user = bot.get_chat(user_id) + user1 = curr_user + user2 = html.escape(slapped_user.first_name) + + else: + user1 = bot.first_name + user2 = curr_user + + temp = random.choice(fun_strings.SLAP_TEMPLATES) + item = random.choice(fun_strings.ITEMS) + hit = random.choice(fun_strings.HIT) + throw = random.choice(fun_strings.THROW) + + reply = temp.format(user1=user1, user2=user2, item=item, hits=hit, throws=throw) + + reply_text(reply, parse_mode=ParseMode.HTML) + + +@run_async +def roll(bot: Bot, update: Update): + update.message.reply_text(random.choice(range(1, 7))) + + +@run_async +def toss(bot: Bot, update: Update): + update.message.reply_text(random.choice(fun_strings.TOSS)) + + +@run_async +def abuse(bot: Bot, update: Update): + msg = update.effective_message + reply_text = msg.reply_to_message.reply_text if msg.reply_to_message else msg.reply_text + reply_text(random.choice(fun_strings.ABUSE_STRINGS)) + + +@run_async +def shrug(bot: Bot, update: Update): + msg = update.effective_message + reply_text = msg.reply_to_message.reply_text if msg.reply_to_message else msg.reply_text + reply_text(r"¯\_(ツ)_/¯") + + +@run_async +def bluetext(bot: Bot, update: Update): + msg = update.effective_message + reply_text = msg.reply_to_message.reply_text if msg.reply_to_message else msg.reply_text + reply_text("/BLUE /TEXT\n/MUST /CLICK\n/I /AM /A /STUPID /ANIMAL /THAT /IS /ATTRACTED /TO /COLORS") + + +@run_async +def rlg(bot: Bot, update: Update): + eyes = random.choice(fun_strings.EYES) + mouth = random.choice(fun_strings.MOUTHS) + ears = random.choice(fun_strings.EARS) + + if len(eyes) == 2: + repl = ears[0] + eyes[0] + mouth[0] + eyes[1] + ears[1] + else: + repl = ears[0] + eyes[0] + mouth[0] + eyes[0] + ears[1] + update.message.reply_text(repl) + + +@run_async +def decide(bot: Bot, update: Update): + reply_text = update.effective_message.reply_to_message.reply_text if update.effective_message.reply_to_message else update.effective_message.reply_text + reply_text(random.choice(fun_strings.DECIDE)) + +@run_async +def table(bot: Bot, update: Update): + reply_text = update.effective_message.reply_to_message.reply_text if update.effective_message.reply_to_message else update.effective_message.reply_text + reply_text(random.choice(fun_strings.TABLE)) + + +__help__ = """ + - /runs: reply a random string from an array of replies. + - /slap: slap a user, or get slapped if not a reply. + - /shrug : get shrug XD. + - /table : get flip/unflip :v. + - /decide : Randomly answers yes/no/maybe + - /toss : Tosses A coin + - /bluetext : check urself :V + - /roll : Roll a dice. + - /rlg : Join ears,nose,mouth and create an emo ;-; +""" + +RUNS_HANDLER = DisableAbleCommandHandler("runs", runs) +SLAP_HANDLER = DisableAbleCommandHandler("slap", slap, pass_args=True) +ROLL_HANDLER = DisableAbleCommandHandler("roll", roll) +TOSS_HANDLER = DisableAbleCommandHandler("toss", toss) +SHRUG_HANDLER = DisableAbleCommandHandler("shrug", shrug) +BLUETEXT_HANDLER = DisableAbleCommandHandler("bluetext", bluetext) +RLG_HANDLER = DisableAbleCommandHandler("rlg", rlg) +DECIDE_HANDLER = DisableAbleCommandHandler("decide", decide) +TABLE_HANDLER = DisableAbleCommandHandler("table", table) + +dispatcher.add_handler(RUNS_HANDLER) +dispatcher.add_handler(SLAP_HANDLER) +dispatcher.add_handler(ROLL_HANDLER) +dispatcher.add_handler(TOSS_HANDLER) +dispatcher.add_handler(SHRUG_HANDLER) +dispatcher.add_handler(BLUETEXT_HANDLER) +dispatcher.add_handler(RLG_HANDLER) +dispatcher.add_handler(DECIDE_HANDLER) +dispatcher.add_handler(TABLE_HANDLER) + +__mod_name__ = "FUN" +__command_list__ = ["runs", "slap", "roll", "toss", "shrug", "bluetext", "rlg", "decide", "table"] +__handlers__ = [RUNS_HANDLER, SLAP_HANDLER, ROLL_HANDLER, TOSS_HANDLER, SHRUG_HANDLER, BLUETEXT_HANDLER, RLG_HANDLER, + DECIDE_HANDLER, TABLE_HANDLER] diff --git a/fire_bot/modules/fun_strings.py b/fire_bot/modules/fun_strings.py new file mode 100644 index 0000000..ebd477e --- /dev/null +++ b/fire_bot/modules/fun_strings.py @@ -0,0 +1,425 @@ +RUN_STRINGS = ( + "Where do you think you're going?", + "Huh? what? did they get away?", + "ZZzzZZzz... Huh? what? oh, just them again, nevermind.", + "Get back here!", + "Not so fast...", + "Look out for the wall!", + "Don't leave me alone with them!!", + "You run, you die.", + "Jokes on you, I'm everywhere", + "You're gonna regret that...", + "You could also try /punchme, I hear that's fun.", + "Go bother someone else, no-one here cares.", + "You can run, but you can't hide.", + "Is that all you've got?", + "I'm behind you...", + "You've got company!", + "We can do this the easy way, or the hard way.", + "You just don't get it, do you?", + "Yeah, you better run!", + "Please, remind me how much I care?", + "I'd run faster if I were you.", + "That's definitely the droid we're looking for.", + "May the odds be ever in your favour.", + "Famous last words.", + "And they disappeared forever, never to be seen again.", + "\"Oh, look at me! I'm so cool, I can run from a bot!\" - this person", + "Yeah yeah, just tap /punchme already.", + "Here, take this ring and head to Mordor while you're at it.", + "Legend has it, they're still running...", + "Unlike Harry Potter, your parents can't protect you from me.", + "Fear leads to anger. Anger leads to hate. Hate leads to suffering. If you keep running in fear, you might " + "be the next Vader.", + "Multiple calculations later, I have decided my interest in your shenanigans is exactly 0.", + "Legend has it, they're still running.", + "Keep it up, not sure we want you here anyway.", + "You're a wiza- Oh. Wait. You're not Harry, keep moving.", + "NO RUNNING IN THE HALLWAYS!", + "Hasta la vista, baby.", + "Who let the dogs out?", + "It's funny, because no one cares.", + "Ah, what a waste. I liked that one.", + "Frankly, my dear, I don't give a damn.", + "My milkshake brings all the boys to yard... So run faster!", + "You can't HANDLE the truth!", + "A long time ago, in a galaxy far far away... Someone would've cared about that. Not anymore though.", + "Hey, look at them! They're running from the inevitable banhammer... Cute.", + "Han shot first. So will I.", + "What are you running after, a white rabbit?", + "As The Doctor would say... RUN!", +) + +SLAP_SAITAMA_TEMPLATES = ( + "Slap me one more time and I'll mute you.", + "Stop slapping me. 😡.", + [ + "I am muting you for a minute.", # normal reply + "Stop slapping me just because I can't mute you. 🤧.", # reply to admin + "tmute" # command + ] +) + + +SLAP_TEMPLATES = ( + "{user2} was shot by {user1}.", + "{user2} walked into a cactus while trying to escape {user1}.", + "{user2} drowned whilst trying to escape {user1}.", + "{user2} fell into a patch of cacti.", + "{user2} went up in flames.", + "{user2} burned to death.", + "{user2} was burnt to a crisp whilst fighting {user1}.", + "{user2} was struck by lightning.", + "{user2} was slain by {user1}.", + "{user2} was killed by magic.", + "{user2} starved to death.", + "{user2} fell out of the world.", + "{user2} was knocked into the void by {user1}.", + "{user2}'s bones are scraped clean by the desolate wind.", + "{user2} fainted.", + "{user2} is out of usable Pokemon! {user2} whited out!.", + "{user2} is out of usable Pokemon! {user2} blacked out!.", + "{user2} says goodbye to this cruel world.", + "{user2} got rekt.", + "{user2} was sawn in half by {user1}.", + "{user2}'s melon was split by {user1}.", + "{user2} was sliced and diced by {user1}.", + "{user2}'s death put another notch in {user1}'s axe.", + "{user2} died from {user1}'s mysterious tropical disease.", + "{user2} played hot-potato with a grenade.", + "{user2} was knifed by {user1}.", + "{user2} ate a grenade.", + "{user2} is what's for dinner!", + "{user2} was terminated by {user1}.", + "{user2} was shot before being thrown out of a plane.", + "{user2} has encountered an error.", + "{user2} died and reincarnated as a goat.", + "{user1} threw {user2} off a building.", + "{user2} got a premature burial.", + "{user1} spammed {user2}'s email.", + "{user1} hit {user2} with a small, interstellar spaceship.", + "{user1} put {user2} in check-mate.", + "{user1} RSA-encrypted {user2} and deleted the private key.", + "{user1} put {user2} in the friendzone.", + "{user1} slaps {user2} with a DMCA takedown request!", + "{user2} died of hospital gangrene.", + "{user2} got a house call from Doctor {user1}.", + "{user1} beheaded {user2}.", + "{user2} got stoned...by an angry mob.", + "{user1} sued the pants off {user2}.", + "{user2} was one-hit KO'd by {user1}.", + "{user1} sent {user2} down the memory hole.", + "{user2} was a mistake. - '{user1}' ", + "{user1} checkmated {user2} in two moves.", + "{user2} was made redundant.", + "{user1} {hits} {user2} with a bat!.", + "{user1} {hits} {user2} with a Taijutsu Kick!.", + "{user1} {hits} {user2} with X-Gloves!.", + "{user1} {hits} {user2} with a Jet Punch!.", + "{user1} {hits} {user2} with a Jet Pistol!.", + "{user1} {hits} {user2} with a United States of Smash!.", + "{user1} {hits} {user2} with a Detroit Smash!.", + "{user1} {hits} {user2} with a Texas Smash!.", + "{user1} {hits} {user2} with a California Smash!.", + "{user1} {hits} {user2} with a New Hampshire Smash!.", + "{user1} {hits} {user2} with a Missouri Smash!.", + "{user1} {hits} {user2} with a Carolina Smash!.", + "{user1} {hits} {user2} with a King Kong Gun!.", + "{user1} {hits} {user2} with a baseball bat - metal one.!.", + "*Serious punches {user2}*.", + "*Normal punches {user2}*.", + "*Consecutive Normal punches {user2}*.", + "*Two Handed Consecutive Normal Punches {user2}*.", + "*Ignores {user2} to let them die of embarassment*.", + "*points at {user2}* What's with this sassy... lost child?.", + "*Hits {user2} with a Fire Tornado*.", + "{user1} pokes {user2} in the eye !", + "{user1} pokes {user2} on the sides!", + "{user1} pokes {user2}!", + "{user1} pokes {user2} with a needle!", + "{user1} pokes {user2} with a pen!", + "{user1} pokes {user2} with a stun gun!", + "{user2} is secretly a Furry!.", + "Hey Everybody! {user1} is asking me to be mean!", + "( ・_・)ノ⌒●~* (・.・;) <-{user2}", + "Take this {user2}\n(ノ゚Д゚)ノ ))))●~* ", + "Here {user2} hold this\n(`・ω・)つ ●~*", + "( ・_・)ノΞ●~* {user2},Shinaeeeee!!.", + "( ・∀・)r鹵~<≪巛;゚Д゚)ノ\n*Bug sprays {user2}*.", + "( ゚Д゚)ノ占~<巛巛巛.\n-{user2} You pest!", + "( う-´)づ︻╦̵̵̿╤── \(˚☐˚”)/ {user2}.", + "{user1} {hits} {user2} with a {item}.", + "{user1} {hits} {user2} in the face with a {item}.", + "{user1} {hits} {user2} around a bit with a {item}.", + "{user1} {throws} a {item} at {user2}.", + "{user1} grabs a {item} and {throws} it at {user2}'s face.", + "{user1} launches a {item} in {user2}'s general direction.", + "{user1} starts slapping {user2} silly with a {item}.", + "{user1} pins {user2} down and repeatedly {hits} them with a {item}.", + "{user1} grabs up a {item} and {hits} {user2} with it.", + "{user1} ties {user2} to a chair and {throws} a {item} at them.", + "{user1} gave a friendly push to help {user2} learn to swim in lava.", + "{user1} bullied {user2}.", + "Nyaan ate {user2}'s leg. *nomnomnom*", + "{user1} {throws} a master ball at {user2}, resistance is futile.", + "{user1} hits {user2} with an action beam...bbbbbb (ง・ω・)ง ====*", + "{user1} ara ara's {user2}.", + "{user1} ora ora's {user2}.", + "{user1} muda muda's {user2}.", + "{user2} was turned into a Jojo reference!", + "{user1} hits {user2} with {item}.", +) + +PING_STRING = ( + "PONG!!", + "I am here!", +) + +ITEMS = ( + "cast iron skillet", + "angry meow", + "cricket bat", + "wooden cane", + "shovel", + "toaster", + "book", + "laptop", + "rubber chicken", + "spiked bat", + "heavy rock", + "chunk of dirt", + "ton of bricks", + "rasengan", + "spirit bomb", + "100-Type Guanyin Bodhisattva", + "rasenshuriken", + "Murasame", + "ban", + "chunchunmaru", + "Kubikiribōchō", + "rasengan", + "spherical flying kat", +) + +THROW = ( + "എറിഞ്ഞു", + "വിക്ഷേപിച്ചു", + "തട്ടി", + "വീശിയെറിഞ്ഞു", +) + +HIT = ( + "അടിച്ചു", + "ശക്തിയായി പ്രഹരിച്ചു", + "തല്ലി", + "ഇടിച്ചു", + "തൊഴിച്ചു", + +) + +ABUSE_STRINGS = ( + "Fuck off", + "Stfu go fuck yourself", + "Ur mum gey", + "Ur dad lesbo", + "Bsdk", + "Nigga", + "Ur granny tranny", + "you noob", + "Relax your Rear,ders nothing to fear,The Rape train is finally here", + "Stfu bc", + "Stfu and Gtfo U nub", + "GTFO bsdk", + "CUnt", + " Gay is here", + "Ur dad gey bc ", +) + +EYES = [ + ['⌐■', '■'], + [' ͠°', ' °'], + ['⇀', '↼'], + ['´• ', ' •`'], + ['´', '`'], + ['`', '´'], + ['ó', 'ò'], + ['ò', 'ó'], + ['⸌', '⸍'], + ['>', '<'], + ['Ƹ̵̡', 'Ʒ'], + ['ᗒ', 'ᗕ'], + ['⟃', '⟄'], + ['⪧', '⪦'], + ['⪦', '⪧'], + ['⪩', '⪨'], + ['⪨', '⪩'], + ['⪰', '⪯'], + ['⫑', '⫒'], + ['⨴', '⨵'], + ['⩿', '⪀'], + ['⩾', '⩽'], + ['⩺', '⩹'], + ['⩹', '⩺'], + ['◥▶', '◀◤'], + ['◍', '◎'], + ['/͠-', '┐͡-\\'], + ['⌣', '⌣”'], + [' ͡⎚', ' ͡⎚'], + ['≋'], + ['૦ઁ'], + [' ͯ'], + [' ͌'], + ['ළ'], + ['◉'], + ['☉'], + ['・'], + ['▰'], + ['ᵔ'], + [' ゚'], + ['□'], + ['☼'], + ['*'], + ['`'], + ['⚆'], + ['⊜'], + ['>'], + ['❍'], + [' ̄'], + ['─'], + ['✿'], + ['•'], + ['T'], + ['^'], + ['ⱺ'], + ['@'], + ['ȍ'], + ['  '], + ['  '], + ['x'], + ['-'], + ['$'], + ['Ȍ'], + ['ʘ'], + ['Ꝋ'], + [''], + ['⸟'], + ['๏'], + ['ⴲ'], + ['◕'], + ['◔'], + ['✧'], + ['■'], + ['♥'], + [' ͡°'], + ['¬'], + [' º '], + ['⨶'], + ['⨱'], + ['⏓'], + ['⏒'], + ['⍜'], + ['⍤'], + ['ᚖ'], + ['ᴗ'], + ['ಠ'], + ['σ'], + ['☯'] +] + +MOUTHS = [ + ['v'], + ['ᴥ'], + ['ᗝ'], + ['Ѡ'], + ['ᗜ'], + ['Ꮂ'], + ['ᨓ'], + ['ᨎ'], + ['ヮ'], + ['╭͜ʖ╮'], + [' ͟ل͜'], + [' ͜ʖ'], + [' ͟ʖ'], + [' ʖ̯'], + ['ω'], + [' ³'], + [' ε '], + ['﹏'], + ['□'], + ['ل͜'], + ['‿'], + ['╭╮'], + ['‿‿'], + ['▾'], + ['‸'], + ['Д'], + ['∀'], + ['!'], + ['人'], + ['.'], + ['ロ'], + ['_'], + ['෴'], + ['ѽ'], + ['ഌ'], + ['⏠'], + ['⏏'], + ['⍊'], + ['⍘'], + ['ツ'], + ['益'], + ['╭∩╮'], + ['Ĺ̯'], + ['◡'], + [' ͜つ'] +] + +EARS = [ + ['q', 'p'], + ['ʢ', 'ʡ'], + ['⸮', '?'], + ['ʕ', 'ʔ'], + ['ᖗ', 'ᖘ'], + ['ᕦ', 'ᕥ'], + ['ᕦ(', ')ᕥ'], + ['ᕙ(', ')ᕗ'], + ['ᘳ', 'ᘰ'], + ['ᕮ', 'ᕭ'], + ['ᕳ', 'ᕲ'], + ['(', ')'], + ['[', ']'], + ['¯\\_', '_/¯'], + ['୧', '୨'], + ['୨', '୧'], + ['⤜(', ')⤏'], + ['☞', '☞'], + ['ᑫ', 'ᑷ'], + ['ᑴ', 'ᑷ'], + ['ヽ(', ')ノ'], + ['\\(', ')/'], + ['乁(', ')ㄏ'], + ['└[', ']┘'], + ['(づ', ')づ'], + ['(ง', ')ง'], + ['⎝', '⎠'], + ['ლ(', 'ლ)'], + ['ᕕ(', ')ᕗ'], + ['(∩', ')⊃━☆゚.*'], +] + +TOSS = ( + "Heads", + "Tails", +) + +DECIDE = ( + "Yes.", + "NoU.", + "Maybe." +) + +TABLE = ( + "(╯°□°)╯彡 ┻━┻", + "I ran out of tables, will order more.", + "Go do some work instead of flippin tables." +) diff --git a/fire_bot/modules/gettime.py b/fire_bot/modules/gettime.py new file mode 100644 index 0000000..d6b1621 --- /dev/null +++ b/fire_bot/modules/gettime.py @@ -0,0 +1,91 @@ +import datetime +from typing import List + +import requests +from telegram import Bot, Update, ParseMode +from telegram.ext import run_async + +from tg_bot import dispatcher, TIME_API_KEY +from tg_bot.modules.disable import DisableAbleCommandHandler + + +def generate_time(to_find: str, findtype: List[str]) -> str: + data = requests.get(f"http://api.timezonedb.com/v2.1/list-time-zone" + f"?key={TIME_API_KEY}" + f"&format=json" + f"&fields=countryCode,countryName,zoneName,gmtOffset,timestamp,dst").json() + + for zone in data["zones"]: + for eachtype in findtype: + if to_find in zone[eachtype].lower(): + country_name = zone['countryName'] + country_zone = zone['zoneName'] + country_code = zone['countryCode'] + + if zone['dst'] == 1: + daylight_saving = "Yes" + else: + daylight_saving = "No" + + date_fmt = r"%d-%m-%Y" + time_fmt = r"%H:%M:%S" + day_fmt = r"%A" + gmt_offset = zone['gmtOffset'] + timestamp = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=gmt_offset) + current_date = timestamp.strftime(date_fmt) + current_time = timestamp.strftime(time_fmt) + current_day = timestamp.strftime(day_fmt) + + break + + try: + result = (f"🌍Country : {country_name}\n" + f"⏳Zone Name : {country_zone}\n" + f"🗺Country Code : {country_code}\n" + f"🌞Daylight saving : {daylight_saving}\n" + f"🌅Day : {current_day}\n" + f"⌚Current Time : {current_time}\n" + f"📆Current Date : {current_date}") + except: + result = None + + return result + + +@run_async +def gettime(bot: Bot, update: Update): + message = update.effective_message + + try: + query = message.text.strip().split(" ", 1)[1] + except: + message.reply_text("Provide a country name/abbreviation/timezone to find.") + return + send_message = message.reply_text(f"Finding timezone info for {query}", parse_mode=ParseMode.HTML) + + query_timezone = query.lower() + if len(query_timezone) == 2: + result = generate_time(query_timezone, ["countryCode"]) + else: + result = generate_time(query_timezone, ["zoneName", "countryName"]) + + if not result: + send_message.edit_text(f"Timezone info not available for {query}", parse_mode=ParseMode.HTML) + return + + send_message.edit_text(result, parse_mode=ParseMode.HTML) + + +__help__ = """ + - /time : Gives information about a timezone. + +Available queries : Country Code/Country Name/Timezone Name +""" + +TIME_HANDLER = DisableAbleCommandHandler("time", gettime) + +dispatcher.add_handler(TIME_HANDLER) + +__mod_name__ = "TIME" +__command_list__ = ["time"] +__handlers__ = [TIME_HANDLER] diff --git a/fire_bot/modules/github.py b/fire_bot/modules/github.py new file mode 100644 index 0000000..3b0df66 --- /dev/null +++ b/fire_bot/modules/github.py @@ -0,0 +1,46 @@ +from telegram import ParseMode, Update, Bot +from telegram.ext import run_async + +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot import dispatcher + +from requests import get + + +@run_async +def github(bot: Bot, update: Update): + message = update.effective_message + text = message.text[len('/git '):] + usr = get(f'https://api.github.com/users/{text}').json() + if usr.get('login'): + reply_text = f"""*Name:* `{usr['name']}` +*Username:* `{usr['login']}` +*Account ID:* `{usr['id']}` +*Account type:* `{usr['type']}` +*Location:* `{usr['location']}` +*Bio:* `{usr['bio']}` +*Followers:* `{usr['followers']}` +*Following:* `{usr['following']}` +*Hireable:* `{usr['hireable']}` +*Public Repos:* `{usr['public_repos']}` +*Public Gists:* `{usr['public_gists']}` +*Email:* `{usr['email']}` +*Company:* `{usr['company']}` +*Website:* `{usr['blog']}` +*Last updated:* `{usr['updated_at']}` +*Account created at:* `{usr['created_at']}` +""" + else: + reply_text = "User not found. Make sure you entered valid username!" + message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN) + + +__help__ = """ + - /git:{GitHub username} Returns info about a GitHub user or organization. +""" + +__mod_name__ = "GITHUB" + +github_handle = DisableAbleCommandHandler("git", github) + +dispatcher.add_handler(github_handle) diff --git a/fire_bot/modules/global_bans.py b/fire_bot/modules/global_bans.py new file mode 100644 index 0000000..a6f1000 --- /dev/null +++ b/fire_bot/modules/global_bans.py @@ -0,0 +1,328 @@ +import html +from io import BytesIO +from typing import Optional, List + +from telegram import Message, Update, Bot, User, Chat, ParseMode +from telegram.error import BadRequest, TelegramError +from telegram.ext import run_async, CommandHandler, MessageHandler, Filters +from telegram.utils.helpers import mention_html + +import tg_bot.modules.sql.global_bans_sql as sql +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS, STRICT_GBAN +from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_admin +from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.helper_funcs.misc import send_to_list +from tg_bot.modules.sql.users_sql import get_all_chats + +GBAN_ENFORCE_GROUP = 6 + +GBAN_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Peer_id_invalid", + "Group chat was deactivated", + "Need to be inviter of a user to kick it from a basic group", + "Chat_admin_required", + "Only the creator of a basic group can kick group administrators", + "Channel_private", + "Not in the chat" +} + +UNGBAN_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Method is available for supergroup and channel chats only", + "Not in the chat", + "Channel_private", + "Chat_admin_required", +} + + +@run_async +def gban(bot: Bot, update: Update, args: List[str]): + message = update.effective_message # type: Optional[Message] + + user_id, reason = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + + if int(user_id) in SUDO_USERS: + message.reply_text("I spy, with my little eye... a sudo user war! Why are you guys turning on each other?") + return + + if int(user_id) in SUPPORT_USERS: + message.reply_text("OOOH someone's trying to gban a support user! *grabs popcorn*") + return + + if user_id == bot.id: + message.reply_text("-_- So funny, lets gban myself why don't I? Nice try.") + return + + try: + user_chat = bot.get_chat(user_id) + except BadRequest as excp: + message.reply_text(excp.message) + return + + if user_chat.type != 'private': + message.reply_text("That's not a user!") + return + + if sql.is_user_gbanned(user_id): + if not reason: + message.reply_text("This user is already gbanned; I'd change the reason, but you haven't given me one...") + return + + old_reason = sql.update_gban_reason(user_id, user_chat.username or user_chat.first_name, reason) + if old_reason: + message.reply_text("This user is already gbanned, for the following reason:\n" + "{}\n" + "I've gone and updated it with your new reason!".format(html.escape(old_reason)), + parse_mode=ParseMode.HTML) + else: + message.reply_text("This user is already gbanned, but had no reason set; I've gone and updated it!") + + return + + message.reply_text("⚡️ *Snaps the Banhammer* ⚡️") + + banner = update.effective_user # type: Optional[User] + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "Global Ban" \ + "\n#GBAN" \ + "\nStatus: Enforcing" \ + "\nSudo Admin: {}" \ + "\nUser: {}" \ + "\nID: {}" \ + "\nReason: {}".format(mention_html(banner.id, banner.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id, reason or "No reason given"), + html=True) + + sql.gban_user(user_id, user_chat.username or user_chat.first_name, reason) + + chats = get_all_chats() + for chat in chats: + chat_id = chat.chat_id + + # Check if this group has disabled gbans + if not sql.does_chat_gban(chat_id): + continue + + try: + bot.kick_chat_member(chat_id, user_id) + except BadRequest as excp: + if excp.message in GBAN_ERRORS: + pass + else: + message.reply_text("Could not gban due to: {}".format(excp.message)) + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, "Could not gban due to: {}".format(excp.message)) + sql.ungban_user(user_id) + return + except TelegramError: + pass + + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "{} has been successfully gbanned!".format(mention_html(user_chat.id, user_chat.first_name)), + html=True) + message.reply_text("Person has been gbanned.") + + +@run_async +def ungban(bot: Bot, update: Update, args: List[str]): + message = update.effective_message # type: Optional[Message] + + user_id = extract_user(message, args) + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + + user_chat = bot.get_chat(user_id) + if user_chat.type != 'private': + message.reply_text("That's not a user!") + return + + if not sql.is_user_gbanned(user_id): + message.reply_text("This user is not gbanned!") + return + + banner = update.effective_user # type: Optional[User] + + message.reply_text("I pardon {}, globally with a second chance.".format(user_chat.first_name)) + + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "Regression of Global Ban" \ + "\n#UNGBAN" \ + "\nStatus: Ceased" \ + "\nSudo Admin: {}" \ + "\nUser: {}" \ + "\nID: {}".format(mention_html(banner.id, banner.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id), + html=True) + + chats = get_all_chats() + for chat in chats: + chat_id = chat.chat_id + + # Check if this group has disabled gbans + if not sql.does_chat_gban(chat_id): + continue + + try: + member = bot.get_chat_member(chat_id, user_id) + if member.status == 'kicked': + bot.unban_chat_member(chat_id, user_id) + + except BadRequest as excp: + if excp.message in UNGBAN_ERRORS: + pass + else: + message.reply_text("Could not un-gban due to: {}".format(excp.message)) + bot.send_message(OWNER_ID, "Could not un-gban due to: {}".format(excp.message)) + return + except TelegramError: + pass + + sql.ungban_user(user_id) + + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "{} has been pardoned from gban!".format(mention_html(user_chat.id, + user_chat.first_name)), + html=True) + + message.reply_text("This person has been un-gbanned and pardon is granted!") + + +@run_async +def gbanlist(bot: Bot, update: Update): + banned_users = sql.get_gban_list() + + if not banned_users: + update.effective_message.reply_text("There aren't any gbanned users! You're kinder than I expected...") + return + + banfile = 'Screw these guys.\n' + for user in banned_users: + banfile += "[x] {} - {}\n".format(user["name"], user["user_id"]) + if user["reason"]: + banfile += "Reason: {}\n".format(user["reason"]) + + with BytesIO(str.encode(banfile)) as output: + output.name = "gbanlist.txt" + update.effective_message.reply_document(document=output, filename="gbanlist.txt", + caption="Here is the list of currently gbanned users.") + + +def check_and_ban(update, user_id, should_message=True): + if sql.is_user_gbanned(user_id): + update.effective_chat.kick_member(user_id) + if should_message: + update.effective_message.reply_text("This is a bad person, they shouldn't be here!") + + +@run_async +def enforce_gban(bot: Bot, update: Update): + # Not using @restrict handler to avoid spamming - just ignore if cant gban. + if sql.does_chat_gban(update.effective_chat.id) and update.effective_chat.get_member(bot.id).can_restrict_members: + user = update.effective_user # type: Optional[User] + chat = update.effective_chat # type: Optional[Chat] + msg = update.effective_message # type: Optional[Message] + + if user and not is_user_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.reply_to_message: + user = msg.reply_to_message.from_user # type: Optional[User] + if user and not is_user_admin(chat, user.id): + check_and_ban(update, user.id, should_message=False) + + +@run_async +@user_admin +def gbanstat(bot: Bot, update: Update, args: List[str]): + if len(args) > 0: + if args[0].lower() in ["on", "yes"]: + sql.enable_gbans(update.effective_chat.id) + update.effective_message.reply_text("I've enabled gbans in this group. This will help protect you " + "from spammers, unsavoury characters, and the biggest trolls.") + elif args[0].lower() in ["off", "no"]: + sql.disable_gbans(update.effective_chat.id) + update.effective_message.reply_text("I've disabled gbans in this group. GBans wont affect your users " + "anymore. You'll be less protected from any trolls and spammers " + "though!") + else: + update.effective_message.reply_text("Give me some arguments to choose a setting! on/off, yes/no!\n\n" + "Your current setting is: {}\n" + "When True, any gbans that happen will also happen in your group. " + "When False, they won't, leaving you at the possible mercy of " + "spammers.".format(sql.does_chat_gban(update.effective_chat.id))) + + +def __stats__(): + return "{} gbanned users.".format(sql.num_gbanned_users()) + + +def __user_info__(user_id): + is_gbanned = sql.is_user_gbanned(user_id) + + text = "Globally banned: {}" + if is_gbanned: + text = text.format("Yes") + user = sql.get_gbanned_user(user_id) + if user.reason: + text += "\nReason: {}".format(html.escape(user.reason)) + else: + text = text.format("No") + return text + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + return "This chat is enforcing *gbans*: `{}`.".format(sql.does_chat_gban(chat_id)) + + +__help__ = """ +*Admin only:* + - /gbanstat : Will disable the effect of global bans on your group, or return your current settings. + +Gbans, also known as global bans, are used by the bot owners to ban spammers across all groups. This helps protect \ +you and your groups by removing spam flooders as quickly as possible. They can be disabled for you group by calling \ +/gbanstat +""" + +__mod_name__ = "GLOBAL BAN" + +GBAN_HANDLER = CommandHandler("gban", gban, pass_args=True, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) +UNGBAN_HANDLER = CommandHandler("ungban", ungban, pass_args=True, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) +GBAN_LIST = CommandHandler("gbanlist", gbanlist, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) + +GBAN_STATUS = CommandHandler("gbanstat", gbanstat, pass_args=True, filters=Filters.group) + +GBAN_ENFORCER = MessageHandler(Filters.all & Filters.group, enforce_gban) + +dispatcher.add_handler(GBAN_HANDLER) +dispatcher.add_handler(UNGBAN_HANDLER) +dispatcher.add_handler(GBAN_LIST) +dispatcher.add_handler(GBAN_STATUS) + +if STRICT_GBAN: # enforce GBANS if this is set + dispatcher.add_handler(GBAN_ENFORCER, GBAN_ENFORCE_GROUP) diff --git a/fire_bot/modules/global_kick.py b/fire_bot/modules/global_kick.py new file mode 100644 index 0000000..f196029 --- /dev/null +++ b/fire_bot/modules/global_kick.py @@ -0,0 +1,173 @@ +import html +from telegram import Message, Update, Bot, User, Chat, ParseMode +from typing import List, Optional +from telegram.error import BadRequest, TelegramError +from telegram.ext import run_async, CommandHandler, MessageHandler, Filters +from telegram.utils.helpers import mention_html +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS +from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_admin +from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.helper_funcs.misc import send_to_list +from tg_bot.modules.sql.users_sql import get_all_chats +import tg_bot.modules.sql.global_kicks_sql as sql + +GKICK_ERRORS = { + "Bots can't add new chat members", + "Channel_private", + "Chat not found", + "Can't demote chat creator", + "Chat_admin_required", + "Group chat was deactivated", + "Method is available for supergroup and channel chats only", + "Method is available only for supergroups", + "Need to be inviter of a user to kick it from a basic group", + "Not enough rights to restrict/unrestrict chat member", + "Not in the chat", + "Only the creator of a basic group can kick group administrators", + "Peer_id_invalid", + "User is an administrator of the chat", + "User_not_participant", + "Reply message not found", + "User not found" +} + +@run_async +def gkick(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + user_id = extract_user(message, args) + try: + user_chat = bot.get_chat(user_id) + except BadRequest as excp: + if excp.message in GKICK_ERRORS: + pass + else: + message.reply_text("User cannot be Globally kicked because: {}".format(excp.message)) + return + except TelegramError: + pass + + if not user_id or int(user_id)==777000: + message.reply_text("You don't seem to be referring to a user.") + return + if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: + message.reply_text("OHHH! Someone's trying to gkick a sudo/support user! *Grabs popcorn*") + return + if int(user_id) == OWNER_ID: + message.reply_text("Wow! Someone's so noob that he want to gkick my owner! *Grabs Potato Chips*") + return + + if user_id == bot.id: + message.reply_text("Welp, I'm not gonna to gkick myself!") + return + + chats = get_all_chats() + banner = update.effective_user # type: Optional[User] + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "Global Kick" \ + "\n#GKICK" \ + "\nStatus: Enforcing" \ + "\nSudo Admin: {}" \ + "\nUser: {}" \ + "\nID: {}".format(mention_html(banner.id, banner.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id), + html=True) + message.reply_text("Globally kicking user @{}".format(user_chat.username)) + sql.gkick_user(user_id, user_chat.username, 1) + for chat in chats: + try: + member = bot.get_chat_member(chat.chat_id, user_id) + if member.can_send_messages is False: + bot.unban_chat_member(chat.chat_id, user_id) # Unban_member = kick (and not ban) + bot.restrict_chat_member(chat.chat_id, user_id, can_send_messages = False) + else: + bot.unban_chat_member(chat.chat_id, user_id) + except BadRequest as excp: + if excp.message in GKICK_ERRORS: + pass + else: + message.reply_text("User cannot be Globally kicked because: {}".format(excp.message)) + return + except TelegramError: + pass + +def __user_info__(user_id): + times = sql.get_times(user_id) + + if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: + text="Globally kicked: No (Immortal)" + else: + text = "Globally kicked: {}" + if times!=0: + text = text.format("Yes (Times: {})".format(times)) + else: + text = text.format("No") + return text + +@run_async +def gkickset(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + user_id, value = extract_user_and_text(message, args) + try: + user_chat = bot.get_chat(user_id) + except BadRequest as excp: + if excp.message in GKICK_ERRORS: + pass + else: + message.reply_text("GENERIC ERROR: {}".format(excp.message)) + except TelegramError: + pass + if not user_id: + message.reply_text("You do not seems to be referring to a user") + return + if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: + message.reply_text("SUDOER: Irrelevant") + return + if int(user_id) == OWNER_ID: + message.reply_text("OWNER: Irrelevant") + return + if user_id == bot.id: + message.reply_text("It's me") + return + + sql.gkick_setvalue(user_id, user_chat.username, int(value)) + return + +def gkickreset(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + user_id, value = extract_user_and_text(message, args) + try: + user_chat = bot.get_chat(user_id) + except BadRequest as excp: + if excp.message in GKICK_ERRORS: + pass + else: + message.reply_text("GENERIC ERROR: {}".format(excp.message)) + except TelegramError: + pass + if not user_id: + message.reply_text("You do not seems to be referring to a user") + return + if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: + message.reply_text("SUDOER: Irrelevant") + return + if int(user_id) == OWNER_ID: + message.reply_text("OWNER: Irrelevant") + return + if user_id == bot.id: + message.reply_text("It's me") + return + + sql.gkick_reset(user_id) + return + + +GKICK_HANDLER = CommandHandler("gkick", gkick, pass_args=True, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) +SET_HANDLER = CommandHandler("gkickset", gkickset, pass_args=True,filters=Filters.user(OWNER_ID)) +RESET_HANDLER = CommandHandler("gkickreset", gkickreset, pass_args=True,filters=Filters.user(OWNER_ID)) + +dispatcher.add_handler(GKICK_HANDLER) +dispatcher.add_handler(SET_HANDLER) +dispatcher.add_handler(RESET_HANDLER) diff --git a/fire_bot/modules/global_mutes.py b/fire_bot/modules/global_mutes.py new file mode 100644 index 0000000..b3730c0 --- /dev/null +++ b/fire_bot/modules/global_mutes.py @@ -0,0 +1,337 @@ +import html +from io import BytesIO +from typing import Optional, List + +from telegram import Message, Update, Bot, User, Chat +from telegram.error import BadRequest, TelegramError +from telegram.ext import run_async, CommandHandler, MessageHandler, Filters +from telegram.utils.helpers import mention_html + +import tg_bot.modules.sql.global_mutes_sql as sql +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS, STRICT_GMUTE +from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_admin +from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.helper_funcs.misc import send_to_list +from tg_bot.modules.sql.users_sql import get_all_chats + +GMUTE_ENFORCE_GROUP = 6 + + +@run_async +def gmute(bot: Bot, update: Update, args: List[str]): + message = update.effective_message # type: Optional[Message] + + user_id, reason = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + + if int(user_id) in SUDO_USERS: + message.reply_text("I spy, with my little eye... a sudo user war! Why are you guys turning on each other?") + return + + if int(user_id) in SUPPORT_USERS: + message.reply_text("OOOH someone's trying to gmute a support user! *grabs popcorn*") + return + + if user_id == bot.id: + message.reply_text("-_- So funny, lets gmute myself why don't I? Nice try.") + return + + try: + user_chat = bot.get_chat(user_id) + except BadRequest as excp: + message.reply_text(excp.message) + return + + if user_chat.type != 'private': + message.reply_text("That's not a user!") + return + + if sql.is_user_gmuted(user_id): + if not reason: + message.reply_text("This user is already gmuted; I'd change the reason, but you haven't given me one...") + return + + success = sql.update_gmute_reason(user_id, user_chat.username or user_chat.first_name, reason) + if success: + message.reply_text("This user is already gmuted; I've gone and updated the gmute reason though!") + else: + message.reply_text("Do you mind trying again? I thought this person was gmuted, but then they weren't? " + "Am very confused") + + return + + message.reply_text("*Gets duct tape ready* 😉") + + muter = update.effective_user # type: Optional[User] + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "Global Mute" \ + "\n#GMUTE" \ + "\nStatus: Enforcing" \ + "\nSudo Admin: {}" \ + "\nUser: {}" \ + "\nID: {}" \ + "\nReason: {}".format(mention_html(muter.id, muter.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id, reason or "No reason given"), + html=True) + + + sql.gmute_user(user_id, user_chat.username or user_chat.first_name, reason) + + chats = get_all_chats() + for chat in chats: + chat_id = chat.chat_id + + # Check if this group has disabled gmutes + if not sql.does_chat_gmute(chat_id): + continue + + try: + bot.restrict_chat_member(chat_id, user_id, can_send_messages=False) + except BadRequest as excp: + if excp.message == "User is an administrator of the chat": + pass + elif excp.message == "Chat not found": + pass + elif excp.message == "Not enough rights to restrict/unrestrict chat member": + pass + elif excp.message == "User_not_participant": + pass + elif excp.message == "Peer_id_invalid": # Suspect this happens when a group is suspended by telegram. + pass + elif excp.message == "Group chat was deactivated": + pass + elif excp.message == "Need to be inviter of a user to kick it from a basic group": + pass + elif excp.message == "Chat_admin_required": + pass + elif excp.message == "Only the creator of a basic group can kick group administrators": + pass + elif excp.message == "Method is available only for supergroups": + pass + elif excp.message == "Can't demote chat creator": + pass + else: + message.reply_text("Could not gmute due to: {}".format(excp.message)) + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, "Could not gmute due to: {}".format(excp.message)) + sql.ungmute_user(user_id) + return + except TelegramError: + pass + + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "{} has been successfully gmuted!".format(mention_html(user_chat.id, user_chat.first_name)), + html=True) + + message.reply_text("They won't be talking again anytime soon.") + + +@run_async +def ungmute(bot: Bot, update: Update, args: List[str]): + message = update.effective_message # type: Optional[Message] + + user_id = extract_user(message, args) + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + + user_chat = bot.get_chat(user_id) + if user_chat.type != 'private': + message.reply_text("That's not a user!") + return + + if not sql.is_user_gmuted(user_id): + message.reply_text("This user is not gmuted!") + return + + muter = update.effective_user # type: Optional[User] + + message.reply_text("I'll let {} speak again, globally.".format(user_chat.first_name)) + + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "Regression of Global Mute" \ + "\n#UNGMUTE" \ + "\nStatus: Ceased" \ + "\nSudo Admin: {}" \ + "\nUser: {}" \ + "\nID: {}".format(mention_html(muter.id, muter.first_name), + mention_html(user_chat.id, user_chat.first_name), + user_chat.id), + html=True) + + + chats = get_all_chats() + for chat in chats: + chat_id = chat.chat_id + + # Check if this group has disabled gmutes + if not sql.does_chat_gmute(chat_id): + continue + + try: + member = bot.get_chat_member(chat_id, user_id) + if member.status == 'restricted': + bot.restrict_chat_member(chat_id, int(user_id), + can_send_messages=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True) + + except BadRequest as excp: + if excp.message == "User is an administrator of the chat": + pass + elif excp.message == "Chat not found": + pass + elif excp.message == "Not enough rights to restrict/unrestrict chat member": + pass + elif excp.message == "User_not_participant": + pass + elif excp.message == "Method is available for supergroup and channel chats only": + pass + elif excp.message == "Not in the chat": + pass + elif excp.message == "Channel_private": + pass + elif excp.message == "Chat_admin_required": + pass + else: + message.reply_text("Could not un-gmute due to: {}".format(excp.message)) + bot.send_message(OWNER_ID, "Could not un-gmute due to: {}".format(excp.message)) + return + except TelegramError: + pass + + sql.ungmute_user(user_id) + + send_to_list(bot, SUDO_USERS + SUPPORT_USERS, + "{} has been successfully un-gmuted!".format(mention_html(user_chat.id, + user_chat.first_name)), + html=True) + + message.reply_text("Person has been un-gmuted.") + + +@run_async +def gmutelist(bot: Bot, update: Update): + muted_users = sql.get_gmute_list() + + if not muted_users: + update.effective_message.reply_text("There aren't any gmuted users! You're kinder than I expected...") + return + + mutefile = 'Screw these guys.\n' + for user in muted_users: + mutefile += "[x] {} - {}\n".format(user["name"], user["user_id"]) + if user["reason"]: + mutefile += "Reason: {}\n".format(user["reason"]) + + with BytesIO(str.encode(mutefile)) as output: + output.name = "gmutelist.txt" + update.effective_message.reply_document(document=output, filename="gmutelist.txt", + caption="Here is the list of currently gmuted users.") + + +def check_and_mute(bot, update, user_id, should_message=True): + if sql.is_user_gmuted(user_id): + bot.restrict_chat_member(update.effective_chat.id, user_id, can_send_messages=False) + if should_message: + update.effective_message.reply_text("This is a bad person, I'll silence them for you!") + + +@run_async +def enforce_gmute(bot: Bot, update: Update): + # Not using @restrict handler to avoid spamming - just ignore if cant gmute. + if sql.does_chat_gmute(update.effective_chat.id) and update.effective_chat.get_member(bot.id).can_restrict_members: + user = update.effective_user # type: Optional[User] + chat = update.effective_chat # type: Optional[Chat] + msg = update.effective_message # type: Optional[Message] + + if user and not is_user_admin(chat, user.id): + check_and_mute(bot, update, user.id, should_message=True) + if msg.new_chat_members: + new_members = update.effective_message.new_chat_members + for mem in new_members: + check_and_mute(bot, update, mem.id, should_message=True) + if msg.reply_to_message: + user = msg.reply_to_message.from_user # type: Optional[User] + if user and not is_user_admin(chat, user.id): + check_and_mute(bot, update, user.id, should_message=True) + +@run_async +@user_admin +def gmutestat(bot: Bot, update: Update, args: List[str]): + if len(args) > 0: + if args[0].lower() in ["on", "yes"]: + sql.enable_gmutes(update.effective_chat.id) + update.effective_message.reply_text("I've enabled gmutes in this group. This will help protect you " + "from spammers, unsavoury characters, and Anirudh.") + elif args[0].lower() in ["off", "no"]: + sql.disable_gmutes(update.effective_chat.id) + update.effective_message.reply_text("I've disabled gmutes in this group. GMutes wont affect your users " + "anymore. You'll be less protected from Anirudh though!") + else: + update.effective_message.reply_text("Give me some arguments to choose a setting! on/off, yes/no!\n\n" + "Your current setting is: {}\n" + "When True, any gmutes that happen will also happen in your group. " + "When False, they won't, leaving you at the possible mercy of " + "spammers.".format(sql.does_chat_gmute(update.effective_chat.id))) + + +def __stats__(): + return "{} gmuted users.".format(sql.num_gmuted_users()) + + +def __user_info__(user_id): + is_gmuted = sql.is_user_gmuted(user_id) + + text = "Globally muted: {}" + if is_gmuted: + text = text.format("Yes") + user = sql.get_gmuted_user(user_id) + if user.reason: + text += "\nReason: {}".format(html.escape(user.reason)) + else: + text = text.format("No") + return text + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + return "This chat is enforcing *gmutes*: `{}`.".format(sql.does_chat_gmute(chat_id)) + + +__help__ = """ +*Admin only:* + - /gmutestat : Will disable the effect of global mutes on your group, or return your current settings. +Gmutes, also known as global mutes, are used by the bot owners to mute spammers across all groups. This helps protect \ +you and your groups by removing spam flooders as quickly as possible. They can be disabled for you group by calling \ +/gmutestat +""" + +__mod_name__ = "GLOBAL MUTE" + +GMUTE_HANDLER = CommandHandler("gmute", gmute, pass_args=True, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) +UNGMUTE_HANDLER = CommandHandler("ungmute", ungmute, pass_args=True, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) +GMUTE_LIST = CommandHandler("gmutelist", gmutelist, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) + +GMUTE_STATUS = CommandHandler("gmutestat", gmutestat, pass_args=True, filters=Filters.group) + +GMUTE_ENFORCER = MessageHandler(Filters.all & Filters.group, enforce_gmute) + +dispatcher.add_handler(GMUTE_HANDLER) +dispatcher.add_handler(UNGMUTE_HANDLER) +dispatcher.add_handler(GMUTE_LIST) +dispatcher.add_handler(GMUTE_STATUS) + +if STRICT_GMUTE: + dispatcher.add_handler(GMUTE_ENFORCER, GMUTE_ENFORCE_GROUP) diff --git a/fire_bot/modules/google_reverse_search.py b/fire_bot/modules/google_reverse_search.py new file mode 100644 index 0000000..273bf98 --- /dev/null +++ b/fire_bot/modules/google_reverse_search.py @@ -0,0 +1,192 @@ +import os +import re +import requests +import urllib +from urllib.request import urlopen +from urllib.error import URLError, HTTPError +from bs4 import BeautifulSoup + +from typing import List +from telegram import ParseMode, InputMediaPhoto, Update, Bot, TelegramError +from telegram.ext import run_async + +from tg_bot import dispatcher + +from tg_bot.modules.disable import DisableAbleCommandHandler + + +opener = urllib.request.build_opener() +useragent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.38 Safari/537.36' +#useragent = 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36' +opener.addheaders = [('User-agent', useragent)] + + +@run_async +def reverse(bot: Bot, update: Update, args: List[str]): + if os.path.isfile("okgoogle.png"): + os.remove("okgoogle.png") + + msg = update.effective_message + chat_id = update.effective_chat.id + rtmid = msg.message_id + imagename = "okgoogle.png" + + reply = msg.reply_to_message + if reply: + if reply.sticker: + file_id = reply.sticker.file_id + elif reply.photo: + file_id = reply.photo[-1].file_id + elif reply.document: + file_id = reply.document.file_id + else: + msg.reply_text("Reply to an image or sticker to lookup.") + return + image_file = bot.get_file(file_id) + image_file.download(imagename) + if args: + txt = args[0] + try: + lim = int(txt) + except: + lim = 2 + else: + lim = 2 + elif args and not reply: + splatargs = msg.text.split(" ") + if len(splatargs) == 3: + img_link = splatargs[1] + try: + lim = int(splatargs[2]) + except: + lim = 2 + elif len(splatargs) == 2: + img_link = splatargs[1] + lim = 2 + else: + msg.reply_text("/reverse ") + return + try: + urllib.request.urlretrieve(img_link, imagename) + except HTTPError as HE: + if HE.reason == 'Not Found': + msg.reply_text("Image not found.") + return + elif HE.reason == 'Forbidden': + msg.reply_text("Couldn't access the provided link, The website might have blocked accessing to the website by bot or the website does not existed.") + return + except URLError as UE: + msg.reply_text(f"{UE.reason}") + return + except ValueError as VE: + msg.reply_text(f"{VE}\nPlease try again using http or https protocol.") + return + else: + msg.reply_markdown("Please reply to a sticker, or an image to search it!\nDo you know that you can search an image with a link too? `/reverse [picturelink] `.") + return + + try: + searchUrl = 'https://www.google.com/searchbyimage/upload' + multipart = {'encoded_image': (imagename, open(imagename, 'rb')), 'image_content': ''} + response = requests.post(searchUrl, files=multipart, allow_redirects=False) + fetchUrl = response.headers['Location'] + + if response != 400: + xx = bot.send_message(chat_id, "Image was successfully uploaded to Google." + "\nParsing source now. Maybe.", reply_to_message_id=rtmid) + else: + xx = bot.send_message(chat_id, "Google told me to go away.", reply_to_message_id=rtmid) + return + + os.remove(imagename) + match = ParseSauce(fetchUrl + "&hl=en") + guess = match['best_guess'] + if match['override'] and not match['override'] == '': + imgspage = match['override'] + else: + imgspage = match['similar_images'] + + if guess and imgspage: + xx.edit_text(f"[{guess}]({fetchUrl})\nLooking for images...", parse_mode='Markdown', disable_web_page_preview=True) + else: + xx.edit_text("Couldn't find anything.") + return + + images = scam(imgspage, lim) + if len(images) == 0: + xx.edit_text(f"[{guess}]({fetchUrl})\n[Visually similar images]({imgspage})" + "\nCouldn't fetch any images.", parse_mode='Markdown', disable_web_page_preview=True) + return + + imglinks = [] + for link in images: + lmao = InputMediaPhoto(media=str(link)) + imglinks.append(lmao) + + bot.send_media_group(chat_id=chat_id, media=imglinks, reply_to_message_id=rtmid) + xx.edit_text(f"[{guess}]({fetchUrl})\n[Visually similar images]({imgspage})", parse_mode='Markdown', disable_web_page_preview=True) + except TelegramError as e: + print(e) + except Exception as exception: + print(exception) + +def ParseSauce(googleurl): + """Parse/Scrape the HTML code for the info we want.""" + + source = opener.open(googleurl).read() + soup = BeautifulSoup(source, 'html.parser') + + results = { + 'similar_images': '', + 'override': '', + 'best_guess': '' + } + + try: + for bess in soup.findAll('a', {'class': 'PBorbe'}): + url = 'https://www.google.com' + bess.get('href') + results['override'] = url + except: + pass + + for similar_image in soup.findAll('input', {'class': 'gLFyf'}): + url = 'https://www.google.com/search?tbm=isch&q=' + urllib.parse.quote_plus(similar_image.get('value')) + results['similar_images'] = url + + for best_guess in soup.findAll('div', attrs={'class':'r5a77d'}): + results['best_guess'] = best_guess.get_text() + + return results + +def scam(imgspage, lim): + """Parse/Scrape the HTML code for the info we want.""" + + single = opener.open(imgspage).read() + decoded = single.decode('utf-8') + if int(lim) > 10: + lim = 10 + + imglinks = [] + counter = 0 + + pattern = r'^,\[\"(.*[.png|.jpg|.jpeg])\",[0-9]+,[0-9]+\]$' + oboi = re.findall(pattern, decoded, re.I | re.M) + + for imglink in oboi: + counter += 1 + imglinks.append(imglink) + if counter >= int(lim): + break + + return imglinks + + +__help__ = """ +- /reverse: Does a reverse image search of the media which it was replied to. +""" + +__mod_name__ = "IMAGE SEARCH" + +REVERSE_HANDLER = DisableAbleCommandHandler("reverse", reverse, pass_args=True, admin_ok=True) + +dispatcher.add_handler(REVERSE_HANDLER) diff --git a/fire_bot/modules/gps.py b/fire_bot/modules/gps.py new file mode 100644 index 0000000..4b95cf5 --- /dev/null +++ b/fire_bot/modules/gps.py @@ -0,0 +1,53 @@ +import html +import json +import random +from datetime import datetime +from typing import Optional, List +import time +import requests +from telegram import Message, Chat, Update, Bot, MessageEntity +from telegram import ParseMode +from telegram.ext import CommandHandler, run_async, Filters +from telegram.utils.helpers import escape_markdown, mention_html + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.helper_funcs.filters import CustomFilters + +from geopy.geocoders import Nominatim +from telegram import Location + + +GMAPS_LOC = "https://maps.googleapis.com/maps/api/geocode/json" + + + +def gps(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + if len(args) == 0: + update.effective_message.reply_text("That was a funny joke, but no really, put in a location") + try: + geolocator = Nominatim(user_agent="SkittBot") + location = " ".join(args) + geoloc = geolocator.geocode(location) + chat_id = update.effective_chat.id + lon = geoloc.longitude + lat = geoloc.latitude + the_loc = Location(lon, lat) + gm = "https://www.google.com/maps/search/{},{}".format(lat,lon) + bot.send_location(chat_id, location=the_loc) + update.message.reply_text("Open with: [Google Maps]({})".format(gm), parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + except AttributeError: + update.message.reply_text("I can't find that") + + +__help__ = """ +- /gps: Get gps location.. +""" + +__mod_name__ = "GPS" + +GPS_HANDLER = DisableAbleCommandHandler("gps", gps, pass_args=True) + +dispatcher.add_handler(GPS_HANDLER) diff --git a/fire_bot/modules/grammar.py b/fire_bot/modules/grammar.py new file mode 100644 index 0000000..6ef6424 --- /dev/null +++ b/fire_bot/modules/grammar.py @@ -0,0 +1,60 @@ + +import json +from pprint import pprint + +import requests +from telegram import Update, Bot +from telegram.ext import CommandHandler + +from tg_bot import dispatcher + +# Open API key +API_KEY = "6ae0c3a0-afdc-4532-a810-82ded0054236" +URL = "http://services.gingersoftware.com/Ginger/correct/json/GingerTheText" + + +def translate(bot: Bot, update: Update): + if update.effective_message.reply_to_message: + msg = update.effective_message.reply_to_message + + params = dict( + lang="US", + clientVersion="2.0", + apiKey=API_KEY, + text=msg.text + ) + + res = requests.get(URL, params=params) + # print(res) + # print(res.text) + pprint(json.loads(res.text)) + changes = json.loads(res.text).get('LightGingerTheTextResult') + curr_string = "" + + prev_end = 0 + + for change in changes: + start = change.get('From') + end = change.get('To') + 1 + suggestions = change.get('Suggestions') + if suggestions: + sugg_str = suggestions[0].get('Text') # should look at this list more + curr_string += msg.text[prev_end:start] + sugg_str + + prev_end = end + + curr_string += msg.text[prev_end:] + print(curr_string) + update.effective_message.reply_text(curr_string) + + +__help__ = """ + - /t: while replying to a message, will reply with a grammar corrected version +""" + +__mod_name__ = "GRAMMAR" + + +TRANSLATE_HANDLER = CommandHandler('t', translate) + +dispatcher.add_handler(TRANSLATE_HANDLER) diff --git a/fire_bot/modules/gtranslator.py b/fire_bot/modules/gtranslator.py new file mode 100644 index 0000000..261592b --- /dev/null +++ b/fire_bot/modules/gtranslator.py @@ -0,0 +1,131 @@ +from emoji import UNICODE_EMOJI +from googletrans import Translator, LANGUAGES +from telegram import Bot, Update, ParseMode +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + + +@run_async +def totranslate(bot: Bot, update: Update): + msg = update.effective_message + problem_lang_code = [] + for key in LANGUAGES: + if "-" in key: + problem_lang_code.append(key) + try: + if msg.reply_to_message and msg.reply_to_message.text: + + args = update.effective_message.text.split(None, 1) + text = msg.reply_to_message.text + message = update.effective_message + dest_lang = None + + try: + source_lang = args[1].split(None, 1)[0] + except: + source_lang = "en" + + 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 == 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 = Translator() + if source_lang == None: + detection = trl.detect(text) + tekstr = trl.translate(text, dest=dest_lang) + return message.reply_text( + f"Translated from `{detection.lang}` to `{dest_lang}`:\n`{tekstr.text}`", + parse_mode=ParseMode.MARKDOWN) + else: + tekstr = trl.translate(text, dest=dest_lang, src=source_lang) + message.reply_text(f"Translated from `{source_lang}` to `{dest_lang}`:\n`{tekstr.text}`", + parse_mode=ParseMode.MARKDOWN) + else: + args = update.effective_message.text.split(None, 2) + message = update.effective_message + source_lang = args[1] + text = args[2] + exclude_list = UNICODE_EMOJI.keys() + for emoji in exclude_list: + if emoji in text: + text = text.replace(emoji, '') + dest_lang = None + temp_source_lang = source_lang + if temp_source_lang.count('-') == 2: + for lang in problem_lang_code: + if lang in temp_source_lang: + if temp_source_lang.startswith(lang): + dest_lang = temp_source_lang.rsplit("-", 1)[1] + source_lang = temp_source_lang.rsplit("-", 1)[0] + else: + dest_lang = temp_source_lang.split("-", 1)[1] + source_lang = temp_source_lang.split("-", 1)[0] + elif temp_source_lang.count('-') == 1: + for lang in problem_lang_code: + if lang in temp_source_lang: + dest_lang = None + break + else: + dest_lang = temp_source_lang.split("-")[1] + source_lang = temp_source_lang.split("-")[0] + trl = Translator() + if dest_lang == None: + detection = trl.detect(text) + tekstr = trl.translate(text, dest=source_lang) + return message.reply_text( + "Translated from `{}` to `{}`:\n`{}`".format(detection.lang, source_lang, tekstr.text), + parse_mode=ParseMode.MARKDOWN) + else: + tekstr = trl.translate(text, dest=dest_lang, src=source_lang) + message.reply_text("Translated from `{}` to `{}`:\n`{}`".format(source_lang, dest_lang, tekstr.text), + parse_mode=ParseMode.MARKDOWN) + + except IndexError: + update.effective_message.reply_text( + "Reply to messages or write 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.", + parse_mode="markdown", disable_web_page_preview=True) + except ValueError: + update.effective_message.reply_text("The intended language is not found!") + else: + return + + +__help__ = """ +- /tr (language code) as reply to a long message. +""" + +TRANSLATE_HANDLER = DisableAbleCommandHandler("tr", totranslate) + +dispatcher.add_handler(TRANSLATE_HANDLER) + +__mod_name__ = "TRANSLATOR" +__command_list__ = ["tr"] +__handlers__ = [TRANSLATE_HANDLER] diff --git a/fire_bot/modules/locks.py b/fire_bot/modules/locks.py new file mode 100644 index 0000000..fc8f072 --- /dev/null +++ b/fire_bot/modules/locks.py @@ -0,0 +1,355 @@ +import html + +from typing import List + +import telegram.ext as tg +from telegram import Bot, Update, ParseMode, MessageEntity +from telegram import TelegramError +from telegram.error import BadRequest +from telegram.ext import CommandHandler, MessageHandler, Filters, run_async +from telegram.utils.helpers import mention_html + +import tg_bot.modules.sql.locks_sql as sql +from tg_bot import dispatcher, SUDO_USERS, DEV_USERS, LOGGER +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import can_delete, is_user_admin, user_not_admin, user_admin, bot_can_delete, is_bot_admin, connection_status +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.log_channel import loggable +from tg_bot.modules.sql import users_sql + +LOCK_TYPES = { + 'sticker': Filters.sticker, + 'audio': Filters.audio, + 'voice': Filters.voice, + 'document': Filters.document, + 'video': Filters.video, + 'contact': Filters.contact, + 'photo': Filters.photo, + 'gif': Filters.document & CustomFilters.mime_type("video/mp4"), + 'url': Filters.entity(MessageEntity.URL) | Filters.caption_entity(MessageEntity.URL), + 'bots': Filters.status_update.new_chat_members, + 'forward': Filters.forwarded, + 'game': Filters.game, + 'location': Filters.location, +} + +GIF = Filters.document & CustomFilters.mime_type("video/mp4") +OTHER = Filters.game | Filters.sticker | GIF +MEDIA = Filters.audio | Filters.document | Filters.video | Filters.voice | Filters.photo +MESSAGES = Filters.text | Filters.contact | Filters.location | Filters.venue | Filters.command | MEDIA | OTHER +PREVIEWS = Filters.entity("url") + +RESTRICTION_TYPES = { + 'messages': MESSAGES, + 'media': MEDIA, + 'other': OTHER, + # 'previews': PREVIEWS, # NOTE: this has been removed cos its useless atm. + 'all': Filters.all +} + +PERM_GROUP = 1 +REST_GROUP = 2 + + +class CustomCommandHandler(tg.CommandHandler): + def __init__(self, command, callback, **kwargs): + super().__init__(command, callback, **kwargs) + + def check_update(self, update): + return super().check_update(update) and not ( + sql.is_restr_locked(update.effective_chat.id, 'messages') and not is_user_admin(update.effective_chat, + update.effective_user.id)) + + +tg.CommandHandler = CustomCommandHandler + + +# NOT ASYNC +def restr_members(bot, chat_id, members, messages=False, media=False, other=False, previews=False): + for mem in members: + if mem.user in SUDO_USERS or mem.user in DEV_USERS: + pass + try: + bot.restrict_chat_member(chat_id, mem.user, + can_send_messages=messages, + can_send_media_messages=media, + can_send_other_messages=other, + can_add_web_page_previews=previews) + except TelegramError: + pass + + +# NOT ASYNC +def unrestr_members(bot, chat_id, members, messages=True, media=True, other=True, previews=True): + for mem in members: + try: + bot.restrict_chat_member(chat_id, mem.user, + can_send_messages=messages, + can_send_media_messages=media, + can_send_other_messages=other, + can_add_web_page_previews=previews) + except TelegramError: + pass + + +@run_async +@connection_status +def locktypes(bot: Bot, update: Update): + update.effective_message.reply_text("\n - ".join(["Locks: "] + list(LOCK_TYPES) + list(RESTRICTION_TYPES))) + + +@user_admin +@connection_status +@bot_can_delete +@loggable +def lock(bot: Bot, update: Update, args: List[str]) -> str: + + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + + if can_delete(chat, bot.id): + if len(args) >= 1: + if args[0] in LOCK_TYPES: + sql.update_lock(chat.id, args[0], locked=True) + message.reply_text("Locked {} messages for all non-admins!".format(args[0])) + + return "{}:" \ + "\n#LOCK" \ + "\nAdmin: {}" \ + "\nLocked {}.".format(html.escape(chat.title), + mention_html(user.id, user.first_name), args[0]) + + elif args[0] in RESTRICTION_TYPES: + sql.update_restriction(chat.id, args[0], locked=True) + """ + if args[0] == "messages": + chat.set_permissions(can_send_messages=False) + + elif args[0] == "media": + chat.set_permissions(can_send_media_messages=False) + + elif args[0] == "other": + chat.set_permissions(can_send_other_messages=False) + + elif args[0] == "previews": + chat.set_permissions(can_add_web_page_previews=False) + + elif args[0] == "all": + chat.set_permissions(can_send_messages=False) + """ + message.reply_text("Locked {} for all non-admins!".format(args[0])) + return "{}:" \ + "\n#LOCK" \ + "\nAdmin: {}" \ + "\nLocked {}.".format(html.escape(chat.title), + mention_html(user.id, user.first_name), args[0]) + + else: + message.reply_text("What are you trying to lock...? Try /locktypes for the list of lockables") + + else: + message.reply_text("I'm not an administrator, or haven't got delete rights.") + + return "" + + +@run_async +@connection_status +@user_admin +@loggable +def unlock(bot: Bot, update: Update, args: List[str]) -> str: + + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + + if is_user_admin(chat, message.from_user.id): + if len(args) >= 1: + if args[0] in LOCK_TYPES: + sql.update_lock(chat.id, args[0], locked=False) + message.reply_text("Unlocked {} for everyone!".format(args[0])) + return "{}:" \ + "\n#UNLOCK" \ + "\nAdmin: {}" \ + "\nUnlocked {}.".format(html.escape(chat.title), + mention_html(user.id, user.first_name), args[0]) + + elif args[0] in RESTRICTION_TYPES: + sql.update_restriction(chat.id, args[0], locked=False) + """ + #members = users_sql.get_chat_members(chat.id) + if args[0] == "messages": + chat.set_permissions(can_send_messages=True) + + elif args[0] == "media": + chat.set_permissions(can_send_media_messages=True) + + elif args[0] == "other": + chat.set_permissions(can_send_other_messages=True) + + elif args[0] == "previews": + chat.set_permissions(can_add_web_page_previews=True) + + elif args[0] == "all": + chat.set_permissions(can_send_messages=True, can_send_media_messages=True, can_send_other_messages=True, can_add_web_page_previews=True, can_send_polls=True) + """ + message.reply_text("Unlocked {} for everyone!".format(args[0])) + + return "{}:" \ + "\n#UNLOCK" \ + "\nAdmin: {}" \ + "\nUnlocked {}.".format(html.escape(chat.title), + mention_html(user.id, user.first_name), args[0]) + else: + message.reply_text("What are you trying to unlock...? Try /locktypes for the list of lockables") + + else: + bot.sendMessage(chat.id, "What are you trying to unlock...?") + + return "" + + +@run_async +@user_not_admin +def del_lockables(bot: Bot, update: Update): + + chat = update.effective_chat + message = update.effective_message + + for lockable, filter in LOCK_TYPES.items(): + if filter(message) and sql.is_locked(chat.id, lockable) and can_delete(chat, bot.id): + if lockable == "bots": + new_members = update.effective_message.new_chat_members + for new_mem in new_members: + if new_mem.is_bot: + if not is_bot_admin(chat, bot.id): + message.reply_text("I see a bot, and I've been told to stop them joining... " + "but I'm not admin!") + return + + chat.kick_member(new_mem.id) + message.reply_text("Only admins are allowed to add bots to this chat!.") + else: + try: + message.delete() + except BadRequest as excp: + if excp.message == "Message to delete not found": + pass + else: + LOGGER.exception("ERROR in lockables") + + break + + +@run_async +@user_not_admin +def rest_handler(bot: Bot, update: Update): + + msg = update.effective_message + chat = update.effective_chat + for restriction, filter in RESTRICTION_TYPES.items(): + if filter(msg) and sql.is_restr_locked(chat.id, restriction) and can_delete(chat, bot.id): + try: + msg.delete() + except BadRequest as excp: + if excp.message == "Message to delete not found": + pass + else: + LOGGER.exception("ERROR in restrictions") + break + + +def format_lines(lst, spaces): + widths = [max([len(str(lst[i][j])) for i in range(len(lst))]) for j in range(len(lst[0]))] + + lines = [(" " * spaces).join( + [" " * int((widths[i] - len(str(r[i]))) / 2) + str(r[i]) + + " " * int((widths[i] - len(str(r[i])) + (1 if widths[i] % 2 != len(str(r[i])) % 2 else 0)) / 2) + for i in range(len(r))]) for r in lst] + + return "\n".join(lines) + + +def repl(lst, index, true_val, false_val): + return [t[0:index] + [true_val if t[index] else false_val] + t[index+1:len(t)] for t in lst] + + +def build_lock_message(chat_id): + + locks = sql.get_locks(chat_id) + restr = sql.get_restr(chat_id) + + if not (locks or restr): + res = "There are no current locks in this chat." + else: + res = "These are the locks in this chat:\n" + ls = [] + if locks: + ls += repl([["sticker", "=", locks.sticker], ["audio", "=", locks.audio], ["voice", "=", locks.voice], + ["document", "=", locks.document], ["video", "=", locks.video], ["contact", "=", locks.contact], + ["photo", "=", locks.photo], ["gif", "=", locks.gif], ["url", "=", locks.url], + ["bots", "=", locks.bots], ["forward", "=", locks.forward], ["game", "=", locks.game], + ["location", "=", locks.location]] + , 2, "Locked", "Unlocked") + if restr: + ls += repl([["messages", "=", restr.messages], ["media", "=", restr.media], + ["other", "=", restr.other], ["previews", "=", restr.preview], + ["all", "=", all([restr.messages, restr.media, restr.other, restr.preview])]] + , 2, "Restricted", "Unrestricted") + # DON'T REMOVE THE NEWLINE BELOW + res += "```\n" + format_lines(ls,1) + "```" + return res + + +@run_async +@connection_status +@user_admin +def list_locks(bot: Bot, update: Update): + + chat = update.effective_chat + + res = build_lock_message(chat.id) + + update.effective_message.reply_text(res, parse_mode=ParseMode.MARKDOWN) + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + return build_lock_message(chat_id) + + +__help__ = """ + - /locktypes: a list of possible locktypes + +*Admin only:* + - /lock : lock items of a certain type (not available in private) + - /unlock : unlock items of a certain type (not available in private) + - /locks: the current list of locks in this chat. + +Locks can be used to restrict a group's users. +eg: +Locking urls will auto-delete all messages with urls which haven't been whitelisted, locking stickers will delete all \ +stickers, etc. +Locking bots will stop non-admins from adding bots to the chat. +""" + +LOCKTYPES_HANDLER = DisableAbleCommandHandler("locktypes", locktypes) +LOCK_HANDLER = CommandHandler("lock", lock, pass_args=True) +UNLOCK_HANDLER = CommandHandler("unlock", unlock, pass_args=True) +LOCKED_HANDLER = CommandHandler("locks", list_locks) +LOCKABLE_HANDLER = MessageHandler(Filters.all & Filters.group, del_lockables) +RESTRICTION_HANDLER = MessageHandler(Filters.all & Filters.group, rest_handler) + +dispatcher.add_handler(LOCK_HANDLER) +dispatcher.add_handler(UNLOCK_HANDLER) +dispatcher.add_handler(LOCKTYPES_HANDLER) +dispatcher.add_handler(LOCKED_HANDLER) +dispatcher.add_handler(LOCKABLE_HANDLER, PERM_GROUP) +dispatcher.add_handler(RESTRICTION_HANDLER, REST_GROUP) + +__mod_name__ = "LOCKS" +__handlers__ = [LOCKTYPES_HANDLER, LOCK_HANDLER, UNLOCK_HANDLER, LOCKED_HANDLER, (LOCKABLE_HANDLER, PERM_GROUP), (RESTRICTION_HANDLER, REST_GROUP)] diff --git a/fire_bot/modules/log_channel.py b/fire_bot/modules/log_channel.py new file mode 100644 index 0000000..d385a92 --- /dev/null +++ b/fire_bot/modules/log_channel.py @@ -0,0 +1,204 @@ +from datetime import datetime +from functools import wraps + +from tg_bot.modules.helper_funcs.misc import is_module_loaded + +FILENAME = __name__.rsplit(".", 1)[-1] + +if is_module_loaded(FILENAME): + from telegram import Bot, Update, ParseMode + from telegram.error import BadRequest, Unauthorized + from telegram.ext import CommandHandler, run_async + from telegram.utils.helpers import escape_markdown + + from tg_bot import dispatcher, LOGGER, GBAN_LOGS + from tg_bot.modules.helper_funcs.chat_status import user_admin + from tg_bot.modules.sql import log_channel_sql as sql + + + def loggable(func): + @wraps(func) + def log_action(bot: Bot, update: Update, *args, **kwargs): + + result = func(bot, update, *args, **kwargs) + chat = update.effective_chat + message = update.effective_message + + if result: + datetime_fmt = "%H:%M - %d-%m-%Y" + result += f"\nEvent Stamp: {datetime.utcnow().strftime(datetime_fmt)}" + + 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(bot, log_chat, chat.id, result) + elif result == "": + pass + else: + LOGGER.warning("%s was set as loggable, but had no return statement.", func) + + return result + + return log_action + + + def gloggable(func): + @wraps(func) + def glog_action(bot: Bot, update: Update, *args, **kwargs): + + result = func(bot, update, *args, **kwargs) + chat = update.effective_chat + message = update.effective_message + + if result: + datetime_fmt = "%H:%M - %d-%m-%Y" + result += "\nEvent Stamp: {}".format(datetime.utcnow().strftime(datetime_fmt)) + + if message.chat.type == chat.SUPERGROUP and message.chat.username: + result += f'\nLink: click here' + log_chat = str(GBAN_LOGS) + if log_chat: + send_log(bot, log_chat, chat.id, result) + elif result == "": + pass + else: + LOGGER.warning("%s was set as loggable to gbanlogs, but had no return statement.", func) + + return result + + return glog_action + + + def send_log(bot: Bot, log_chat_id: str, orig_chat_id: str, result: str): + + try: + bot.send_message(log_chat_id, result, parse_mode=ParseMode.HTML, disable_web_page_preview=True) + except BadRequest as excp: + if excp.message == "Chat not found": + bot.send_message(orig_chat_id, "This log channel has been deleted - unsetting.") + sql.stop_chat_logging(orig_chat_id) + else: + LOGGER.warning(excp.message) + LOGGER.warning(result) + LOGGER.exception("Could not parse") + + bot.send_message(log_chat_id, result + "\n\nFormatting has been disabled due to an unexpected error.") + + + @run_async + @user_admin + def logging(bot: Bot, update: Update): + + message = update.effective_message + chat = update.effective_chat + + 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) + + else: + message.reply_text("No log channel has been set for this group!") + + + @run_async + @user_admin + def setlog(bot: Bot, update: Update): + + message = update.effective_message + chat = update.effective_chat + if chat.type == chat.CHANNEL: + message.reply_text("Now, forward the /setlog to the group you want to tie this channel to!") + + elif message.forward_from_chat: + sql.set_chat_log_channel(chat.id, message.forward_from_chat.id) + try: + message.delete() + except BadRequest as excp: + if excp.message == "Message to delete not found": + pass + else: + LOGGER.exception("Error deleting message in log channel. Should work anyway though.") + + try: + bot.send_message(message.forward_from_chat.id, + f"This channel has been set as the log channel for {chat.title or chat.first_name}.") + except Unauthorized as excp: + if excp.message == "Forbidden: bot is not a member of the channel chat": + bot.send_message(chat.id, "Successfully set log channel!") + else: + LOGGER.exception("ERROR in setting the log channel.") + + bot.send_message(chat.id, "Successfully set log channel!") + + else: + message.reply_text("The steps to set a log channel are:\n" + " - add bot to the desired channel\n" + " - send /setlog to the channel\n" + " - forward the /setlog to the group\n") + + + @run_async + @user_admin + def unsetlog(bot: Bot, update: Update): + + message = update.effective_message + chat = update.effective_chat + + log_channel = sql.stop_chat_logging(chat.id) + if log_channel: + bot.send_message(log_channel, f"Channel has been unlinked from {chat.title}") + message.reply_text("Log channel has been un-set.") + + else: + message.reply_text("No log channel has been set yet!") + + + def __stats__(): + return f"{sql.num_logchannels()} log channels set." + + + def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + + def __chat_settings__(chat_id, user_id): + log_channel = sql.get_chat_log_channel(chat_id) + if log_channel: + log_channel_info = dispatcher.bot.get_chat(log_channel) + return f"This group has all it's logs sent to: {escape_markdown(log_channel_info.title)} (`{log_channel}`)" + return "No log channel is set for this group!" + + + __help__ = """ +*Admin only:* +- /logchannel: get log channel info +- /setlog: set the log channel. +- /unsetlog: unset the log channel. +Setting the log channel is done by: +- adding the bot to the desired channel (as an admin!) +- sending /setlog in the channel +- forwarding the /setlog to the group +""" + + __mod_name__ = "LOG CHANNEL" + + LOG_HANDLER = CommandHandler("logchannel", logging) + SET_LOG_HANDLER = CommandHandler("setlog", setlog) + UNSET_LOG_HANDLER = CommandHandler("unsetlog", unsetlog) + + dispatcher.add_handler(LOG_HANDLER) + dispatcher.add_handler(SET_LOG_HANDLER) + dispatcher.add_handler(UNSET_LOG_HANDLER) + +else: + # run anyway if module not loaded + def loggable(func): + return func + + + def gloggable(func): + return func diff --git a/fire_bot/modules/lyrics.py b/fire_bot/modules/lyrics.py new file mode 100644 index 0000000..e302030 --- /dev/null +++ b/fire_bot/modules/lyrics.py @@ -0,0 +1,53 @@ +# Simple lyrics module using tswift by @TheRealPhoenix + +from tswift import Song + +from telegram import Bot, Update, Message, Chat +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + + +@run_async +def lyrics(bot: Bot, update: Update, args): + msg = update.effective_message + query = " ".join(args) + song = "" + if not query: + msg.reply_text("You haven't specified which song to look for!") + return + else: + song = Song.find_song(query) + if song: + if song.lyrics: + reply = song.format() + else: + reply = "Couldn't find any lyrics for that song!" + else: + reply = "Song not found!" + if len(reply) > 4090: + with open("lyrics.txt", 'w') as f: + f.write(f"{reply}\n\n\nOwO UwU OmO") + with open("lyrics.txt", 'rb') as f: + msg.reply_document(document=f, + caption="Message length exceeded max limit! Sending as a text file.") + else: + msg.reply_text(reply) + + + +__help__ = """ +Want to get the lyrics of your favorite songs straight from the app? This module is perfect for that! +*Available commands:* + - /lyrics : returns the lyrics of that song. + You can either enter just the song name or both the artist and song name. +""" + +__mod_name__ = "SONGS LYRICS" + + + +LYRICS_HANDLER = DisableAbleCommandHandler("lyrics", lyrics, pass_args=True) + +dispatcher.add_handler(LYRICS_HANDLER) diff --git a/fire_bot/modules/math.py b/fire_bot/modules/math.py new file mode 100644 index 0000000..ffe76af --- /dev/null +++ b/fire_bot/modules/math.py @@ -0,0 +1,140 @@ +from typing import List +import requests +from telegram import Message, Update, Bot, MessageEntity +from telegram.ext import CommandHandler, run_async +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler +import pynewtonmath as newton +import math + +@run_async +def simplify(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + message.reply_text(newton.simplify('{}'.format(args[0]))) + +@run_async +def factor(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + message.reply_text(newton.factor('{}'.format(args[0]))) + +@run_async +def derive(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + message.reply_text(newton.derive('{}'.format(args[0]))) + +@run_async +def integrate(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + message.reply_text(newton.integrate('{}'.format(args[0]))) + +@run_async +def zeroes(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + message.reply_text(newton.zeroes('{}'.format(args[0]))) + +@run_async +def tangent(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + message.reply_text(newton.tangent('{}'.format(args[0]))) + +@run_async +def area(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + message.reply_text(newton.area('{}'.format(args[0]))) + +@run_async +def cos(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.cos(int(args[0]))) + +@run_async +def sin(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.sin(int(args[0]))) + +@run_async +def tan(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.tan(int(args[0]))) + +@run_async +def arccos(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.acos(int(args[0]))) + +@run_async +def arcsin(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.asin(int(args[0]))) + +@run_async +def arctan(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.atan(int(args[0]))) + +@run_async +def abs(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.fabs(int(args[0]))) + +@run_async +def log(bot: Bot, update: Update, args): + message = update.effective_message + message.reply_text(math.log(int(args[0]))) + +__help__ = """ +Solves complex math problems using https://newton.now.sh + - /math: Simplify `/simplify 2^2+2(2)` + - /factor: Factor `/factor x^2 + 2x` + - /derive: Derive `/derive x^2+2x` + - /integrate: Integrate `/integrate x^2+2x` + - /zeroes: Find 0's `/zeroes x^2+2x` + - /tangent: Find Tangent `/tangent 2lx^3` + - /area: Area Under Curve `/area 2:4lx^3` + - /cos: Cosine `/cos pi` + - /sin: Sine `/sin 0` + - /tan: Tangent `/tan 0` + - /arccos: Inverse Cosine `/arccos 1` + - /arcsin: Inverse Sine `/arcsin 0` + - /arctan: Inverse Tangent `/arctan 0` + - /abs: Absolute Value `/abs -1` + - /log: Logarithm `/log 2l8` + +__Keep in mind__: To find the tangent line of a function at a certain x value, send the request as c|f(x) where c is the given x value and f(x) is the function expression, the separator is a vertical bar '|'. See the table above for an example request. +To find the area under a function, send the request as c:d|f(x) where c is the starting x value, d is the ending x value, and f(x) is the function under which you want the curve between the two x values. +To compute fractions, enter expressions as numerator(over)denominator. For example, to process 2/4 you must send in your expression as 2(over)4. The result expression will be in standard math notation (1/2, 3/4). +""" + +__mod_name__ = "MATH" + +SIMPLIFY_HANDLER = DisableAbleCommandHandler("math", simplify, pass_args=True) +FACTOR_HANDLER = DisableAbleCommandHandler("factor", factor, pass_args=True) +DERIVE_HANDLER = DisableAbleCommandHandler("derive", derive, pass_args=True) +INTEGRATE_HANDLER = DisableAbleCommandHandler("integrate", integrate, pass_args=True) +ZEROES_HANDLER = DisableAbleCommandHandler("zeroes", zeroes, pass_args=True) +TANGENT_HANDLER = DisableAbleCommandHandler("tangent", tangent, pass_args=True) +AREA_HANDLER = DisableAbleCommandHandler("area", area, pass_args=True) +COS_HANDLER = DisableAbleCommandHandler("cos", cos, pass_args=True) +SIN_HANDLER = DisableAbleCommandHandler("sin", sin, pass_args=True) +TAN_HANDLER = DisableAbleCommandHandler("tan", tan, pass_args=True) +ARCCOS_HANDLER = DisableAbleCommandHandler("arccos", arccos, pass_args=True) +ARCSIN_HANDLER = DisableAbleCommandHandler("arcsin", arcsin, pass_args=True) +ARCTAN_HANDLER = DisableAbleCommandHandler("arctan", arctan, pass_args=True) +ABS_HANDLER = DisableAbleCommandHandler("abs", abs, pass_args=True) +LOG_HANDLER = DisableAbleCommandHandler("log", log, pass_args=True) + +dispatcher.add_handler(SIMPLIFY_HANDLER) +dispatcher.add_handler(FACTOR_HANDLER) +dispatcher.add_handler(DERIVE_HANDLER) +dispatcher.add_handler(INTEGRATE_HANDLER) +dispatcher.add_handler(ZEROES_HANDLER) +dispatcher.add_handler(TANGENT_HANDLER) +dispatcher.add_handler(AREA_HANDLER) +dispatcher.add_handler(COS_HANDLER) +dispatcher.add_handler(SIN_HANDLER) +dispatcher.add_handler(TAN_HANDLER) +dispatcher.add_handler(ARCCOS_HANDLER) +dispatcher.add_handler(ARCSIN_HANDLER) +dispatcher.add_handler(ARCTAN_HANDLER) +dispatcher.add_handler(ABS_HANDLER) +dispatcher.add_handler(LOG_HANDLER) diff --git a/fire_bot/modules/misc.py b/fire_bot/modules/misc.py new file mode 100644 index 0000000..013bdf2 --- /dev/null +++ b/fire_bot/modules/misc.py @@ -0,0 +1,473 @@ +import html +import re +import json +from typing import List + +import requests +from requests import get +from bs4 import BeautifulSoup +from telegram import Bot, Update, MessageEntity, ParseMode +from telegram.error import BadRequest +from telegram.ext import CommandHandler, run_async, Filters +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS, DEV_USERS, TIGER_USERS, WHITELIST_USERS +from tg_bot.__main__ import STATS, USER_INFO, TOKEN +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import user_admin, sudo_plus, bot_admin, can_restrict +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.sql.safemode_sql import set_safemode, is_safemoded +import tg_bot.modules.sql.users_sql as sql + + +MARKDOWN_HELP = f""" +Markdown is a very powerful formatting tool supported by telegram. {dispatcher.bot.first_name} has some enhancements, to make sure that \ +saved messages are correctly parsed, and to allow you to create buttons. + +- _italic_: wrapping text with '_' will produce italic text +- *bold*: wrapping text with '*' will produce bold text +- `code`: wrapping text with '`' will produce monospaced text, also known as 'code' +- [sometext](someURL): this will create a link - the message will just show sometext, \ +and tapping on it will open the page at someURL. +EG: [test](example.com) + +- [buttontext](buttonurl:someURL): this is a special enhancement to allow users to have telegram \ +buttons in their markdown. buttontext will be what is displayed on the button, and someurl \ +will be the url which is opened. +EG: [This is a button](buttonurl:example.com) + +If you want multiple buttons on the same line, use :same, as such: +[one](buttonurl://example.com) +[two](buttonurl://google.com:same) +This will create two buttons on a single line, instead of one button per line. + +Keep in mind that your message MUST contain some text other than just a button! +""" + + +@run_async +def get_id(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + chat = update.effective_chat + msg = update.effective_message + user_id = extract_user(msg, args) + + if user_id: + + if msg.reply_to_message and msg.reply_to_message.forward_from: + + user1 = message.reply_to_message.from_user + user2 = message.reply_to_message.forward_from + + msg.reply_text(f"The original sender, {html.escape(user2.first_name)}," + f" has an ID of {user2.id}.\n" + f"The forwarder, {html.escape(user1.first_name)}," + f" has an ID of {user1.id}.", + parse_mode=ParseMode.HTML) + + 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) + + else: + + if chat.type == "private": + msg.reply_text(f"Your id is {chat.id}.", + parse_mode=ParseMode.HTML) + + else: + msg.reply_text(f"This group's id is {chat.id}.", + parse_mode=ParseMode.HTML) + + +@run_async +def gifid(bot: Bot, update: Update): + msg = update.effective_message + if msg.reply_to_message and msg.reply_to_message.animation: + update.effective_message.reply_text(f"Gif ID:\n{msg.reply_to_message.animation.file_id}", + parse_mode=ParseMode.HTML) + else: + update.effective_message.reply_text("Please reply to a gif to get its ID.") + + +@run_async +def info(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + chat = update.effective_chat + user_id = extract_user(update.effective_message, args) + + if user_id: + user = bot.get_chat(user_id) + + elif not message.reply_to_message and not args: + user = message.from_user + + elif not message.reply_to_message and (not args or ( + len(args) >= 1 and not args[0].startswith("@") and not args[0].isdigit() and not message.parse_entities( + [MessageEntity.TEXT_MENTION]))): + message.reply_text("I can't extract a user from this.") + return + + else: + return + + text = (f"user information:\n" + f"🆔️ID: {user.id}\n" + f"👤First Name: {html.escape(user.first_name)}") + + if user.last_name: + text += f"\n👤Last Name: {html.escape(user.last_name)}" + + if user.username: + text += f"\n👤Username: @{html.escape(user.username)}" + + text += f"\n👤Permanent user link: {mention_html(user.id, 'link')}" + + num_chats = sql.get_user_num_chats(user.id) + text += f"\n🌍Chat count: {num_chats}" + + 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"\nThis user holds the title {custom_title} here." + except BadRequest: + pass + + disaster_level_present = False + + if user.id == OWNER_ID: + text += "\n😎The Disaster level of this person is 'LEGEND'." + disaster_level_present = True + elif user.id in DEV_USERS: + text += "\n🔥This member is one of 'Hero Association'." + disaster_level_present = True + elif user.id in SUDO_USERS: + text += "\n🔥The Disaster level of this person is 'Dragon'." + disaster_level_present = True + elif user.id in SUPPORT_USERS: + text += "\n🔥The Disaster level of this person is 'HACKER'." + disaster_level_present = True + elif user.id in TIGER_USERS: + text += "\n🔥The Disaster level of this person is 'Tiger'." + disaster_level_present = True + elif user.id in WHITELIST_USERS: + text += "\n🔥The Disaster level of this person is 'Wolf'." + disaster_level_present = True + + if disaster_level_present: + text += ' [?]'.format(bot.username) + + text += "\n" + for mod in USER_INFO: + if mod.__mod_name__ == "Users": + continue + + try: + mod_info = mod.__user_info__(user.id) + except TypeError: + mod_info = mod.__user_info__(user.id, chat.id) + if mod_info: + text += "\n" + mod_info + + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML, disable_web_page_preview=True) + + +@run_async +@user_admin +def echo(bot: Bot, update: Update): + args = update.effective_message.text.split(None, 1) + message = update.effective_message + + if message.reply_to_message: + message.reply_to_message.reply_text(args[1]) + else: + message.reply_text(args[1], quote=False) + + message.delete() + + +@run_async +def markdown_help(bot: Bot, update: Update): + update.effective_message.reply_text(MARKDOWN_HELP, parse_mode=ParseMode.HTML) + update.effective_message.reply_text("Try forwarding the following message to me, and you'll see!") + update.effective_message.reply_text("/save test This is a markdown test. _italics_, *bold*, `code`, " + "[URL](example.com) [button](buttonurl:github.com) " + "[button2](buttonurl://google.com:same)") + + +@run_async +@sudo_plus +def stats(bot: Bot, update: Update): + stats = "Current stats:\n" + "\n".join([mod.__stats__() for mod in STATS]) + result = re.sub(r'(\d+)', r'\1', stats) + update.effective_message.reply_text(result, parse_mode=ParseMode.HTML) + + +@bot_admin +@can_restrict +@user_admin +def safe_mode(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + message = update.effective_message + if not args: + message.reply_text("This chat has its Safe Mode set to *{}*".format(is_safemoded(chat.id).safemode_status), parse_mode=ParseMode.MARKDOWN) + return + + if str(args[0]).lower() in ["on", "yes"]: + set_safemode(chat.id, True) + message.reply_text("Safe Mode has been set to *{}*".format(is_safemoded(chat.id).safemode_status), parse_mode=ParseMode.MARKDOWN) + return + + elif str(args[0]).lower() in ["off", "no"]: + set_safemode(chat.id, False) + message.reply_text("Safe Mode has been set to *{}*".format(is_safemoded(chat.id).safemode_status), parse_mode=ParseMode.MARKDOWN) + return + else: + message.reply_text("I only recognize the arguments `{}`, `{}`, `{}` or `{}`".format("Yes", "No", "On", "Off"), parse_mode=ParseMode.MARKDOWN) + +@run_async +def magisk(bot, update): + url = 'https://raw.githubusercontent.com/topjohnwu/magisk_files/' + releases = "" + for type, branch in {"Stable":["master/stable","master"], "Beta":["master/beta","master"], "Canary (release)":["canary/release","canary"], "Canary (debug)":["canary/debug","canary"]}.items(): + data = get(url + branch[0] + '.json').json() + releases += f'*{type}*: \n' \ + f'• [Changelog](https://github.com/topjohnwu/magisk_files/blob/{branch[1]}/notes.md)\n' \ + f'• Zip - [{data["magisk"]["version"]}-{data["magisk"]["versionCode"]}]({data["magisk"]["link"]}) \n' \ + f'• App - [{data["app"]["version"]}-{data["app"]["versionCode"]}]({data["app"]["link"]}) \n' \ + f'• Uninstaller - [{data["magisk"]["version"]}-{data["magisk"]["versionCode"]}]({data["uninstaller"]["link"]})\n\n' + + + del_msg = update.message.reply_text("*Latest Magisk Releases:*\n{}".format(releases), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + time.sleep(300) + try: + del_msg.delete() + update.effective_message.delete() + except BadRequest as err: + if (err.message == "Message to delete not found" ) or (err.message == "Message can't be deleted" ): + return + +@run_async +def checkfw(bot, update, args): + if not len(args) == 2: + reply = f'Give me something to fetch, like:\n`/checkfw SM-N975F DBT`' + del_msg = update.effective_message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + time.sleep(5) + try: + del_msg.delete() + update.effective_message.delete() + except BadRequest as err: + if (err.message == "Message to delete not found" ) or (err.message == "Message can't be deleted" ): + return + 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') + test = get(f'http://fota-cloud-dn.ospserver.net/firmware/{csc.upper()}/{model.upper()}/version.test.xml') + if test.status_code != 200: + reply = f"Couldn't check for {temp.upper()} and {csc.upper()}, please refine your search or try again later!" + del_msg = update.effective_message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + time.sleep(5) + try: + del_msg.delete() + update.effective_message.delete() + except BadRequest as err: + if (err.message == "Message to delete not found" ) or (err.message == "Message can't be deleted" ): + return + page1 = BeautifulSoup(fota.content, 'lxml') + page2 = BeautifulSoup(test.content, 'lxml') + os1 = page1.find("latest").get("o") + os2 = page2.find("latest").get("o") + if page1.find("latest").text.strip(): + pda1,csc1,phone1=page1.find("latest").text.strip().split('/') + reply = f'*Latest released firmware for {model.upper()} and {csc.upper()} is:*\n' + reply += f'• PDA: `{pda1}`\n• CSC: `{csc1}`\n' + if phone1: + reply += f'• Phone: `{phone1}`\n' + if os1: + reply += f'• Android: `{os1}`\n' + reply += f'\n' + else: + reply = f'*No public release found for {model.upper()} and {csc.upper()}.*\n\n' + reply += f'*Latest test firmware for {model.upper()} and {csc.upper()} is:*\n' + if len(page2.find("latest").text.strip().split('/')) == 3: + pda2,csc2,phone2=page2.find("latest").text.strip().split('/') + reply += f'• PDA: `{pda2}`\n• CSC: `{csc2}`\n' + if phone2: + reply += f'• Phone: `{phone2}`\n' + if os2: + reply += f'• Android: `{os2}`\n' + reply += f'\n' + else: + md5=page2.find("latest").text.strip() + reply += f'• Hash: `{md5}`\n• Android: `{os2}`\n\n' + + update.message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + +@run_async +def getfw(bot, update, args): + if not len(args) == 2: + reply = f'Give me something to fetch, like:\n`/getfw SM-N975F DBT`' + del_msg = update.effective_message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + time.sleep(5) + try: + del_msg.delete() + update.effective_message.delete() + except BadRequest as err: + if (err.message == "Message to delete not found" ) or (err.message == "Message can't be deleted" ): + return + temp,csc = args + model = f'sm-'+temp if not temp.upper().startswith('SM-') else temp + test = get(f'http://fota-cloud-dn.ospserver.net/firmware/{csc.upper()}/{model.upper()}/version.test.xml') + if test.status_code != 200: + reply = f"Couldn't find any firmware downloads for {temp.upper()} and {csc.upper()}, please refine your search or try again later!" + del_msg = update.effective_message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + time.sleep(5) + try: + del_msg.delete() + update.effective_message.delete() + except BadRequest as err: + if (err.message == "Message to delete not found" ) or (err.message == "Message can't be deleted" ): + return + url1 = f'https://samfrew.com/model/{model.upper()}/region/{csc.upper()}/' + 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') + os = page.find("latest").get("o") + reply = "" + if page.find("latest").text.strip(): + pda,csc2,phone=page.find("latest").text.strip().split('/') + reply += f'*Latest firmware for {model.upper()} and {csc.upper()} is:*\n' + reply += f'• PDA: `{pda}`\n• CSC: `{csc2}`\n' + if phone: + reply += f'• Phone: `{phone}`\n' + if os: + reply += f'• Android: `{os}`\n' + reply += f'\n' + reply += f'*Downloads for {model.upper()} and {csc.upper()}*\n' + reply += f'• [samfrew.com]({url1})\n' + reply += f'• [sammobile.com]({url2})\n' + reply += f'• [sfirmware.com]({url3})\n' + reply += f'• [samfw.com]({url4})\n' + update.message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + +@run_async +def twrp(bot, update, args): + if len(args) == 0: + reply='No codename provided, write a codename for fetching informations.' + del_msg = update.effective_message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + time.sleep(5) + try: + del_msg.delete() + update.effective_message.delete() + except BadRequest as err: + if (err.message == "Message to delete not found" ) or (err.message == "Message can't be deleted" ): + return + + device = " ".join(args) + url = get(f'https://eu.dl.twrp.me/{device}/') + if url.status_code == 404: + reply = f"Couldn't find twrp downloads for {device}!\n" + del_msg = update.effective_message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + time.sleep(5) + try: + del_msg.delete() + update.effective_message.delete() + except BadRequest as err: + if (err.message == "Message to delete not found" ) or (err.message == "Message can't be deleted" ): + return + else: + reply = f'*Latest Official TWRP for {device}*\n' + db = get(DEVICES_DATA).json() + newdevice = device.strip('lte') if device.startswith('beyond') else device + try: + brand = db[newdevice][0]['brand'] + name = db[newdevice][0]['name'] + reply += f'*{brand} - {name}*\n' + except KeyError as err: + pass + page = BeautifulSoup(url.content, 'lxml') + date = page.find("em").text.strip() + reply += f'*Updated:* {date}\n' + trs = page.find('table').find_all('tr') + row = 2 if trs[0].find('a').text.endswith('tar') else 1 + for i in range(row): + download = trs[i].find('a') + dl_link = f"https://eu.dl.twrp.me{download['href']}" + dl_file = download.text + size = trs[i].find("span", {"class": "filesize"}).text + reply += f'[{dl_file}]({dl_link}) - {size}\n' + + update.message.reply_text("{}".format(reply), + parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + + + +__help__ = """ + - /id: get the current group id. If used by replying to a message, gets that user's id. + + - /gifid: reply to a gif to me to tell you its file ID. + + - /info: get information about a user. + + - /markdownhelp: quick summary of how markdown works in telegram - can only be called in private chats. + + - /safemode : Disallows new users to send media for 24 hours after joining a group. + Use unmute to unrestrict them. + + - /magisk - gets the latest magisk release for Stable/Beta/Canary + + - /twrp - gets latest twrp for the android device using the codename + + - /checkfw - Samsung only - shows the latest firmware info for the given device, taken from samsung servers + + - /getfw - Samsung only - gets firmware download links from samfrew, sammobile and sfirmwares for the given device + + - /imdb : View IMDb results for selected movie or TV series +""" + +ID_HANDLER = DisableAbleCommandHandler("id", get_id, pass_args=True) +GIFID_HANDLER = DisableAbleCommandHandler("gifid", gifid) +INFO_HANDLER = DisableAbleCommandHandler("info", info, pass_args=True) +ECHO_HANDLER = DisableAbleCommandHandler("echo", echo, filters=Filters.group) +MD_HELP_HANDLER = CommandHandler("markdownhelp", markdown_help, filters=Filters.private) +STATS_HANDLER = CommandHandler("stats", stats) +MAGISK_HANDLER = DisableAbleCommandHandler("magisk", magisk) +TWRP_HANDLER = DisableAbleCommandHandler("twrp", twrp, pass_args=True) +GETFW_HANDLER = DisableAbleCommandHandler("getfw", getfw, pass_args=True) +CHECKFW_HANDLER = DisableAbleCommandHandler("checkfw", checkfw, pass_args=True) + +SAFEMODE_HANDLER = CommandHandler("safemode", safe_mode, pass_args=True) + +dispatcher.add_handler(ID_HANDLER) +dispatcher.add_handler(GIFID_HANDLER) +dispatcher.add_handler(INFO_HANDLER) +dispatcher.add_handler(ECHO_HANDLER) +dispatcher.add_handler(MD_HELP_HANDLER) +dispatcher.add_handler(STATS_HANDLER) +dispatcher.add_handler(SAFEMODE_HANDLER) +dispatcher.add_handler(MAGISK_HANDLER) +dispatcher.add_handler(TWRP_HANDLER) +dispatcher.add_handler(GETFW_HANDLER) +dispatcher.add_handler(CHECKFW_HANDLER) + + +__mod_name__ = "MASTER MOD" +__command_list__ = ["id", "info", "echo"] +__handlers__ = [ID_HANDLER, GIFID_HANDLER, INFO_HANDLER, ECHO_HANDLER, MD_HELP_HANDLER, STATS_HANDLER, SAFEMODE_HANDLER, MAGISK_HANDLER, TWRP_HANDLER, GETFW_HANDLER, CHECKFW_HANDLER] diff --git a/fire_bot/modules/modules.py b/fire_bot/modules/modules.py new file mode 100644 index 0000000..538cd93 --- /dev/null +++ b/fire_bot/modules/modules.py @@ -0,0 +1,161 @@ +import importlib + +from telegram import Bot, Update, ParseMode +from telegram.ext import CommandHandler, run_async + +from tg_bot import dispatcher +from tg_bot.__main__ import (IMPORTED, HELPABLE, MIGRATEABLE, STATS, USER_INFO, DATA_IMPORT, DATA_EXPORT, CHAT_SETTINGS, + USER_SETTINGS) +from tg_bot.modules.helper_funcs.chat_status import sudo_plus, dev_plus + + +@run_async +@dev_plus +def load(bot: Bot, update: Update): + message = update.effective_message + text = message.text.split(" ", 1)[1] + load_messasge = message.reply_text(f"Attempting to load module : {text}", parse_mode=ParseMode.HTML) + + try: + imported_module = importlib.import_module("tg_bot.modules." + text) + except: + load_messasge.edit_text("Does that module even exist?") + return + + if not hasattr(imported_module, "__mod_name__"): + imported_module.__mod_name__ = imported_module.__name__ + + if not imported_module.__mod_name__.lower() in IMPORTED: + IMPORTED[imported_module.__mod_name__.lower()] = imported_module + else: + load_messasge.edit_text("Module already loaded.") + return + if "__handlers__" in dir(imported_module): + handlers = imported_module.__handlers__ + for handler in handlers: + if type(handler) != tuple: + dispatcher.add_handler(handler) + else: + handler_name, priority = handler + dispatcher.add_handler(handler_name, priority) + else: + IMPORTED.pop(imported_module.__mod_name__.lower()) + load_messasge.edit_text("The module cannot be loaded.") + return + + if hasattr(imported_module, "__help__") and imported_module.__help__: + HELPABLE[imported_module.__mod_name__.lower()] = imported_module + + # Chats to migrate on chat_migrated events + if hasattr(imported_module, "__migrate__"): + MIGRATEABLE.append(imported_module) + + if hasattr(imported_module, "__stats__"): + STATS.append(imported_module) + + if hasattr(imported_module, "__user_info__"): + USER_INFO.append(imported_module) + + if hasattr(imported_module, "__import_data__"): + DATA_IMPORT.append(imported_module) + + if hasattr(imported_module, "__export_data__"): + DATA_EXPORT.append(imported_module) + + if hasattr(imported_module, "__chat_settings__"): + CHAT_SETTINGS[imported_module.__mod_name__.lower()] = imported_module + + if hasattr(imported_module, "__user_settings__"): + USER_SETTINGS[imported_module.__mod_name__.lower()] = imported_module + + load_messasge.edit_text("Successfully loaded module : {}".format(text), parse_mode=ParseMode.HTML) + + +@run_async +@dev_plus +def unload(bot: Bot, update: Update): + message = update.effective_message + text = message.text.split(" ", 1)[1] + unload_messasge = message.reply_text(f"Attempting to unload module : {text}", parse_mode=ParseMode.HTML) + + try: + imported_module = importlib.import_module("tg_bot.modules." + text) + except: + unload_messasge.edit_text("Does that module even exist?") + return + + if not hasattr(imported_module, "__mod_name__"): + imported_module.__mod_name__ = imported_module.__name__ + if imported_module.__mod_name__.lower() in IMPORTED: + IMPORTED.pop(imported_module.__mod_name__.lower()) + else: + unload_messasge.edit_text("Can't unload something that isn't loaded.") + return + if "__handlers__" in dir(imported_module): + handlers = imported_module.__handlers__ + for handler in handlers: + if type(handler) == bool: + unload_messasge.edit_text("This module can't be unloaded!") + return + elif type(handler) != tuple: + dispatcher.remove_handler(handler) + else: + handler_name, priority = handler + dispatcher.remove_handler(handler_name, priority) + else: + unload_messasge.edit_text("The module cannot be unloaded.") + return + + if hasattr(imported_module, "__help__") and imported_module.__help__: + HELPABLE.pop(imported_module.__mod_name__.lower()) + + # Chats to migrate on chat_migrated events + if hasattr(imported_module, "__migrate__"): + MIGRATEABLE.remove(imported_module) + + if hasattr(imported_module, "__stats__"): + STATS.remove(imported_module) + + if hasattr(imported_module, "__user_info__"): + USER_INFO.remove(imported_module) + + if hasattr(imported_module, "__import_data__"): + DATA_IMPORT.remove(imported_module) + + if hasattr(imported_module, "__export_data__"): + DATA_EXPORT.remove(imported_module) + + if hasattr(imported_module, "__chat_settings__"): + CHAT_SETTINGS.pop(imported_module.__mod_name__.lower()) + + if hasattr(imported_module, "__user_settings__"): + USER_SETTINGS.pop(imported_module.__mod_name__.lower()) + + unload_messasge.edit_text(f"Successfully unloaded module : {text}", parse_mode=ParseMode.HTML) + + +@run_async +@sudo_plus +def listmodules(bot: Bot, update: Update): + message = update.effective_message + module_list = [] + + for helpable_module in HELPABLE: + helpable_module_info = IMPORTED[helpable_module] + file_info = IMPORTED[helpable_module_info.__mod_name__.lower()] + file_name = file_info.__name__.rsplit("tg_bot.modules.", 1)[1] + mod_name = file_info.__mod_name__ + module_list.append(f'- {mod_name} ({file_name})\n') + module_list = "Following modules are loaded : \n\n" + ''.join(module_list) + message.reply_text(module_list, parse_mode=ParseMode.HTML) + + +LOAD_HANDLER = CommandHandler("load", load) +UNLOAD_HANDLER = CommandHandler("unload", unload) +LISTMODULES_HANDLER = CommandHandler("listmodules", listmodules) + +dispatcher.add_handler(LOAD_HANDLER) +dispatcher.add_handler(UNLOAD_HANDLER) +dispatcher.add_handler(LISTMODULES_HANDLER) + +__mod_name__ = "MODULES" diff --git a/fire_bot/modules/music.py b/fire_bot/modules/music.py new file mode 100644 index 0000000..0498b1a --- /dev/null +++ b/fire_bot/modules/music.py @@ -0,0 +1,82 @@ +#Made by @LEGENDX22 +import html +import time +import datetime +from telegram.ext import CommandHandler, run_async, Filters +import requests, logging +from telegram.ext import Updater, MessageHandler, Filters, CommandHandler +from telegram import Message, Chat, Update, Bot, MessageEntity +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS, WHITELIST_USERS +from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from tg_bot.modules.helper_funcs.chat_status import user_admin + +count = 0 +@run_async +def music(bot: Bot, update: Update, args): + message = update.effective_message + global count + + chatId = update.message.chat_id + + video_id = ''.join(args) + + if video_id.find('youtu.be') != -1: + index = video_id.rfind('/') + 1 + video_id = video_id[index:][:11] + message.reply_text("Please wait...\ndownloading audio.") + + elif video_id.find('youtube') != -1: + index = video_id.rfind('?v=') + 3 + video_id = video_id[index:][:11] + message.reply_text("Please wait...\downloading audio.") + + elif not video_id.find('youtube') != -1: + message.reply_text("Please provide me youtube link") + + elif not video_id.find('youtu.be') != -1: + message.reply_text("Please provide me youtube link") + + + + + + + r = requests.get(f'https://api.pointmp3.com/dl/{video_id}?format=mp3') + + + json1_response = r.json() + + if not json1_response['error']: + + + redirect_link = json1_response['url'] + + r = requests.get(redirect_link) + + + json2_response = r.json() + + if not json2_response['error']: + payload = json2_response['payload'] + + info = '*{0}* \nUploaded by @AnonymousD3061'.format(payload['fulltitle']) + + try: + + bot.send_audio(chat_id=chatId, audio=json2_response['url'] ,parse_mode='Markdown',text="meanya", caption=info) + count += 1 + print("\033[1m\033[96m" + "Download count: " + str(count) + "\033[0m") + except: + bot.send_message(chat_id=chatId, text='Something went wrong with the download..!\nPlease Report there @AnonymousD3061') + + +__help__ = """ Youtube audio Downloader + - /music : download audio file from youtube link. + +""" +__mod_name__ = "MP3 DOWNLOADER" + +music_handler = CommandHandler('music', music, pass_args=True) +dispatcher.add_handler(music_handler) + + diff --git a/fire_bot/modules/muting.py b/fire_bot/modules/muting.py new file mode 100644 index 0000000..0576830 --- /dev/null +++ b/fire_bot/modules/muting.py @@ -0,0 +1,208 @@ +import html + +from typing import Optional, List + +from telegram import Bot, Chat, Update, ParseMode +from telegram.error import BadRequest +from telegram.ext import CommandHandler, run_async +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, LOGGER, TIGER_USERS +from tg_bot.modules.helper_funcs.chat_status import (bot_admin, user_admin, is_user_admin, can_restrict, + connection_status) +from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text +from tg_bot.modules.helper_funcs.string_handling import extract_time +from tg_bot.modules.log_channel import loggable + + +def check_user(user_id: int, bot: Bot, chat: Chat) -> Optional[str]: + if not user_id: + reply = "You don't seem to be referring to a user." + return reply + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + reply = "I can't seem to find this user" + return reply + else: + raise + + if user_id == bot.id: + reply = "I'm not gonna MUTE myself, How high are you?" + return reply + + if is_user_admin(chat, user_id, member) or user_id in TIGER_USERS: + reply = "I really wish I could mute admins...Perhaps a Punch?" + return reply + + return None + + +@run_async +@connection_status +@bot_admin +@user_admin +@loggable +def mute(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + + user_id, reason = extract_user_and_text(message, args) + reply = check_user(user_id, bot, chat) + + if reply: + message.reply_text(reply) + return "" + + member = chat.get_member(user_id) + + log = (f"{html.escape(chat.title)}:\n" + f"#MUTE\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(member.user.id, member.user.first_name)}") + + if reason: + log += f"\nReason: {reason}" + + if member.can_send_messages is None or member.can_send_messages: + bot.restrict_chat_member(chat.id, user_id, can_send_messages=False) + bot.sendMessage(chat.id, f"Muted {html.escape(member.user.first_name)} with no expiration date!", + parse_mode=ParseMode.HTML) + return log + + else: + message.reply_text("This user is already muted!") + + return "" + + +@run_async +@connection_status +@bot_admin +@user_admin +@loggable +def unmute(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + + 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.") + 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): + message.reply_text("This user already has the right to speak.") + else: + bot.restrict_chat_member(chat.id, int(user_id), + can_send_messages=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True) + bot.sendMessage(chat.id, f"I shall allow {html.escape(member.user.first_name)} to text!", + parse_mode=ParseMode.HTML) + return (f"{html.escape(chat.title)}:\n" + f"#UNMUTE\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + 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!") + + return "" + + +@run_async +@connection_status +@bot_admin +@can_restrict +@user_admin +@loggable +def temp_mute(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + message = update.effective_message + + user_id, reason = extract_user_and_text(message, args) + reply = check_user(user_id, bot, chat) + + if reply: + message.reply_text(reply) + return "" + + member = chat.get_member(user_id) + + if not reason: + message.reply_text("You haven't specified a time to mute this user for!") + return "" + + split_reason = reason.split(None, 1) + + time_val = split_reason[0].lower() + if len(split_reason) > 1: + reason = split_reason[1] + else: + reason = "" + + mutetime = extract_time(message, time_val) + + if not mutetime: + return "" + + log = (f"{html.escape(chat.title)}:\n" + f"#TEMP MUTED\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(member.user.id, member.user.first_name)}\n" + f"Time: {time_val}") + if reason: + log += f"\nReason: {reason}" + + try: + if member.can_send_messages is None or member.can_send_messages: + bot.restrict_chat_member(chat.id, user_id, until_date=mutetime, can_send_messages=False) + bot.sendMessage(chat.id, f"Muted {html.escape(member.user.first_name)} for {time_val}!", + parse_mode=ParseMode.HTML) + return log + else: + message.reply_text("This user is already muted.") + + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text(f"Muted for {time_val}!", quote=False) + return log + else: + LOGGER.warning(update) + LOGGER.exception("ERROR muting user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, + excp.message) + message.reply_text("Well damn, I can't mute that user.") + + return "" + + +__help__ = """ +*Admin only:* + - /mute : silences a user. Can also be used as a reply, muting the replied to user. + - /tmute x(m/h/d): mutes a user for x time. (via handle, or reply). m = minutes, h = hours, d = days. + - /unmute : unmutes a user. Can also be used as a reply, muting the replied to user. +""" + +MUTE_HANDLER = CommandHandler("mute", mute, pass_args=True) +UNMUTE_HANDLER = CommandHandler("unmute", unmute, pass_args=True) +TEMPMUTE_HANDLER = CommandHandler(["tmute", "tempmute"], temp_mute, pass_args=True) + +dispatcher.add_handler(MUTE_HANDLER) +dispatcher.add_handler(UNMUTE_HANDLER) +dispatcher.add_handler(TEMPMUTE_HANDLER) + +__mod_name__ = "MUTES" +__handlers__ = [MUTE_HANDLER, UNMUTE_HANDLER, TEMPMUTE_HANDLER] diff --git a/fire_bot/modules/notes.py b/fire_bot/modules/notes.py new file mode 100644 index 0000000..3b7134f --- /dev/null +++ b/fire_bot/modules/notes.py @@ -0,0 +1,258 @@ +import re +from io import BytesIO +from typing import Optional, List + +from telegram import MAX_MESSAGE_LENGTH, ParseMode, InlineKeyboardMarkup +from telegram import Message, Update, Bot +from telegram.error import BadRequest +from telegram.ext import CommandHandler, RegexHandler +from telegram.ext.dispatcher import run_async +from telegram.utils.helpers import escape_markdown + +import tg_bot.modules.sql.notes_sql as sql +from tg_bot import dispatcher, MESSAGE_DUMP, LOGGER +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import user_admin +from tg_bot.modules.helper_funcs.misc import build_keyboard, revert_buttons +from tg_bot.modules.helper_funcs.msg_types import get_note_type + +FILE_MATCHER = re.compile(r"^###file_id(!photo)?###:(.*?)(?:\s|$)") + +ENUM_FUNC_MAP = { + sql.Types.TEXT.value: dispatcher.bot.send_message, + sql.Types.BUTTON_TEXT.value: dispatcher.bot.send_message, + sql.Types.STICKER.value: dispatcher.bot.send_sticker, + sql.Types.DOCUMENT.value: dispatcher.bot.send_document, + sql.Types.PHOTO.value: dispatcher.bot.send_photo, + sql.Types.AUDIO.value: dispatcher.bot.send_audio, + sql.Types.VOICE.value: dispatcher.bot.send_voice, + sql.Types.VIDEO.value: dispatcher.bot.send_video +} + + +# Do not async +def get(bot, update, notename, show_none=True, no_format=False): + chat_id = update.effective_chat.id + note = sql.get_note(chat_id, notename) + message = update.effective_message # type: Optional[Message] + + if note: + # If we're replying to a message, reply to that message (unless it's an error) + if message.reply_to_message: + reply_id = message.reply_to_message.message_id + else: + reply_id = message.message_id + + if note.is_reply: + if MESSAGE_DUMP: + try: + bot.forward_message(chat_id=chat_id, from_chat_id=MESSAGE_DUMP, message_id=note.value) + 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(chat_id, notename) + else: + raise + else: + try: + bot.forward_message(chat_id=chat_id, from_chat_id=chat_id, message_id=note.value) + 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(chat_id, notename) + else: + raise + else: + text = note.value + keyb = [] + parseMode = ParseMode.MARKDOWN + buttons = sql.get_buttons(chat_id, notename) + if no_format: + parseMode = None + text += revert_buttons(buttons) + else: + keyb = build_keyboard(buttons) + + keyboard = InlineKeyboardMarkup(keyb) + + try: + if note.msgtype in (sql.Types.BUTTON_TEXT, sql.Types.TEXT): + bot.send_message(chat_id, text, reply_to_message_id=reply_id, + parse_mode=parseMode, disable_web_page_preview=True, + reply_markup=keyboard) + else: + ENUM_FUNC_MAP[note.msgtype](chat_id, note.file, caption=text, reply_to_message_id=reply_id, + parse_mode=parseMode, disable_web_page_preview=True, + reply_markup=keyboard) + + except BadRequest as excp: + if excp.message == "Entity_mention_user_invalid": + message.reply_text("Looks like you tried to mention someone I've never seen before. If you really " + "want to mention them, forward one of their messages to me, and I'll be able " + "to tag them!") + elif FILE_MATCHER.match(note.value): + message.reply_text("This note was an incorrectly imported file from another bot - I can't use " + "it. If you really need it, you'll have to save it again. In " + "the meantime, I'll remove it from your notes list.") + sql.rm_note(chat_id, notename) + else: + message.reply_text("This note could not be sent, as it is incorrectly formatted. Ask in " + "@OnePunchSupport if you can't figure out why!") + LOGGER.exception("Could not parse message #%s in chat %s", notename, str(chat_id)) + LOGGER.warning("Message was: %s", str(note.value)) + return + elif show_none: + message.reply_text("This note doesn't exist") + + +@run_async +def cmd_get(bot: Bot, update: Update, args: List[str]): + if len(args) >= 2 and args[1].lower() == "noformat": + get(bot, update, args[0], show_none=True, no_format=True) + elif len(args) >= 1: + get(bot, update, args[0], show_none=True) + else: + update.effective_message.reply_text("Get rekt") + + +@run_async +def hash_get(bot: Bot, update: Update): + message = update.effective_message.text + fst_word = message.split()[0] + no_hash = fst_word[1:] + get(bot, update, no_hash, show_none=False) + + +@run_async +@user_admin +def save(bot: Bot, update: Update): + chat_id = update.effective_chat.id + msg = update.effective_message # type: Optional[Message] + + note_name, text, data_type, content, buttons = get_note_type(msg) + + if data_type is None: + msg.reply_text("Dude, there's no note") + return + + sql.add_note_to_db(chat_id, note_name, text, data_type, buttons=buttons, file=content) + + msg.reply_text(f"Yas! Added {note_name}.\nGet it with /get {note_name}, or #{note_name}") + + if msg.reply_to_message and msg.reply_to_message.from_user.is_bot: + if text: + msg.reply_text("Seems like you're trying to save a message from a bot. Unfortunately, " + "bots can't forward bot messages, so I can't save the exact message. " + "\nI'll save all the text I can, but if you want more, you'll have to " + "forward the message yourself, and then save it.") + else: + msg.reply_text("Bots are kinda handicapped by telegram, making it hard for bots to " + "interact with other bots, so I can't save this message " + "like I usually would - do you mind forwarding it and " + "then saving that new message? Thanks!") + return + + +@run_async +@user_admin +def clear(bot: Bot, update: Update, args: List[str]): + chat_id = update.effective_chat.id + if len(args) >= 1: + notename = args[0] + + if sql.rm_note(chat_id, notename): + update.effective_message.reply_text("Successfully removed note.") + else: + update.effective_message.reply_text("That's not a note in my database!") + + +@run_async +def list_notes(bot: Bot, update: Update): + chat_id = update.effective_chat.id + note_list = sql.get_all_chat_notes(chat_id) + + msg = "*Notes in chat:*\n" + for note in note_list: + note_name = escape_markdown(f" - {note.name}\n") + if len(msg) + len(note_name) > MAX_MESSAGE_LENGTH: + update.effective_message.reply_text(msg, parse_mode=ParseMode.MARKDOWN) + msg = "" + msg += note_name + + if msg == "*Notes in chat:*\n": + update.effective_message.reply_text("No notes in this chat!") + + elif len(msg) != 0: + update.effective_message.reply_text(msg, parse_mode=ParseMode.MARKDOWN) + + +def __import_data__(chat_id, data): + failures = [] + for notename, notedata in data.get('extra', {}).items(): + match = FILE_MATCHER.match(notedata) + + if match: + failures.append(notename) + notedata = notedata[match.end():].strip() + if notedata: + sql.add_note_to_db(chat_id, notename[1:], notedata, sql.Types.TEXT) + else: + sql.add_note_to_db(chat_id, notename[1:], notedata, sql.Types.TEXT) + + if failures: + with BytesIO(str.encode("\n".join(failures))) as output: + output.name = "failed_imports.txt" + dispatcher.bot.send_document(chat_id, document=output, filename="failed_imports.txt", + caption="These files/photos failed to import due to originating " + "from another bot. This is a telegram API restriction, and can't " + "be avoided. Sorry for the inconvenience!") + + +def __stats__(): + return f"{sql.num_notes()} notes, across {sql.num_chats()} chats." + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + notes = sql.get_all_chat_notes(chat_id) + return f"There are `{len(notes)}` notes in this chat." + + +__help__ = """ + - /get : get the note with this notename + - #: same as /get + - /notes or /saved: list all saved notes in this chat + +If you would like to retrieve the contents of a note without any formatting, use `/get noformat`. This can \ +be useful when updating a current note. + +*Admin only:* + - /save : saves notedata as a note with name notename +A button can be added to a note by using standard markdown link syntax - the link should just be prepended with a \ +`buttonurl:` section, as such: `[somelink](buttonurl:example.com)`. Check /markdownhelp for more info. + - /save : save the replied message as a note with name notename + - /clear : clear note with this name +""" + +__mod_name__ = "NOTES" + +GET_HANDLER = CommandHandler("get", cmd_get, pass_args=True) +HASH_GET_HANDLER = RegexHandler(r"^#[^\s]+", hash_get) + +SAVE_HANDLER = CommandHandler("save", save) +DELETE_HANDLER = CommandHandler("clear", clear, pass_args=True) + +LIST_HANDLER = DisableAbleCommandHandler(["notes", "saved"], list_notes, admin_ok=True) + +dispatcher.add_handler(GET_HANDLER) +dispatcher.add_handler(SAVE_HANDLER) +dispatcher.add_handler(LIST_HANDLER) +dispatcher.add_handler(DELETE_HANDLER) +dispatcher.add_handler(HASH_GET_HANDLER) diff --git a/fire_bot/modules/paste.py b/fire_bot/modules/paste.py new file mode 100644 index 0000000..9e1bb97 --- /dev/null +++ b/fire_bot/modules/paste.py @@ -0,0 +1,42 @@ +from typing import List + +import requests +from telegram import Update, Bot, ParseMode +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + + +@run_async +def paste(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if message.reply_to_message: + data = message.reply_to_message.text + + elif len(args) >= 1: + data = message.text.split(None, 1)[1] + + else: + 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}' + + message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + +__help__ = """ + - /paste - Do a paste at `neko.bin` +""" + +PASTE_HANDLER = DisableAbleCommandHandler("paste", paste, pass_args=True) +dispatcher.add_handler(PASTE_HANDLER) + +__mod_name__ = "PASTE" +__command_list__ = ["paste"] +__handlers__ = [PASTE_HANDLER] diff --git a/fire_bot/modules/ping.py b/fire_bot/modules/ping.py new file mode 100644 index 0000000..2909627 --- /dev/null +++ b/fire_bot/modules/ping.py @@ -0,0 +1,109 @@ +import time +from typing import List + +import requests +from telegram import Bot, Update, ParseMode +from telegram.ext import run_async + +from tg_bot import dispatcher, StartTime +from tg_bot.modules.disable import DisableAbleCommandHandler + +sites_list = { + "Telegram": "https://api.telegram.org", + "Kaizoku": "https://animekaizoku.com", + "Kayo": "https://animekayo.com", + "Jikan": "https://api.jikan.moe/v3" +} + + +def get_readable_time(seconds: int) -> str: + count = 0 + ping_time = "" + time_list = [] + time_suffix_list = ["s", "m", "h", "days"] + + while count < 4: + count += 1 + if count < 3: + remainder, result = divmod(seconds, 60) + else: + remainder, result = divmod(seconds, 24) + if seconds == 0 and remainder == 0: + break + time_list.append(int(result)) + seconds = int(remainder) + + for x in range(len(time_list)): + time_list[x] = str(time_list[x]) + time_suffix_list[x] + if len(time_list) == 4: + ping_time += time_list.pop() + ", " + + time_list.reverse() + ping_time += ":".join(time_list) + + return ping_time + + +def ping_func(to_ping: List[str]) -> List[str]: + ping_result = [] + + for each_ping in to_ping: + + start_time = time.time() + site_to_ping = sites_list[each_ping] + r = requests.get(site_to_ping) + end_time = time.time() + ping_time = str(round((end_time - start_time), 2)) + "s" + + pinged_site = f"{each_ping}" + + if each_ping is "calculating..." or each_ping is "calculating..": + pinged_site = f'{each_ping}' + ping_time = f"{ping_time} (Status: {r.status_code})" + + ping_text = f"{pinged_site}: {ping_time}" + ping_result.append(ping_text) + + return ping_result + + +@run_async +def ping(bot: Bot, update: Update): + telegram_ping = ping_func(["Telegram"])[0].split(": ", 1)[1] + uptime = get_readable_time((time.time() - StartTime)) + + reply_msg = ("PONG!!\n" + "Time Taken: {}\n" + "Service uptime: {}".format(telegram_ping, uptime)) + + update.effective_message.reply_text(reply_msg, parse_mode=ParseMode.HTML) + + +@run_async +def pingall(bot: Bot, update: Update): + to_ping = ["Kaizoku", "Kayo", "Telegram", "Jikan"] + pinged_list = ping_func(to_ping) + pinged_list.insert(2, '') + uptime = get_readable_time((time.time() - StartTime)) + + reply_msg = "⏱Ping results are:\n" + reply_msg += "\n".join(pinged_list) + reply_msg += '\nService uptime: {}'.format(uptime) + + update.effective_message.reply_text(reply_msg, parse_mode=ParseMode.HTML, disable_web_page_preview=True) + + +__help__ = """ + - /ping - get ping time of bot to telegram server + - /pingall - get all listed ping time +""" + +PING_HANDLER = DisableAbleCommandHandler("ping", ping) +PINGALL_HANDLER = DisableAbleCommandHandler("pingall", pingall) + +dispatcher.add_handler(PING_HANDLER) +dispatcher.add_handler(PINGALL_HANDLER) + +__mod_name__ = "PING" +__command_list__ = ["ping", "pingall"] +__handlers__ = [PING_HANDLER, PINGALL_HANDLER] diff --git a/fire_bot/modules/police.py b/fire_bot/modules/police.py new file mode 100644 index 0000000..d318403 --- /dev/null +++ b/fire_bot/modules/police.py @@ -0,0 +1,47 @@ +import html +import random +import time +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import is_user_admin, user_admin +from tg_bot.modules.helper_funcs.extraction import extract_user + +#sleep how many times after each edit in 'police' +EDIT_SLEEP = 1 +#edit how many times in 'police' +EDIT_TIMES = 3 + +police_siren = [ + "🔴🔴🔴⬜️⬜️⬜️🔵🔵🔵\n🔴🔴🔴⬜️⬜️⬜️🔵🔵🔵\n🔴🔴🔴⬜️⬜️⬜️🔵🔵🔵", + "🔵🔵🔵⬜️⬜️⬜️🔴🔴🔴\n🔵🔵🔵⬜️⬜️⬜️🔴🔴🔴\n🔵🔵🔵⬜️⬜️⬜️🔴🔴🔴" +] + + + +@user_admin +@run_async +def police(bot: Bot, update: Update): + msg = update.effective_message.reply_text('Police is coming!') + for x in range(EDIT_TIMES): + msg.edit_text(police_siren[x%2]) + time.sleep(EDIT_SLEEP) + msg.edit_text('Police is here!') + + +__help__ = """ +- /police : 🚔 +""" + +POLICE_HANDLER = DisableAbleCommandHandler("police", police) + + +dispatcher.add_handler(POLICE_HANDLER) + +__mod_name__ = "POLICE" +__command_list__ = ["police"] +__handlers__ = [POLICE_HANDLER] diff --git a/fire_bot/modules/purge.py b/fire_bot/modules/purge.py new file mode 100644 index 0000000..9e5ef31 --- /dev/null +++ b/fire_bot/modules/purge.py @@ -0,0 +1,116 @@ +import html +from typing import List + +from telegram import Bot, Update, ParseMode +from telegram.error import BadRequest +from telegram.ext import Filters, run_async +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, LOGGER +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import user_admin, can_delete +from tg_bot.modules.log_channel import loggable + + +@run_async +@user_admin +@loggable +def purge(bot: Bot, update: Update, args: List[str]) -> str: + msg = update.effective_message + user = update.effective_user + chat = update.effective_chat + + if can_delete(chat, bot.id): + + if msg.reply_to_message: + + message_id = msg.reply_to_message.message_id + start_message_id = message_id - 1 + delete_to = msg.message_id - 1 + + if args and args[0].isdigit(): + new_del = message_id + int(args[0]) + # No point deleting messages which haven't been written yet. + if new_del < delete_to: + delete_to = new_del + else: + + if args and args[0].isdigit(): + messages_to_delete = int(args[0]) + + if messages_to_delete < 1: + msg.reply_text("Can't purge less than 1 message.") + return "" + + delete_to = msg.message_id - 1 + start_message_id = delete_to - messages_to_delete + + for m_id in range(delete_to, start_message_id, -1): # Reverse iteration over message ids + + try: + bot.deleteMessage(chat.id, m_id) + except BadRequest as err: + if err.message == "Message can't be deleted": + bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " + "not have delete rights, or this might not be a supergroup.") + + elif err.message != "Message to delete not found": + LOGGER.exception("Error while purging chat messages.") + + try: + msg.delete() + except BadRequest as err: + if err.message == "Message can't be deleted": + bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " + "not have delete rights, or this might not be a supergroup.") + + elif err.message != "Message to delete not found": + LOGGER.exception("Error while purging chat messages.") + + bot.send_message(chat.id, f"Purge {delete_to - start_message_id} messages.", + parse_mode=ParseMode.HTML) + return (f"{html.escape(chat.title)}:\n" + f"#PURGE\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Purged {delete_to - start_message_id} messages.") + + return "" + + +@run_async +@user_admin +@loggable +def del_message(bot: Bot, update: Update) -> str: + if update.effective_message.reply_to_message: + user = update.effective_user + chat = update.effective_chat + if can_delete(chat, bot.id): + update.effective_message.reply_to_message.delete() + update.effective_message.delete() + return (f"{html.escape(chat.title)}:\n" + f"#DEL\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Message deleted.") + else: + update.effective_message.reply_text("Whadya want to delete?") + + return "" + + +__help__ = """ +*Admin only:* + - /del: deletes the message you replied to + - /purge: deletes all messages between this and the replied to message. + - /purge : deletes the replied message, and X messages following it if replied to a message. + - /purge : deletes the number of messages starting from bottom. (Counts manaully deleted messages too) +""" + +DELETE_HANDLER = DisableAbleCommandHandler("del", del_message, filters=Filters.group) +PURGE_HANDLER = DisableAbleCommandHandler("purge", purge, filters=Filters.group, pass_args=True) + +dispatcher.add_handler(DELETE_HANDLER) +dispatcher.add_handler(PURGE_HANDLER) + +__mod_name__ = "PURGE & DEL" +__command_list__ = ["del", "purge"] +__handlers__ = [DELETE_HANDLER, PURGE_HANDLER] diff --git a/fire_bot/modules/reactions.py b/fire_bot/modules/reactions.py new file mode 100644 index 0000000..6cfdd93 --- /dev/null +++ b/fire_bot/modules/reactions.py @@ -0,0 +1,235 @@ +import random + +from telegram import Bot, Update +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + +reactions = [ + "( ͡° ͜ʖ ͡°)", + "( . •́ _ʖ •̀ .)", + "( ಠ ͜ʖ ಠ)", + "( ͡ ͜ʖ ͡ )", + "(ʘ ͜ʖ ʘ)", + "ヾ(´〇`)ノ♪♪♪", + "ヽ(o´∀`)ノ♪♬", + "♪♬((d⌒ω⌒b))♬♪", + "└(^^)┐", + "( ̄▽ ̄)/♫•*¨*•.¸¸♪", + "ヾ(⌐■_■)ノ♪", + "乁( • ω •乁)", + "♬♫♪◖(● o ●)◗♪♫♬", + "(っ˘ڡ˘ς)", + "( ˘▽˘)っ♨", + "( ・ω・)⊃-[二二]", + "(*´ー`)旦 旦( ̄ω ̄*)", + "(  ̄▽ ̄)[] [](≧▽≦ )", + "(* ̄▽ ̄)旦 且(´∀`*)", + "(ノ ˘_˘)ノ ζ|||ζ ζ|||ζ ζ|||ζ", + "(ノ°∀°)ノ⌒・*:.。. .。.:*・゜゚・*☆", + "(⊃。•́‿•̀。)⊃━✿✿✿✿✿✿", + "(∩` ロ ´)⊃━炎炎炎炎炎", + "( ・∀・)・・・--------☆", + "( -ω-)/占~~~~~", + "○∞∞∞∞ヽ(^ー^ )", + "(*^^)/~~~~~~~~~~◎", + "(((  ̄□)_/", + "(メ ̄▽ ̄)︻┳═一", + "ヽ( ・∀・)ノ_θ彡☆Σ(ノ `Д´)ノ", + "(*`0´)θ☆(メ°皿°)ノ", + "(; -_-)――――――C<―_-)", + "ヽ(>_<ヽ) ―⊂|=0ヘ(^‿^ )", + "(҂` ロ ´)︻デ═一 \(º □ º l|l)/", + "/( .□.)\ ︵╰(°益°)╯︵ /(.□. /)", + "(`⌒*)O-(`⌒´Q)", + "(っ•﹏•)っ ✴==≡눈٩(`皿´҂)ง", + "ヾ(・ω・)メ(・ω・)ノ", + "(*^ω^)八(⌒▽⌒)八(-‿‿- )ヽ", + "ヽ( ⌒ω⌒)人(=^‥^= )ノ", + "。*:☆(・ω・人・ω・)。:゜☆。", + "(°(°ω(°ω°(☆ω☆)°ω°)ω°)°)", + "(っ˘▽˘)(˘▽˘)˘▽˘ς)", + "(*^ω^)人(^ω^*)", + "\(▽ ̄ \ ( ̄▽ ̄) /  ̄▽)/", + "( ̄Θ ̄)", + "\( ˋ Θ ´ )/", + "( ´(00)ˋ )", + "\( ̄(oo) ̄)/", + "/(≧ x ≦)\", + "/(=・ x ・=)\", + "(=^・ω・^=)", + "(= ; ェ ; =)", + "(=⌒‿‿⌒=)", + "(^• ω •^)", + "ଲ(ⓛ ω ⓛ)ଲ", + "ଲ(ⓛ ω ⓛ)ଲ", + "(^◔ᴥ◔^)", + "[(--)]..zzZ", + "( ̄o ̄) zzZZzzZZ", + "(_ _*) Z z z", + "☆ミ(o*・ω・)ノ", + "ε=ε=ε=ε=┌(; ̄▽ ̄)┘", + "ε===(っ≧ω≦)っ", + "__φ(..)", + "ヾ( `ー´)シφ__", + "( ^▽^)ψ__", + "|・ω・)", + "|д・)", + "┬┴┬┴┤・ω・)ノ", + "|・д・)ノ", + "(* ̄ii ̄)", + "(^〃^)", + "m(_ _)m", + "人(_ _*)", + "(シ. .)シ", + "(^_~)", + "(>ω^)", + "(^_<)〜☆", + "(^_<)", + "(づ ̄ ³ ̄)づ", + "(⊃。•́‿•̀。)⊃", + "⊂(´• ω •`⊂)", + "(*・ω・)ノ", + "(^-^*)/", + "ヾ(*'▽'*)", + "(^0^)ノ", + "(*°ー°)ノ", + "( ̄ω ̄)/", + "(≧▽≦)/", + "w(°o°)w", + "(⊙_⊙)", + "(°ロ°) !", + "∑(O_O;)", + "(¬_¬)", + "(¬_¬ )", + "(↼_↼)", + "( ̄ω ̄;)", + "┐('~`;)┌", + "(・_・;)", + "(@_@)", + "(•ิ_•ิ)?", + "ヽ(ー_ー )ノ", + "┐( ̄ヘ ̄)┌", + "┐( ̄~ ̄)┌", + "┐( ´ д ` )┌", + "╮(︶▽︶)╭", + "ᕕ( ᐛ )ᕗ", + "(ノωヽ)", + "(″ロ゛)", + "(/ω\)", + "(((><)))", + "~(>_<~)", + "(×_×)", + "(×﹏×)", + "(ノ_<。)", + "(μ_μ)", + "o(TヘTo)", + "( ゚,_ゝ`)", + "( ╥ω╥ )", + "(/ˍ・、)", + "(つω`。)", + "(T_T)", + "o(〒﹏〒)o", + "(#`Д´)", + "(・`ω´・)", + "( `ε´ )", + "(メ` ロ ´)", + "Σ(▼□▼メ)", + "(҂ `з´ )", + "٩(╬ʘ益ʘ╬)۶", + "↑_(ΦwΦ)Ψ", + "(ノಥ益ಥ)ノ", + "(#><)", + "(; ̄Д ̄)", + "(¬_¬;)", + "(^^#)", + "( ̄︿ ̄)", + "ヾ(  ̄O ̄)ツ", + "(ᗒᗣᗕ)՞", + "(ノ_<。)ヾ(´ ▽ ` )", + "ヽ( ̄ω ̄(。。 )ゝ", + "(ノ_;)ヾ(´ ∀ ` )", + "(´-ω-`( _ _ )", + "(⌒_⌒;)", + "(*/_\)", + "( ◡‿◡ *)", + "(//ω//)", + "( ̄▽ ̄*)ゞ", + "(„ಡωಡ„)", + "(ノ´ з `)ノ", + "(♡-_-♡)", + "(─‿‿─)♡", + "(´ ω `♡)", + "(ღ˘⌣˘ღ)", + "(´• ω •`) ♡", + "╰(*´︶`*)╯♡", + "(≧◡≦) ♡", + "♡ (˘▽˘>ԅ( ˘⌣˘)", + "σ(≧ε≦σ) ♡", + "(˘∀˘)/(μ‿μ) ❤", + "Σ>―(〃°ω°〃)♡→", + "(* ^ ω ^)", + "(o^▽^o)", + "ヽ(・∀・)ノ", + "(o・ω・o)", + "(^人^)", + "( ´ ω ` )", + "(´• ω •`)", + "╰(▔∀▔)╯", + "(✯◡✯)", + "(⌒‿⌒)", + "(*°▽°*)", + "(´。• ᵕ •。`)", + "ヽ(>∀<☆)ノ", + "\( ̄▽ ̄)/", + "(o˘◡˘o)", + "(╯✧▽✧)╯", + "( ‾́ ◡ ‾́ )", + "(๑˘︶˘๑)", + "(´・ᴗ・ ` )", + "( ͡° ʖ̯ ͡°)", + "( ఠ ͟ʖ ఠ)", + "( ಥ ʖ̯ ಥ)", + "(≖ ͜ʖ≖)", + "ヘ( ̄ω ̄ヘ)", + "(ノ≧∀≦)ノ", + "└( ̄- ̄└))", + "┌(^^)┘", + "(^_^♪)", + "(〜 ̄△ ̄)〜", + "(「• ω •)「", + "( ˘ ɜ˘) ♬♪♫", + "( o˘◡˘o) ┌iii┐", + "♨o(>_<)o♨", + "( ・・)つ―{}@{}@{}-", + "(*´з`)口゚。゚口(・∀・ )", + "( *^^)o∀*∀o(^^* )", + "-●●●-c(・・ )", + "(ノ≧∀≦)ノ ‥…━━━★", + "╰( ͡° ͜ʖ ͡° )つ──☆*:・゚", + "(∩ᄑ_ᄑ)⊃━☆゚*・。*・:≡( ε:)" +] + + +@run_async +def react(bot: Bot, update: Update): + message = update.effective_message + react = random.choice(reactions) + if message.reply_to_message: + message.reply_to_message.reply_text(react) + else: + message.reply_text(react) + + +__help__ = """ + - /react: Reacts with a random reaction +""" + +REACT_HANDLER = DisableAbleCommandHandler("react", react) + +dispatcher.add_handler(REACT_HANDLER) + +__mod_name__ = "REACT" +__command_list__ = ["react"] +__handlers__ = [REACT_HANDLER] diff --git a/fire_bot/modules/remote_cmds.py b/fire_bot/modules/remote_cmds.py new file mode 100644 index 0000000..a12ac9e --- /dev/null +++ b/fire_bot/modules/remote_cmds.py @@ -0,0 +1,441 @@ +import html +from typing import Optional, List + +from telegram import Message, Chat, Update, Bot, User +from telegram.error import BadRequest +from telegram.ext import run_async, CommandHandler, Filters +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher +from tg_bot.modules.helper_funcs.chat_status import bot_admin, user_admin, is_user_ban_protected, can_restrict, \ + is_user_admin, is_user_in_chat, is_bot_admin +from tg_bot.modules.helper_funcs.extraction import extract_user_and_text +from tg_bot.modules.helper_funcs.string_handling import extract_time +from tg_bot.modules.helper_funcs.filters import CustomFilters + +RBAN_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Peer_id_invalid", + "Group chat was deactivated", + "Need to be inviter of a user to kick it from a basic group", + "Chat_admin_required", + "Only the creator of a basic group can kick group administrators", + "Channel_private", + "Not in the chat" +} + +RUNBAN_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Peer_id_invalid", + "Group chat was deactivated", + "Need to be inviter of a user to kick it from a basic group", + "Chat_admin_required", + "Only the creator of a basic group can kick group administrators", + "Channel_private", + "Not in the chat" +} + +RKICK_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Peer_id_invalid", + "Group chat was deactivated", + "Need to be inviter of a user to kick it from a basic group", + "Chat_admin_required", + "Only the creator of a basic group can kick group administrators", + "Channel_private", + "Not in the chat" +} + +RMUTE_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Peer_id_invalid", + "Group chat was deactivated", + "Need to be inviter of a user to kick it from a basic group", + "Chat_admin_required", + "Only the creator of a basic group can kick group administrators", + "Channel_private", + "Not in the chat" +} + +RUNMUTE_ERRORS = { + "User is an administrator of the chat", + "Chat not found", + "Not enough rights to restrict/unrestrict chat member", + "User_not_participant", + "Peer_id_invalid", + "Group chat was deactivated", + "Need to be inviter of a user to kick it from a basic group", + "Chat_admin_required", + "Only the creator of a basic group can kick group administrators", + "Channel_private", + "Not in the chat" +} + +@run_async +@bot_admin +def rban(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if not args: + message.reply_text("You don't seem to be referring to a chat/user.") + return + + user_id, chat_id = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + elif not chat_id: + message.reply_text("You don't seem to be referring to a chat.") + return + + try: + chat = bot.get_chat(chat_id.split()[0]) + except BadRequest as excp: + if excp.message == "Chat not found": + message.reply_text("Chat not found! Make sure you entered a valid chat ID and I'm part of that chat.") + return + else: + raise + + if chat.type == 'private': + message.reply_text("I'm sorry, but that's a private chat!") + return + + if not is_bot_admin(chat, bot.id) or not chat.get_member(bot.id).can_restrict_members: + message.reply_text("I can't restrict people there! Make sure I'm admin and can ban users.") + return + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user") + return + else: + raise + + if is_user_ban_protected(chat, user_id, member): + message.reply_text("I really wish I could ban admins...") + return + + if user_id == bot.id: + message.reply_text("I'm not gonna BAN myself, are you crazy?") + return + + try: + chat.kick_member(user_id) + message.reply_text("Banned from chat!") + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text('Banned!', quote=False) + elif excp.message in RBAN_ERRORS: + message.reply_text(excp.message) + else: + LOGGER.warning(update) + LOGGER.exception("ERROR banning user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, + excp.message) + message.reply_text("Well damn, I can't ban that user.") + +@run_async +@bot_admin +def runban(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if not args: + message.reply_text("You don't seem to be referring to a chat/user.") + return + + user_id, chat_id = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + elif not chat_id: + message.reply_text("You don't seem to be referring to a chat.") + return + + try: + chat = bot.get_chat(chat_id.split()[0]) + except BadRequest as excp: + if excp.message == "Chat not found": + message.reply_text("Chat not found! Make sure you entered a valid chat ID and I'm part of that chat.") + return + else: + raise + + if chat.type == 'private': + message.reply_text("I'm sorry, but that's a private chat!") + return + + if not is_bot_admin(chat, bot.id) or not chat.get_member(bot.id).can_restrict_members: + message.reply_text("I can't unrestrict people there! Make sure I'm admin and can unban users.") + return + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user there") + return + else: + raise + + if is_user_in_chat(chat, user_id): + message.reply_text("Why are you trying to remotely unban someone that's already in that chat?") + return + + if user_id == bot.id: + message.reply_text("I'm not gonna UNBAN myself, I'm an admin there!") + return + + try: + chat.unban_member(user_id) + message.reply_text("Yep, this user can join that chat!") + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text('Unbanned!', quote=False) + elif excp.message in RUNBAN_ERRORS: + message.reply_text(excp.message) + else: + LOGGER.warning(update) + LOGGER.exception("ERROR unbanning user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, + excp.message) + message.reply_text("Well damn, I can't unban that user.") + +@run_async +@bot_admin +def rkick(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if not args: + message.reply_text("You don't seem to be referring to a chat/user.") + return + + user_id, chat_id = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + elif not chat_id: + message.reply_text("You don't seem to be referring to a chat.") + return + + try: + chat = bot.get_chat(chat_id.split()[0]) + except BadRequest as excp: + if excp.message == "Chat not found": + message.reply_text("Chat not found! Make sure you entered a valid chat ID and I'm part of that chat.") + return + else: + raise + + if chat.type == 'private': + message.reply_text("I'm sorry, but that's a private chat!") + return + + if not is_bot_admin(chat, bot.id) or not chat.get_member(bot.id).can_restrict_members: + message.reply_text("I can't restrict people there! Make sure I'm admin and can kick users.") + return + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user") + return + else: + raise + + if is_user_ban_protected(chat, user_id, member): + message.reply_text("I really wish I could kick admins...") + return + + if user_id == bot.id: + message.reply_text("I'm not gonna KICK myself, are you crazy?") + return + + try: + chat.unban_member(user_id) + message.reply_text("Kicked from chat!") + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text('Kicked!', quote=False) + elif excp.message in RKICK_ERRORS: + message.reply_text(excp.message) + else: + LOGGER.warning(update) + LOGGER.exception("ERROR kicking user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, + excp.message) + message.reply_text("Well damn, I can't kick that user.") + +@run_async +@bot_admin +def rmute(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if not args: + message.reply_text("You don't seem to be referring to a chat/user.") + return + + user_id, chat_id = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + elif not chat_id: + message.reply_text("You don't seem to be referring to a chat.") + return + + try: + chat = bot.get_chat(chat_id.split()[0]) + except BadRequest as excp: + if excp.message == "Chat not found": + message.reply_text("Chat not found! Make sure you entered a valid chat ID and I'm part of that chat.") + return + else: + raise + + if chat.type == 'private': + message.reply_text("I'm sorry, but that's a private chat!") + return + + if not is_bot_admin(chat, bot.id) or not chat.get_member(bot.id).can_restrict_members: + message.reply_text("I can't restrict people there! Make sure I'm admin and can mute users.") + return + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user") + return + else: + raise + + if is_user_ban_protected(chat, user_id, member): + message.reply_text("I really wish I could mute admins...") + return + + if user_id == bot.id: + message.reply_text("I'm not gonna MUTE myself, are you crazy?") + return + + try: + bot.restrict_chat_member(chat.id, user_id, can_send_messages=False) + message.reply_text("Muted from the chat!") + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text('Muted!', quote=False) + elif excp.message in RMUTE_ERRORS: + message.reply_text(excp.message) + else: + LOGGER.warning(update) + LOGGER.exception("ERROR mute user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, + excp.message) + message.reply_text("Well damn, I can't mute that user.") + +@run_async +@bot_admin +def runmute(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + if not args: + message.reply_text("You don't seem to be referring to a chat/user.") + return + + user_id, chat_id = extract_user_and_text(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return + elif not chat_id: + message.reply_text("You don't seem to be referring to a chat.") + return + + try: + chat = bot.get_chat(chat_id.split()[0]) + except BadRequest as excp: + if excp.message == "Chat not found": + message.reply_text("Chat not found! Make sure you entered a valid chat ID and I'm part of that chat.") + return + else: + raise + + if chat.type == 'private': + message.reply_text("I'm sorry, but that's a private chat!") + return + + if not is_bot_admin(chat, bot.id) or not chat.get_member(bot.id).can_restrict_members: + message.reply_text("I can't unrestrict people there! Make sure I'm admin and can unban users.") + return + + try: + member = chat.get_member(user_id) + except BadRequest as excp: + if excp.message == "User not found": + message.reply_text("I can't seem to find this user there") + return + else: + raise + + if is_user_in_chat(chat, user_id): + if member.can_send_messages and member.can_send_media_messages \ + and member.can_send_other_messages and member.can_add_web_page_previews: + message.reply_text("This user already has the right to speak in that chat.") + return + + if user_id == bot.id: + message.reply_text("I'm not gonna UNMUTE myself, I'm an admin there!") + return + + try: + bot.restrict_chat_member(chat.id, int(user_id), + can_send_messages=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True) + message.reply_text("Yep, this user can talk in that chat!") + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text('Unmuted!', quote=False) + elif excp.message in RUNMUTE_ERRORS: + message.reply_text(excp.message) + else: + LOGGER.warning(update) + LOGGER.exception("ERROR unmnuting user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, + excp.message) + message.reply_text("Well damn, I can't unmute that user.") + +__help__ = "" + +__mod_name__ = "REMOTE COMMANDS" + +RBAN_HANDLER = CommandHandler("rban", rban, pass_args=True, filters=CustomFilters.sudo_filter) +RUNBAN_HANDLER = CommandHandler("runban", runban, pass_args=True, filters=CustomFilters.sudo_filter) +RKICK_HANDLER = CommandHandler("rkick", rkick, pass_args=True, filters=CustomFilters.sudo_filter) +RMUTE_HANDLER = CommandHandler("rmute", rmute, pass_args=True, filters=CustomFilters.sudo_filter) +RUNMUTE_HANDLER = CommandHandler("runmute", runmute, pass_args=True, filters=CustomFilters.sudo_filter) + +dispatcher.add_handler(RBAN_HANDLER) +dispatcher.add_handler(RUNBAN_HANDLER) +dispatcher.add_handler(RKICK_HANDLER) +dispatcher.add_handler(RMUTE_HANDLER) +dispatcher.add_handler(RUNMUTE_HANDLER) diff --git a/fire_bot/modules/reporting.py b/fire_bot/modules/reporting.py new file mode 100644 index 0000000..f9606a2 --- /dev/null +++ b/fire_bot/modules/reporting.py @@ -0,0 +1,160 @@ +import html +from typing import List + +from telegram import Bot, Chat, Update, ParseMode +from telegram.error import BadRequest, Unauthorized +from telegram.ext import CommandHandler, RegexHandler, Filters, run_async +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, LOGGER, SUDO_USERS, TIGER_USERS +from tg_bot.modules.helper_funcs.chat_status import user_not_admin, user_admin +from tg_bot.modules.log_channel import loggable +from tg_bot.modules.sql import reporting_sql as sql + +REPORT_GROUP = 5 +REPORT_IMMUNE_USERS = SUDO_USERS + TIGER_USERS + + +@run_async +@user_admin +def report_setting(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + msg = update.effective_message + + if chat.type == chat.PRIVATE: + if len(args) >= 1: + if args[0] in ("yes", "on"): + sql.set_user_setting(chat.id, True) + msg.reply_text("Turned on reporting! You'll be notified whenever anyone reports something.") + + elif args[0] in ("no", "off"): + sql.set_user_setting(chat.id, False) + msg.reply_text("Turned off reporting! You wont get any reports.") + else: + msg.reply_text(f"Your current report preference is: `{sql.user_should_report(chat.id)}`", + parse_mode=ParseMode.MARKDOWN) + + else: + if len(args) >= 1: + if args[0] in ("yes", "on"): + sql.set_chat_setting(chat.id, True) + msg.reply_text("Turned on reporting! Admins who have turned on reports will be notified when /report " + "or @admin are called.") + + elif args[0] in ("no", "off"): + sql.set_chat_setting(chat.id, False) + msg.reply_text("Turned off reporting! No admins will be notified on /report or @admin.") + else: + msg.reply_text(f"This chat's current setting is: `{sql.chat_should_report(chat.id)}`", + parse_mode=ParseMode.MARKDOWN) + + +@run_async +@user_not_admin +@loggable +def report(bot: Bot, update: Update) -> str: + message = update.effective_message + chat = update.effective_chat + user = update.effective_user + + if chat and message.reply_to_message and sql.chat_should_report(chat.id): + + reported_user = message.reply_to_message.from_user + chat_name = chat.title or chat.first or chat.username + admin_list = chat.get_administrators() + message = update.effective_message + + if user.id == reported_user.id: + message.reply_text("Uh yeah, Sure.") + return "" + + if user.id == bot.id: + message.reply_text("Nice try.") + return "" + + if reported_user.id in REPORT_IMMUNE_USERS: + message.reply_text("Uh? You reporting whitelisted users?") + return "" + + if chat.username and chat.type == Chat.SUPERGROUP: + + reported = f"{mention_html(user.id, user.first_name)} reported {mention_html(reported_user.id, reported_user.first_name)} to the admins!" + + msg = (f"{html.escape(chat.title)}:\n" + f"Reported user: {mention_html(reported_user.id, reported_user.first_name)} ({reported_user.id})\n" + f"Reported by: {mention_html(user.id, user.first_name)} ({user.id})") + link = f'\nLink: click here' + + should_forward = False + else: + reported = f"{mention_html(user.id, user.first_name)} reported " \ + f"{mention_html(reported_user.id, reported_user.first_name)} to the admins!" + + msg = f'{mention_html(user.id, user.first_name)} is calling for admins in "{html.escape(chat_name)}"!' + link = "" + should_forward = True + + message.reply_text(reported, parse_mode=ParseMode.HTML) + + for admin in admin_list: + + if admin.user.is_bot: # can't message bots + continue + + if sql.user_should_report(admin.user.id): + + try: + bot.send_message(admin.user.id, msg + link, parse_mode=ParseMode.HTML) + + if should_forward: + message.reply_to_message.forward(admin.user.id) + + if len(message.text.split()) > 1: # If user is giving a reason, send his message too + message.forward(admin.user.id) + + except Unauthorized: + pass + + except BadRequest: # TODO: cleanup exceptions + LOGGER.exception("Exception while reporting user") + + return msg + + return "" + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + return "This chat is setup to send user reports to admins, via /report and @admin: `{}`".format( + sql.chat_should_report(chat_id)) + + +def __user_settings__(user_id): + return "You receive reports from chats you're admin in: `{}`.\nToggle this with /reports in PM.".format( + sql.user_should_report(user_id)) + + +__help__ = """ + - /report : reply to a message to report it to admins. + - @admin: reply to a message to report it to admins. +NOTE: Neither of these will get triggered if used by admins. + +*Admin only:* + - /reports : change report setting, or view current status. + - If done in pm, toggles your status. + - If in chat, toggles that chat's status. +""" + +SETTING_HANDLER = CommandHandler("reports", report_setting, pass_args=True) +REPORT_HANDLER = CommandHandler("report", report, filters=Filters.group) +ADMIN_REPORT_HANDLER = RegexHandler("(?i)@admin(s)?", report) + +dispatcher.add_handler(SETTING_HANDLER) +dispatcher.add_handler(REPORT_HANDLER, REPORT_GROUP) +dispatcher.add_handler(ADMIN_REPORT_HANDLER, REPORT_GROUP) + +__mod_name__ = "REPORTING" +__handlers__ = [(REPORT_HANDLER, REPORT_GROUP), (ADMIN_REPORT_HANDLER, REPORT_GROUP), (SETTING_HANDLER)] diff --git a/fire_bot/modules/rss.py b/fire_bot/modules/rss.py new file mode 100644 index 0000000..c8f132a --- /dev/null +++ b/fire_bot/modules/rss.py @@ -0,0 +1,244 @@ +import html +import re + +from feedparser import parse +from telegram import ParseMode, constants +from telegram.ext import CommandHandler + +from tg_bot import dispatcher, updater +from tg_bot.modules.helper_funcs.chat_status import user_admin +from tg_bot.modules.sql import rss_sql as sql + + +def show_url(bot, update, args): + tg_chat_id = str(update.effective_chat.id) + + if len(args) >= 1: + tg_feed_link = args[0] + link_processed = parse(tg_feed_link) + + if link_processed.bozo == 0: + feed_title = link_processed.feed.get("title", default="Unknown") + feed_description = "{}".format( + re.sub('<[^<]+?>', '', link_processed.feed.get("description", default="Unknown"))) + feed_link = link_processed.feed.get("link", default="Unknown") + + feed_message = "Feed Title: \n{}" \ + "\n\nFeed Description: \n{}" \ + "\n\nFeed Link: \n{}".format(html.escape(feed_title), + feed_description, + html.escape(feed_link)) + + if len(link_processed.entries) >= 1: + entry_title = link_processed.entries[0].get("title", default="Unknown") + entry_description = "{}".format( + re.sub('<[^<]+?>', '', link_processed.entries[0].get("description", default="Unknown"))) + entry_link = link_processed.entries[0].get("link", default="Unknown") + + entry_message = "\n\nEntry Title: \n{}" \ + "\n\nEntry Description: \n{}" \ + "\n\nEntry Link: \n{}".format(html.escape(entry_title), + entry_description, + html.escape(entry_link)) + final_message = feed_message + entry_message + + bot.send_message(chat_id=tg_chat_id, text=final_message, parse_mode=ParseMode.HTML) + else: + bot.send_message(chat_id=tg_chat_id, text=feed_message, parse_mode=ParseMode.HTML) + else: + update.effective_message.reply_text("This link is not an RSS Feed link") + else: + update.effective_message.reply_text("URL missing") + + +def list_urls(bot, update): + tg_chat_id = str(update.effective_chat.id) + + user_data = sql.get_urls(tg_chat_id) + + # this loops gets every link from the DB based on the filter above and appends it to the list + links_list = [row.feed_link for row in user_data] + + final_content = "\n\n".join(links_list) + + # check if the length of the message is too long to be posted in 1 chat bubble + if len(final_content) == 0: + bot.send_message(chat_id=tg_chat_id, text="This chat is not subscribed to any links") + elif len(final_content) <= constants.MAX_MESSAGE_LENGTH: + bot.send_message(chat_id=tg_chat_id, text="This chat is subscribed to the following links:\n" + final_content) + else: + bot.send_message(chat_id=tg_chat_id, parse_mode=ParseMode.HTML, + text="Warning: The message is too long to be sent") + + +@user_admin +def add_url(bot, update, args): + if len(args) >= 1: + chat = update.effective_chat + + tg_chat_id = str(update.effective_chat.id) + + tg_feed_link = args[0] + + link_processed = parse(tg_feed_link) + + # check if link is a valid RSS Feed link + if link_processed.bozo == 0: + if len(link_processed.entries[0]) >= 1: + tg_old_entry_link = link_processed.entries[0].link + else: + tg_old_entry_link = "" + + # gather the row which contains exactly that telegram group ID and link for later comparison + row = sql.check_url_availability(tg_chat_id, tg_feed_link) + + # check if there's an entry already added to DB by the same user in the same group with the same link + if row: + update.effective_message.reply_text("This URL has already been added") + else: + sql.add_url(tg_chat_id, tg_feed_link, tg_old_entry_link) + + update.effective_message.reply_text("Added URL to subscription") + else: + update.effective_message.reply_text("This link is not an RSS Feed link") + else: + update.effective_message.reply_text("URL missing") + + +@user_admin +def remove_url(bot, update, args): + if len(args) >= 1: + tg_chat_id = str(update.effective_chat.id) + + tg_feed_link = args[0] + + link_processed = parse(tg_feed_link) + + if link_processed.bozo == 0: + user_data = sql.check_url_availability(tg_chat_id, tg_feed_link) + + if user_data: + sql.remove_url(tg_chat_id, tg_feed_link) + + update.effective_message.reply_text("Removed URL from subscription") + else: + update.effective_message.reply_text("You haven't subscribed to this URL yet") + else: + update.effective_message.reply_text("This link is not an RSS Feed link") + else: + update.effective_message.reply_text("URL missing") + + +def rss_update(bot, job): + user_data = sql.get_all() + + # this loop checks for every row in the DB + for row in user_data: + row_id = row.id + tg_chat_id = row.chat_id + tg_feed_link = row.feed_link + + feed_processed = parse(tg_feed_link) + + tg_old_entry_link = row.old_entry_link + + new_entry_links = [] + new_entry_titles = [] + + # this loop checks for every entry from the RSS Feed link from the DB row + for entry in feed_processed.entries: + # check if there are any new updates to the RSS Feed from the old entry + if entry.link != tg_old_entry_link: + new_entry_links.append(entry.link) + new_entry_titles.append(entry.title) + else: + break + + # check if there's any new entries queued from the last check + if new_entry_links: + sql.update_url(row_id, new_entry_links) + else: + pass + + if len(new_entry_links) < 5: + # this loop sends every new update to each user from each group based on the DB entries + for link, title in zip(reversed(new_entry_links), reversed(new_entry_titles)): + final_message = "{}\n\n{}".format(html.escape(title), html.escape(link)) + + if len(final_message) <= constants.MAX_MESSAGE_LENGTH: + bot.send_message(chat_id=tg_chat_id, text=final_message, parse_mode=ParseMode.HTML) + else: + bot.send_message(chat_id=tg_chat_id, text="Warning: The message is too long to be sent", + parse_mode=ParseMode.HTML) + else: + for link, title in zip(reversed(new_entry_links[-5:]), reversed(new_entry_titles[-5:])): + final_message = "{}\n\n{}".format(html.escape(title), html.escape(link)) + + if len(final_message) <= constants.MAX_MESSAGE_LENGTH: + bot.send_message(chat_id=tg_chat_id, text=final_message, parse_mode=ParseMode.HTML) + else: + bot.send_message(chat_id=tg_chat_id, text="Warning: The message is too long to be sent", + parse_mode=ParseMode.HTML) + + bot.send_message(chat_id=tg_chat_id, parse_mode=ParseMode.HTML, + text="Warning: {} occurrences have been left out to prevent spam" + .format(len(new_entry_links) - 5)) + + +def rss_set(bot, job): + user_data = sql.get_all() + + # this loop checks for every row in the DB + for row in user_data: + row_id = row.id + tg_feed_link = row.feed_link + tg_old_entry_link = row.old_entry_link + + feed_processed = parse(tg_feed_link) + + new_entry_links = [] + new_entry_titles = [] + + # this loop checks for every entry from the RSS Feed link from the DB row + for entry in feed_processed.entries: + # check if there are any new updates to the RSS Feed from the old entry + if entry.link != tg_old_entry_link: + new_entry_links.append(entry.link) + new_entry_titles.append(entry.title) + else: + break + + # check if there's any new entries queued from the last check + if new_entry_links: + sql.update_url(row_id, new_entry_links) + else: + pass + + +__help__ = """ + - /addrss : add an RSS link to the subscriptions. + - /removerss : removes the RSS link from the subscriptions. + - /rss : shows the link's data and the last entry, for testing purposes. + - /listrss: shows the list of rss feeds that the chat is currently subscribed to. + +NOTE: In groups, only admins can add/remove RSS links to the group's subscription +""" + +__mod_name__ = "RSS FEED" + +job = updater.job_queue + +job_rss_set = job.run_once(rss_set, 5) +job_rss_update = job.run_repeating(rss_update, interval=60, first=60) +job_rss_set.enabled = True +job_rss_update.enabled = True + +SHOW_URL_HANDLER = CommandHandler("rss", show_url, pass_args=True) +ADD_URL_HANDLER = CommandHandler("addrss", add_url, pass_args=True) +REMOVE_URL_HANDLER = CommandHandler("removerss", remove_url, pass_args=True) +LIST_URLS_HANDLER = CommandHandler("listrss", list_urls) + +dispatcher.add_handler(SHOW_URL_HANDLER) +dispatcher.add_handler(ADD_URL_HANDLER) +dispatcher.add_handler(REMOVE_URL_HANDLER) +dispatcher.add_handler(LIST_URLS_HANDLER) diff --git a/fire_bot/modules/rules.py b/fire_bot/modules/rules.py new file mode 100644 index 0000000..45f0221 --- /dev/null +++ b/fire_bot/modules/rules.py @@ -0,0 +1,111 @@ +from typing import Optional + +from telegram import Message, Update, Bot, User +from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.error import BadRequest +from telegram.ext import CommandHandler, run_async, Filters +from telegram.utils.helpers import escape_markdown + +import tg_bot.modules.sql.rules_sql as sql +from tg_bot import dispatcher +from tg_bot.modules.helper_funcs.chat_status import user_admin +from tg_bot.modules.helper_funcs.string_handling import markdown_parser + + +@run_async +def get_rules(bot: Bot, update: Update): + chat_id = update.effective_chat.id + send_rules(update, chat_id) + + +# Do not async - not from a handler +def send_rules(update, chat_id, from_pm=False): + bot = dispatcher.bot + user = update.effective_user # type: Optional[User] + try: + chat = bot.get_chat(chat_id) + except BadRequest as excp: + if excp.message == "Chat not found" and from_pm: + bot.send_message(user.id, "The rules shortcut for this chat hasn't been set properly! Ask admins to " + "fix this.") + return + else: + raise + + rules = sql.get_rules(chat_id) + text = f"The rules for *{escape_markdown(chat.title)}* are:\n\n{rules}" + + if from_pm and rules: + bot.send_message(user.id, text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + elif from_pm: + bot.send_message(user.id, "The group admins haven't set any rules for this chat yet. " + "This probably doesn't mean it's lawless though...!") + elif rules: + update.effective_message.reply_text("Contact me in PM to get this group's rules.", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton(text="Rules", + url=f"t.me/{bot.username}?start={chat_id}")]])) + else: + update.effective_message.reply_text("The group admins haven't set any rules for this chat yet. " + "This probably doesn't mean it's lawless though...!") + + +@run_async +@user_admin +def set_rules(bot: Bot, update: Update): + chat_id = update.effective_chat.id + msg = update.effective_message # type: Optional[Message] + raw_text = msg.text + args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args + if len(args) == 2: + txt = args[1] + offset = len(txt) - len(raw_text) # set correct offset relative to command + markdown_rules = markdown_parser(txt, entities=msg.parse_entities(), offset=offset) + + sql.set_rules(chat_id, markdown_rules) + update.effective_message.reply_text("Successfully set rules for this group.") + + +@run_async +@user_admin +def clear_rules(bot: Bot, update: Update): + chat_id = update.effective_chat.id + sql.set_rules(chat_id, "") + update.effective_message.reply_text("Successfully cleared rules!") + + +def __stats__(): + return f"{sql.num_chats()} chats have rules set." + + +def __import_data__(chat_id, data): + # set chat rules + rules = data.get('info', {}).get('rules', "") + sql.set_rules(chat_id, rules) + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + return f"This chat has had it's rules set: `{bool(sql.get_rules(chat_id))}`" + + +__help__ = """ + - /rules: get the rules for this chat. + +*Admin only:* + - /setrules : set the rules for this chat. + - /clearrules: clear the rules for this chat. +""" + +__mod_name__ = "RULES" + +GET_RULES_HANDLER = CommandHandler("rules", get_rules, filters=Filters.group) +SET_RULES_HANDLER = CommandHandler("setrules", set_rules, filters=Filters.group) +RESET_RULES_HANDLER = CommandHandler("clearrules", clear_rules, filters=Filters.group) + +dispatcher.add_handler(GET_RULES_HANDLER) +dispatcher.add_handler(SET_RULES_HANDLER) +dispatcher.add_handler(RESET_RULES_HANDLER) diff --git a/fire_bot/modules/shout.py b/fire_bot/modules/shout.py new file mode 100644 index 0000000..b7f89b1 --- /dev/null +++ b/fire_bot/modules/shout.py @@ -0,0 +1,44 @@ +from typing import List + +from telegram import Update, Bot +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + + +@run_async +def shout(bot: Bot, update: Update, args: List[str]): + text = " ".join(args) + result = [] + result.append(' '.join([s for s in 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 + "```" + return update.effective_message.reply_text(msg, parse_mode="MARKDOWN") + + +__help__ = """ + A little piece of fun wording! Give a loud shout out in the chatroom. + + i.e /shout HELP, bot replies with huge coded HELP letters within the square. + + - /shout : write anything you want to give loud shout. + ``` + t e s t + e e + s s + t t + ``` +""" + +SHOUT_HANDLER = DisableAbleCommandHandler("shout", shout, pass_args=True) + +dispatcher.add_handler(SHOUT_HANDLER) + +__mod_name__ = "SHOUT" +__command_list__ = ["shout"] +__handlers__ = [SHOUT_HANDLER] diff --git a/fire_bot/modules/special.py b/fire_bot/modules/special.py new file mode 100644 index 0000000..863123d --- /dev/null +++ b/fire_bot/modules/special.py @@ -0,0 +1,165 @@ +from io import BytesIO +import html +from time import sleep +from typing import Optional, List +from telegram import TelegramError, Chat, Message +from telegram import Update, Bot +from telegram.error import BadRequest +from telegram import ParseMode +from telegram.ext import MessageHandler, Filters, CommandHandler +from telegram.ext.dispatcher import run_async +from telegram.utils.helpers import escape_markdown +from html import escape +from tg_bot.modules.helper_funcs.chat_status import is_user_ban_protected, bot_admin + +import tg_bot.modules.sql.users_sql as sql +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS, LOGGER +from tg_bot.modules.helper_funcs.filters import CustomFilters + +USERS_GROUP = 4 + + +def escape_html(word): + return escape(word) + + +@run_async +def quickscope(bot: Bot, update: Update, args: List[int]): + if args: + chat_id = str(args[1]) + to_kick = str(args[0]) + else: + update.effective_message.reply_text("You don't seem to be referring to a chat/user") + try: + bot.kick_chat_member(chat_id, to_kick) + update.effective_message.reply_text("Attempted banning " + to_kick + " from" + chat_id) + except BadRequest as excp: + update.effective_message.reply_text(excp.message + " " + to_kick) + + +@run_async +def quickunban(bot: Bot, update: Update, args: List[int]): + if args: + chat_id = str(args[1]) + to_kick = str(args[0]) + else: + update.effective_message.reply_text("You don't seem to be referring to a chat/user") + try: + bot.unban_chat_member(chat_id, to_kick) + update.effective_message.reply_text("Attempted unbanning " + to_kick + " from" + chat_id) + except BadRequest as excp: + update.effective_message.reply_text(excp.message + " " + to_kick) + + +@run_async +def banall(bot: Bot, update: Update, args: List[int]): + if args: + chat_id = str(args[0]) + all_mems = sql.get_chat_members(chat_id) + else: + chat_id = str(update.effective_chat.id) + all_mems = sql.get_chat_members(chat_id) + for mems in all_mems: + try: + bot.kick_chat_member(chat_id, mems.user) + update.effective_message.reply_text("Tried banning " + str(mems.user)) + sleep(0.1) + except BadRequest as excp: + update.effective_message.reply_text(excp.message + " " + str(mems.user)) + continue + + +@run_async +def snipe(bot: Bot, update: Update, args: List[str]): + try: + chat_id = str(args[0]) + del args[0] + except TypeError as excp: + update.effective_message.reply_text("Please give me a chat to echo to!") + to_send = " ".join(args) + if len(to_send) >= 2: + try: + bot.sendMessage(int(chat_id), str(to_send)) + except TelegramError: + LOGGER.warning("Couldn't send to group %s", str(chat_id)) + update.effective_message.reply_text("Couldn't send the message. Perhaps I'm not part of that group?") + + + +@bot_admin +def leavechat(bot: Bot, update: Update, args: List[int]): + if args: + chat_id = int(args[0]) + else: + update.effective_message.reply_text("You do not seem to be referring to a chat!") + try: + chat = bot.getChat(chat_id) + titlechat = bot.get_chat(chat_id).title + bot.sendMessage(chat_id, "`I Go Away!`") + bot.leaveChat(chat_id) + update.effective_message.reply_text("I left group {}".format(titlechat)) + + except BadRequest as excp: + if excp.message == "Chat not found": + update.effective_message.reply_text("It looks like I've been kicked out of the group :p") + else: + return + +@run_async +def slist(bot: Bot, update: Update): + message = update.effective_message + text1 = "My sudo users are:" + text2 = "My support users are:" + for user_id in SUDO_USERS: + try: + user = bot.get_chat(user_id) + name = "[{}](tg://user?id={})".format(user.first_name + (user.last_name or ""), user.id) + if user.username: + name = escape_html("@" + user.username) + text1 += "\n - `{}`".format(name) + except BadRequest as excp: + if excp.message == 'Chat not found': + text1 += "\n - ({}) - not found".format(user_id) + for user_id in SUPPORT_USERS: + try: + user = bot.get_chat(user_id) + name = "[{}](tg://user?id={})".format(user.first_name + (user.last_name or ""), user.id) + if user.username: + name = escape_html("@" + user.username) + text2 += "\n - `{}`".format(name) + except BadRequest as excp: + if excp.message == 'Chat not found': + text2 += "\n - ({}) - not found".format(user_id) + message.reply_text(text1 + "\n", parse_mode=ParseMode.MARKDOWN) + message.reply_text(text2 + "\n", parse_mode=ParseMode.MARKDOWN) + +__help__ = """ + +- /Stats: check bot's stats +- /chatlist: get chatlist +- /gbanlist: get gbanned users list +- /Gban : Global ban a user +- /Ungban : Ungban a user +- /gkick: Global kick a user +- /slist Gives a list of sudo and support users +- /zal type some text +""" + +__mod_name__ = "SPECIAL" + +SNIPE_HANDLER = CommandHandler("snipe", snipe, pass_args=True, filters=Filters.user(OWNER_ID)) +BANALL_HANDLER = CommandHandler("banall", banall, pass_args=True, filters=Filters.user(OWNER_ID)) +QUICKSCOPE_HANDLER = CommandHandler("quickscope", quickscope, pass_args=True, filters=CustomFilters.sudo_filter) +QUICKUNBAN_HANDLER = CommandHandler("quickunban", quickunban, pass_args=True, filters=CustomFilters.sudo_filter) + +LEAVECHAT_HANDLER = CommandHandler("leavechat", leavechat, pass_args=True, filters=Filters.user(OWNER_ID)) +SLIST_HANDLER = CommandHandler("slist", slist, + filters=CustomFilters.sudo_filter | CustomFilters.support_filter) + +dispatcher.add_handler(SNIPE_HANDLER) +dispatcher.add_handler(BANALL_HANDLER) +dispatcher.add_handler(QUICKSCOPE_HANDLER) +dispatcher.add_handler(QUICKUNBAN_HANDLER) + +dispatcher.add_handler(LEAVECHAT_HANDLER) +dispatcher.add_handler(SLIST_HANDLER) diff --git a/fire_bot/modules/speed_test.py b/fire_bot/modules/speed_test.py new file mode 100644 index 0000000..4b56682 --- /dev/null +++ b/fire_bot/modules/speed_test.py @@ -0,0 +1,56 @@ +import speedtest +from telegram import Update, Bot, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.ext import run_async, CallbackQueryHandler + +from tg_bot import dispatcher, DEV_USERS +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import dev_plus + +def convert(speed): + return round(int(speed)/1048576, 2) + + +@dev_plus +@run_async +def speedtestxyz(bot: Bot, update: Update): + buttons = [ + [InlineKeyboardButton("Image", callback_data="speedtest_image"), InlineKeyboardButton("Text", callback_data="speedtest_text")] + ] + update.effective_message.reply_text("Select SpeedTest Mode", + reply_markup=InlineKeyboardMarkup(buttons)) + + +@run_async +def speedtestxyz_callback(bot: Bot, update: Update): + query = update.callback_query + + if query.from_user.id in DEV_USERS: + msg = update.effective_message.edit_text('Runing a speedtest....') + speed = speedtest.Speedtest() + speed.get_best_server() + speed.download() + speed.upload() + replymsg = 'SpeedTest Results:' + + if query.data == 'speedtest_image': + speedtest_image = speed.results.share() + update.effective_message.reply_photo(photo=speedtest_image, caption=replymsg) + msg.delete() + + elif query.data == 'speedtest_text': + result = speed.results.dict() + replymsg += f"\nDownload: `{convert(result['download'])}Mb/s`\nUpload: `{convert(result['upload'])}Mb/s`\nPing: `{result['ping']}`" + update.effective_message.edit_text(replymsg, parse_mode=ParseMode.MARKDOWN) + else: + query.answer("You are required to join Heroes Association to use this command.") + + +SPEED_TEST_HANDLER = DisableAbleCommandHandler("speedtest", speedtestxyz) +SPEED_TEST_CALLBACKHANDLER = CallbackQueryHandler(speedtestxyz_callback, pattern='speedtest_.*') + +dispatcher.add_handler(SPEED_TEST_HANDLER) +dispatcher.add_handler(SPEED_TEST_CALLBACKHANDLER) + +__mod_name__ = "SPEED TEST" +__command_list__ = ["speedtest"] +__handlers__ = [SPEED_TEST_HANDLER, SPEED_TEST_CALLBACKHANDLER] diff --git a/fire_bot/modules/stickers.py b/fire_bot/modules/stickers.py new file mode 100644 index 0000000..41712b3 --- /dev/null +++ b/fire_bot/modules/stickers.py @@ -0,0 +1,244 @@ +import hashlib +import os +import math +import urllib.request as urllib + +from io import BytesIO +from PIL import Image + +from typing import Optional, List +from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from telegram import TelegramError +from telegram import Update, Bot +from telegram.ext import CommandHandler, run_async +from telegram.utils.helpers import escape_markdown + +from tg_bot import dispatcher + +from tg_bot.modules.disable import DisableAbleCommandHandler + + +@run_async +def stickerid(bot: Bot, update: Update): + msg = update.effective_message + if msg.reply_to_message and msg.reply_to_message.sticker: + update.effective_message.reply_text("Sticker ID:\n```" + + escape_markdown(msg.reply_to_message.sticker.file_id) + "```", + parse_mode=ParseMode.MARKDOWN) + else: + update.effective_message.reply_text("Please reply to a sticker to get its ID.") + + +@run_async +def getsticker(bot: Bot, update: Update): + 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 + newFile = bot.get_file(file_id) + newFile.download('sticker.png') + bot.send_document(chat_id, document=open('sticker.png', 'rb')) + os.remove("sticker.png") + else: + update.effective_message.reply_text("Please reply to a sticker for me to upload its PNG.") + + +@run_async +def kang(bot: Bot, update: Update, args: List[str]): + msg = update.effective_message + user = update.effective_user + packnum = 0 + packname = "a" + str(user.id) + "_by_"+bot.username + packname_found = 0 + max_stickers = 120 + while packname_found == 0: + try: + stickerset = bot.get_sticker_set(packname) + if len(stickerset.stickers) >= max_stickers: + packnum += 1 + packname = "a" + str(packnum) + "_" + str(user.id) + "_by_"+bot.username + else: + packname_found = 1 + except TelegramError as e: + if e.message == "Stickerset_invalid": + packname_found = 1 + stolensticker = "stolensticker.png" + if msg.reply_to_message: + if msg.reply_to_message.sticker: + 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 + else: + msg.reply_text("Yea, I can't kang that.") + stolen_file = bot.get_file(file_id) + stolen_file.download('stolensticker.png') + 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 = "🤔" + try: + im = Image.open(stolensticker) + 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(stolensticker, "PNG") + bot.add_sticker_to_set(user_id=user.id, name=packname, + png_sticker=open('stolensticker.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: + msg.reply_text("I can only kang images, dude.") + print(e) + return + except TelegramError as e: + if e.message == "Stickerset_invalid": + makepack_internal(msg, user, open('stolensticker.png', 'rb'), sticker_emoji, bot, packname, packnum) + elif e.message == "Sticker_png_dimensions": + im.save(stolensticker, "PNG") + bot.add_sticker_to_set(user_id=user.id, name=packname, + png_sticker=open('stolensticker.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.") + 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: + try: + try: + urlemoji = msg.text.split(" ") + png_sticker = urlemoji[1] + sticker_emoji = urlemoji[2] + except IndexError: + sticker_emoji = "🤔" + urllib.urlretrieve(png_sticker, stolensticker) + im = Image.open(stolensticker) + 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) + im.save(stolensticker, "PNG") + msg.reply_photo(photo=open('stolensticker.png', 'rb')) + bot.add_sticker_to_set(user_id=user.id, name=packname, + png_sticker=open('stolensticker.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: + msg.reply_text("I can only kang images, dude.") + print(e) + return + except TelegramError as e: + if e.message == "Stickerset_invalid": + makepack_internal(msg, user, open('stolensticker.png', 'rb'), sticker_emoji, bot, packname, packnum) + elif e.message == "Sticker_png_dimensions": + im.save(stolensticker, "PNG") + bot.add_sticker_to_set(user_id=user.id, name=packname, + png_sticker=open('stolensticker.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.") + 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 to your pack!\nOh by the way, here are your packs:\n" + if packnum > 0: + firstpackname = "a" + str(user.id) + "_by_"+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) + if os.path.isfile("stolensticker.png"): + os.remove("stolensticker.png") + + +def makepack_internal(msg, user, png_sticker, emoji, bot, packname, packnum): + name = user.first_name + name = name[:50] + try: + extra_version = "" + if packnum > 0: + extra_version = " " + str(packnum) + success = bot.create_new_sticker_set(user.id, packname, f"{name}'s Sticker Pack" + extra_version, + png_sticker=png_sticker, + emojis=emoji) + except TelegramError as e: + 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, + parse_mode=ParseMode.MARKDOWN) + elif e.message == "Peer_id_invalid": + msg.reply_text("Contact me in PM first.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton( + text="Start", url=f"t.me/{bot.username}")]])) + elif e.message == "Internal Server Error: created sticker set not found (500)": + msg.reply_text("Sticker pack successfully created! Get it [here](t.me/addstickers/%s)" % packname, + parse_mode=ParseMode.MARKDOWN) + return + + if success: + msg.reply_text("Sticker pack successfully created! Get it [here](t.me/addstickers/%s)" % packname, + parse_mode=ParseMode.MARKDOWN) + else: + msg.reply_text("Failed to create sticker pack.") + + +__help__ = """ +- /stickerid: reply to a sticker to get its ID. +- /getsticker: reply to a sticker to get the raw PNG image. +- /kang: reply to a sticker or image to add it to your pack. +""" + +__mod_name__ = "STICKERS" +STICKERID_HANDLER = DisableAbleCommandHandler("stickerid", stickerid) +GETSTICKER_HANDLER = DisableAbleCommandHandler("getsticker", getsticker) +KANG_HANDLER = DisableAbleCommandHandler("kang", kang, pass_args=True, admin_ok=False) + +dispatcher.add_handler(STICKERID_HANDLER) +dispatcher.add_handler(GETSTICKER_HANDLER) +dispatcher.add_handler(KANG_HANDLER) diff --git a/fire_bot/modules/sudo.py b/fire_bot/modules/sudo.py new file mode 100644 index 0000000..0313631 --- /dev/null +++ b/fire_bot/modules/sudo.py @@ -0,0 +1,84 @@ +import html + +from typing import List + +from telegram import Update, Bot +from telegram.ext import CommandHandler, Filters +from telegram.ext.dispatcher import run_async + +from tg_bot import dispatcher, SUDO_USERS, OWNER_USERNAME, OWNER_ID +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.helper_funcs.chat_status import bot_admin + + +@bot_admin +@run_async +def sudopromote(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + banner = update.effective_user + user_id = extract_user(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return "" + + if int(user_id) == OWNER_ID: + message.reply_text("The specified user is my owner! No need add him to SUDO_USERS list!") + return "" + + if int(user_id) in SUDO_USERS: + message.reply_text("The user is already a sudo user.") + return "" + + with open("sudo_users.txt","a") as file: + file.write(str(user_id) + "\n") + + SUDO_USERS.append(user_id) + message.reply_text("Succefully added to SUDO user list!") + + return "" + +@bot_admin +@run_async +def sudodemote(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + user_id = extract_user(message, args) + + if not user_id: + message.reply_text("You don't seem to be referring to a user.") + return "" + + if int(user_id) == OWNER_ID: + message.reply_text("The specified user is my owner! I won't remove him from SUDO_USERS list!") + return "" + + if user_id not in SUDO_USERS: + message.reply_text("{} is not a sudo user".format(user_id)) + return "" + + users = [line.rstrip('\n') for line in open("sudo_users.txt")] + + with open("sudo_users.txt","w") as file: + for user in users: + if not int(user) == user_id: + file.write(str(user) + "\n") + + SUDO_USERS.remove(user_id) + message.reply_text("Succefully removed from SUDO user list!") + + return "" + + +__help__ = """ +*Bot owner only:* + - /sudopromote: promotes the user to SUDO USER + - /sudodemote: demotes the user from SUDO USER +""" + +__mod_name__ = "SUDO" + +SUDOPROMOTE_HANDLER = CommandHandler("sudopromote", sudopromote, pass_args=True, filters=Filters.user(OWNER_ID)) +SUDODEMOTE_HANDLER = CommandHandler("sudodemote", sudodemote, pass_args=True, filters=Filters.user(OWNER_ID)) + +dispatcher.add_handler(SUDOPROMOTE_HANDLER) +dispatcher.add_handler(SUDODEMOTE_HANDLER) diff --git a/fire_bot/modules/tts.py b/fire_bot/modules/tts.py new file mode 100644 index 0000000..37266f5 --- /dev/null +++ b/fire_bot/modules/tts.py @@ -0,0 +1,39 @@ +from telegram import ChatAction +from gtts import gTTS +import html +import urllib.request +import re +import json +from datetime import datetime +from typing import Optional, List +import time +import requests +from telegram import Message, Chat, Update, Bot, MessageEntity +from telegram import ParseMode +from telegram.ext import CommandHandler, run_async, Filters +from telegram.utils.helpers import escape_markdown, mention_html +from tg_bot import dispatcher +from tg_bot.__main__ import STATS +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.extraction import extract_user + +def tts(bot: Bot, update: Update, args): + current_time = datetime.strftime(datetime.now(), "%d.%m.%Y %H:%M:%S") + filename = datetime.now().strftime("%d%m%y-%H%M%S%f") + reply = " ".join(args) + update.message.chat.send_action(ChatAction.RECORD_AUDIO) + lang="ml" + tts = gTTS(reply, 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(reply, lang) + tts.save("k.mp3") + with open("k.mp3", "rb") as speech: + update.message.reply_voice(speech, quote=False) + +dispatcher.add_handler(CommandHandler('tts', tts, pass_args=True)) diff --git a/fire_bot/modules/ud.py b/fire_bot/modules/ud.py new file mode 100644 index 0000000..bf1207b --- /dev/null +++ b/fire_bot/modules/ud.py @@ -0,0 +1,32 @@ +import requests +from telegram import Update, Bot, ParseMode +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + + +@run_async +def ud(bot: Bot, update: Update): + message = update.effective_message + text = message.text[len('/ud '):] + results = requests.get(f'http://api.urbandictionary.com/v0/define?term={text}').json() + try: + reply_text = f'*{text}*\n\n{results["list"][0]["definition"]}\n\n_{results["list"][0]["example"]}_' + except: + reply_text = "No results found." + message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN) + + +__help__ = """ + - /ud : Type the word or expression you want to search use. + - /urban : Same as /ud +""" + +UD_HANDLER = DisableAbleCommandHandler(["ud", "urban"], ud) + +dispatcher.add_handler(UD_HANDLER) + +__mod_name__ = "URBAN DICTIONARY" +__command_list__ = ["ud", "urban"] +__handlers__ = [UD_HANDLER] diff --git a/fire_bot/modules/userinfo.py b/fire_bot/modules/userinfo.py new file mode 100644 index 0000000..9f543c1 --- /dev/null +++ b/fire_bot/modules/userinfo.py @@ -0,0 +1,148 @@ +import html +from typing import List + +from telegram import Bot, Update, ParseMode, MAX_MESSAGE_LENGTH +from telegram.ext.dispatcher import run_async +from telegram.utils.helpers import escape_markdown + +import tg_bot.modules.sql.userinfo_sql as sql +from tg_bot import dispatcher, SUDO_USERS, DEV_USERS +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.extraction import extract_user + + +@run_async +def about_me(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + user_id = extract_user(message, args) + + if user_id: + user = bot.get_chat(user_id) + else: + user = message.from_user + + info = sql.get_user_me_info(user.id) + + if info: + update.effective_message.reply_text(f"*{user.first_name}*:\n{escape_markdown(info)}", + parse_mode=ParseMode.MARKDOWN) + elif message.reply_to_message: + username = message.reply_to_message.from_user.first_name + update.effective_message.reply_text(f"{username} hasn't set an info message about themselves yet!") + else: + update.effective_message.reply_text("You haven't set an info message about yourself yet!") + + +@run_async +def set_about_me(bot: Bot, update: Update): + message = update.effective_message + user_id = message.from_user.id + if message.reply_to_message: + repl_message = message.reply_to_message + repl_user_id = repl_message.from_user.id + if repl_user_id == bot.id and (user_id in SUDO_USERS or user_id in DEV_USERS): + user_id = repl_user_id + + text = message.text + info = text.split(None, 1) + + if len(info) == 2: + if len(info[1]) < MAX_MESSAGE_LENGTH // 4: + sql.set_user_me_info(user_id, info[1]) + if user_id == bot.id: + message.reply_text("Updated my info!") + else: + message.reply_text("Updated your info!") + else: + message.reply_text( + "The info needs to be under {} characters! You have {}.".format(MAX_MESSAGE_LENGTH // 4, len(info[1]))) + + +@run_async +def about_bio(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + + user_id = extract_user(message, args) + if user_id: + user = bot.get_chat(user_id) + else: + user = message.from_user + + info = sql.get_user_bio(user.id) + + if info: + update.effective_message.reply_text("*{}*:\n{}".format(user.first_name, escape_markdown(info)), + parse_mode=ParseMode.MARKDOWN) + elif message.reply_to_message: + username = user.first_name + update.effective_message.reply_text(f"{username} hasn't had a message set about themselves yet!") + else: + update.effective_message.reply_text("You haven't had a bio set about yourself yet!") + + +@run_async +def set_about_bio(bot: Bot, update: Update): + message = update.effective_message + sender_id = update.effective_user.id + + if message.reply_to_message: + repl_message = message.reply_to_message + user_id = repl_message.from_user.id + + if user_id == message.from_user.id: + message.reply_text("Ha, you can't set your own bio! You're at the mercy of others here...") + return + + if user_id == bot.id and sender_id not in SUDO_USERS and sender_id not in DEV_USERS: + message.reply_text("Erm... yeah, I only trust sudo users or developers to set my bio.") + return + + text = message.text + bio = text.split(None, 1) # use python's maxsplit to only remove the cmd, hence keeping newlines. + + if len(bio) == 2: + if len(bio[1]) < MAX_MESSAGE_LENGTH // 4: + sql.set_user_bio(user_id, bio[1]) + message.reply_text("Updated {}'s bio!".format(repl_message.from_user.first_name)) + else: + message.reply_text( + "A bio needs to be under {} characters! You tried to set {}.".format( + MAX_MESSAGE_LENGTH // 4, len(bio[1]))) + else: + message.reply_text("Reply to someone's message to set their bio!") + + +def __user_info__(user_id): + bio = html.escape(sql.get_user_bio(user_id) or "") + me = html.escape(sql.get_user_me_info(user_id) or "") + if bio and me: + return f"\nAbout user:\n{me}\nWhat others say:\n{bio}\n" + elif bio: + return f"\nWhat others say:\n{bio}\n" + elif me: + return f"\nAbout user:\n{me}\n" + else: + return "\n" + + +__help__ = """ + - /setbio : while replying, will save another user's bio + - /bio: will get your or another user's bio. This cannot be set by yourself. + - /setme : will set your info + - /me: will get your or another user's info +""" + +SET_BIO_HANDLER = DisableAbleCommandHandler("setbio", set_about_bio) +GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio, pass_args=True) + +SET_ABOUT_HANDLER = DisableAbleCommandHandler("setme", set_about_me) +GET_ABOUT_HANDLER = DisableAbleCommandHandler("me", about_me, pass_args=True) + +dispatcher.add_handler(SET_BIO_HANDLER) +dispatcher.add_handler(GET_BIO_HANDLER) +dispatcher.add_handler(SET_ABOUT_HANDLER) +dispatcher.add_handler(GET_ABOUT_HANDLER) + +__mod_name__ = "BIOS & ABOUTS" +__command_list__ = ["setbio", "bio", "setme", "me"] +__handlers__ = [SET_BIO_HANDLER, GET_BIO_HANDLER, SET_ABOUT_HANDLER, GET_ABOUT_HANDLER] diff --git a/fire_bot/modules/users.py b/fire_bot/modules/users.py new file mode 100644 index 0000000..2d71f3d --- /dev/null +++ b/fire_bot/modules/users.py @@ -0,0 +1,131 @@ +from io import BytesIO +from time import sleep + +from telegram import Bot, Update, TelegramError +from telegram.error import BadRequest +from telegram.ext import CommandHandler, MessageHandler, Filters, run_async + +import tg_bot.modules.sql.users_sql as sql + +from tg_bot import dispatcher, OWNER_ID, LOGGER, DEV_USERS +from tg_bot.modules.helper_funcs.chat_status import sudo_plus, dev_plus + +USERS_GROUP = 4 +DEV_AND_MORE = DEV_USERS.append(int(OWNER_ID)) + + +def get_user_id(username): + # ensure valid userid + if len(username) <= 5: + return None + + if username.startswith('@'): + username = username[1:] + + users = sql.get_userid_by_name(username) + + if not users: + return None + + elif len(users) == 1: + return users[0].user_id + + else: + for user_obj in users: + try: + userdat = dispatcher.bot.get_chat(user_obj.user_id) + if userdat.username == username: + return userdat.id + + except BadRequest as excp: + if excp.message == 'Chat not found': + pass + else: + LOGGER.exception("Error extracting user ID") + + return None + + +@run_async +@dev_plus +def broadcast(bot: Bot, update: Update): + + to_send = update.effective_message.text.split(None, 1) + + if len(to_send) >= 2: + chats = sql.get_all_chats() or [] + failed = 0 + for chat in chats: + try: + bot.sendMessage( + int(chat.chat_id), + to_send[1], + parse_mode="MARKDOWN" + ) + sleep(0.1) + except RetryAfter as e: + sleep(e.retry_after) + except TelegramError: + failed += 1 + LOGGER.warning("Couldn't send broadcast to %s, group name %s", str(chat.chat_id), str(chat.chat_name)) + + update.effective_message.reply_text( + f"Broadcast complete. {failed} groups failed to receive the message, probably due to being kicked.") + + +@run_async +def log_user(bot: Bot, update: Update): + 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: + sql.update_user(msg.reply_to_message.from_user.id, + msg.reply_to_message.from_user.username, + chat.id, + chat.title) + + if msg.forward_from: + sql.update_user(msg.forward_from.id, + msg.forward_from.username) + + +@run_async +@sudo_plus +def chats(bot: Bot, update: Update): + + all_chats = sql.get_all_chats() or [] + chatfile = 'List of chats.\n' + for chat in all_chats: + chatfile += f"{chat.chat_name} - ({chat.chat_id})\n" + + with BytesIO(str.encode(chatfile)) as output: + output.name = "chatlist.txt" + update.effective_message.reply_document(document=output, filename="chatlist.txt", + caption="Here is the list of chats in my Hit List.") + + +def __stats__(): + return f"{sql.num_users()} users, across {sql.num_chats()} chats" + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +__help__ = "" # no help string + +BROADCAST_HANDLER = CommandHandler("broadcast", broadcast) +USER_HANDLER = MessageHandler(Filters.all & Filters.group, log_user) +CHATLIST_HANDLER = CommandHandler("chatlist", chats) + +dispatcher.add_handler(USER_HANDLER, USERS_GROUP) +dispatcher.add_handler(BROADCAST_HANDLER) +dispatcher.add_handler(CHATLIST_HANDLER) + +__mod_name__ = "USERS" +__handlers__ = [(USER_HANDLER, USERS_GROUP), BROADCAST_HANDLER, CHATLIST_HANDLER] diff --git a/fire_bot/modules/warns.py b/fire_bot/modules/warns.py new file mode 100644 index 0000000..01405c9 --- /dev/null +++ b/fire_bot/modules/warns.py @@ -0,0 +1,427 @@ +import html +import re +from typing import Optional, List + +import telegram +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, User, CallbackQuery +from telegram import Message, Chat, Update, Bot +from telegram.error import BadRequest +from telegram.ext import CommandHandler, run_async, DispatcherHandlerStop, MessageHandler, Filters, CallbackQueryHandler +from telegram.utils.helpers import mention_html + +from tg_bot import dispatcher, BAN_STICKER, WHITELIST_USERS, TIGER_USERS +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.chat_status import (is_user_admin, bot_admin, user_admin_no_reply, user_admin, + can_restrict) +from tg_bot.modules.helper_funcs.extraction import extract_text, extract_user_and_text, extract_user +from tg_bot.modules.helper_funcs.filters import CustomFilters +from tg_bot.modules.helper_funcs.misc import split_message +from tg_bot.modules.helper_funcs.string_handling import split_quotes +from tg_bot.modules.log_channel import loggable +from tg_bot.modules.sql import warns_sql as sql + +WARN_HANDLER_GROUP = 9 +CURRENT_WARNING_FILTER_STRING = "Current warning filters in this chat:\n" + + +# Not async +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!") + return + + if user.id in TIGER_USERS: + if warner: + message.reply_text("Tigers cant be warned.") + else: + message.reply_text("Tiger triggered an auto warn filter!\n I can't warn tigers but they should avoid abusing this.") + return + + if user.id in WHITELIST_USERS: + if warner: + message.reply_text("Wolf disasters are warn immune.") + else: + message.reply_text("Wolf Disaster triggered an auto warn filter!\n I can't warn wolves but they should avoid abusing this.") + return + + if warner: + warner_tag = mention_html(warner.id, warner.first_name) + else: + warner_tag = "Automated warn filter." + + limit, soft_warn = sql.get_warn_setting(chat.id) + num_warns, reasons = sql.warn_user(user.id, chat.id, reason) + if num_warns >= limit: + sql.reset_warns(user.id, chat.id) + if soft_warn: # punch + chat.unban_member(user.id) + reply = f"{limit} warnings, *Punches {mention_html(user.id, user.first_name)} with a normal punch!* " + + else: # ban + chat.kick_member(user.id) + reply = f"{limit} warnings, *Punches {mention_html(user.id, user.first_name)} with a Serious Punch* " + + for warn_reason in reasons: + reply += f"\n - {html.escape(warn_reason)}" + + message.bot.send_sticker(chat.id, BAN_STICKER) # Saitama's sticker + keyboard = [] + log_reason = (f"{html.escape(chat.title)}:\n" + f"#WARN_BAN\n" + f"Admin: {warner_tag}\n" + f"User: {mention_html(user.id, user.first_name)}\n" + f"Reason: {reason}\n" + f"Counts: {num_warns}/{limit}") + + else: + keyboard = InlineKeyboardMarkup([{InlineKeyboardButton("Remove warn", + callback_data="rm_warn({})".format(user.id))}]) + + reply = f"{mention_html(user.id, user.first_name)} has {num_warns}/{limit} warnings... watch out!" + if reason: + reply += f"\nReason for last warn:\n{html.escape(reason)}" + + log_reason = (f"{html.escape(chat.title)}:\n" + f"#WARN\n" + f"Admin: {warner_tag}\n" + f"User: {mention_html(user.id, user.first_name)}\n" + f"Reason: {reason}\n" + f"Counts: {num_warns}/{limit}") + + try: + message.reply_text(reply, reply_markup=keyboard, parse_mode=ParseMode.HTML) + except BadRequest as excp: + if excp.message == "Reply message not found": + # Do not reply + message.reply_text(reply, reply_markup=keyboard, parse_mode=ParseMode.HTML, quote=False) + else: + raise + return log_reason + + +@run_async +@user_admin_no_reply +@bot_admin +@loggable +def button(bot: Bot, update: Update) -> str: + query: Optional[CallbackQuery] = update.callback_query + user: Optional[User] = update.effective_user + match = re.match(r"rm_warn\((.+?)\)", query.data) + if match: + user_id = match.group(1) + chat: Optional[Chat] = update.effective_chat + res = sql.remove_warn(user_id, chat.id) + if res: + update.effective_message.edit_text( + "Warn removed by {}.".format(mention_html(user.id, user.first_name)), + parse_mode=ParseMode.HTML) + user_member = chat.get_member(user_id) + return (f"{html.escape(chat.title)}:\n" + f"#UNWARN\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(user_member.user.id, user_member.user.first_name)}") + else: + update.effective_message.edit_text(f"User already has no warns.", parse_mode=ParseMode.HTML) + + return "" + + +@run_async +@user_admin +@can_restrict +@loggable +def warn_user(bot: Bot, update: Update, args: List[str]) -> str: + message: Optional[Message] = update.effective_message + chat: Optional[Chat] = update.effective_chat + warner: Optional[User] = update.effective_user + + user_id, reason = extract_user_and_text(message, args) + + if user_id: + if message.reply_to_message and message.reply_to_message.from_user.id == user_id: + return warn(message.reply_to_message.from_user, chat, reason, message.reply_to_message, warner) + else: + return warn(chat.get_member(user_id).user, chat, reason, message, warner) + else: + message.reply_text("That looks like an invalid User ID to me.") + return "" + + +@run_async +@user_admin +@bot_admin +@loggable +def reset_warns(bot: Bot, update: Update, args: List[str]) -> str: + 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.reset_warns(user_id, chat.id) + message.reply_text("Warns have been reset!") + warned = chat.get_member(user_id).user + return (f"{html.escape(chat.title)}:\n" + f"#RESETWARNS\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"User: {mention_html(warned.id, warned.first_name)}") + else: + message.reply_text("No user has been designated!") + return "" + + +@run_async +def warns(bot: Bot, update: Update, args: List[str]): + message: Optional[Message] = update.effective_message + chat: Optional[Chat] = update.effective_chat + user_id = extract_user(message, args) or update.effective_user.id + result = sql.get_warns(user_id, chat.id) + + if result and result[0] != 0: + num_warns, reasons = result + limit, soft_warn = sql.get_warn_setting(chat.id) + + if reasons: + text = f"This user has {num_warns}/{limit} warns, for the following reasons:" + for reason in reasons: + text += f"\n - {reason}" + + msgs = split_message(text) + for msg in msgs: + update.effective_message.reply_text(msg) + else: + update.effective_message.reply_text(f"User has {num_warns}/{limit} warns, but no reasons for any of them.") + else: + update.effective_message.reply_text("This user doesn't have any warns!") + + +# Dispatcher handler stop - do not async +@user_admin +def add_warn_filter(bot: Bot, update: Update): + chat: Optional[Chat] = update.effective_chat + msg: Optional[Message] = update.effective_message + + args = msg.text.split(None, 1) # use python's maxsplit to separate Cmd, keyword, and reply_text + + if len(args) < 2: + return + + extracted = split_quotes(args[1]) + + if len(extracted) >= 2: + # set trigger -> lower, so as to avoid adding duplicate filters with different cases + keyword = extracted[0].lower() + content = extracted[1] + + else: + return + + # Note: perhaps handlers can be removed somehow using sql.get_chat_filters + for handler in dispatcher.handlers.get(WARN_HANDLER_GROUP, []): + if handler.filters == (keyword, chat.id): + dispatcher.remove_handler(handler, WARN_HANDLER_GROUP) + + sql.add_warn_filter(chat.id, keyword, content) + + update.effective_message.reply_text(f"Warn handler added for '{keyword}'!") + raise DispatcherHandlerStop + + +@user_admin +def remove_warn_filter(bot: Bot, update: Update): + chat: Optional[Chat] = update.effective_chat + msg: Optional[Message] = update.effective_message + + args = msg.text.split(None, 1) # use python's maxsplit to separate Cmd, keyword, and reply_text + + if len(args) < 2: + return + + extracted = split_quotes(args[1]) + + if len(extracted) < 1: + return + + to_remove = extracted[0] + + chat_filters = sql.get_chat_warn_triggers(chat.id) + + if not chat_filters: + msg.reply_text("No warning filters are active here!") + return + + for filt in chat_filters: + if filt == to_remove: + sql.remove_warn_filter(chat.id, to_remove) + msg.reply_text("Okay, I'll stop warning people for that.") + raise DispatcherHandlerStop + + msg.reply_text("That's not a current warning filter - run /warnlist for all active warning filters.") + + +@run_async +def list_warn_filters(bot: Bot, update: Update): + chat: Optional[Chat] = update.effective_chat + all_handlers = sql.get_chat_warn_triggers(chat.id) + + if not all_handlers: + update.effective_message.reply_text("No warning filters are active here!") + return + + filter_list = CURRENT_WARNING_FILTER_STRING + for keyword in all_handlers: + entry = f" - {html.escape(keyword)}\n" + if len(entry) + len(filter_list) > telegram.MAX_MESSAGE_LENGTH: + update.effective_message.reply_text(filter_list, parse_mode=ParseMode.HTML) + filter_list = entry + else: + filter_list += entry + + if not filter_list == CURRENT_WARNING_FILTER_STRING: + update.effective_message.reply_text(filter_list, parse_mode=ParseMode.HTML) + + +@run_async +@loggable +def reply_filter(bot: Bot, update: Update) -> str: + chat: Optional[Chat] = update.effective_chat + message: Optional[Message] = update.effective_message + + chat_warn_filters = sql.get_chat_warn_triggers(chat.id) + to_match = extract_text(message) + if not to_match: + return "" + + for keyword in chat_warn_filters: + pattern = r"( |^|[^\w])" + re.escape(keyword) + r"( |$|[^\w])" + if re.search(pattern, to_match, flags=re.IGNORECASE): + user: Optional[User] = update.effective_user + warn_filter = sql.get_warn_filter(chat.id, keyword) + return warn(user, chat, warn_filter.reply, message) + return "" + + +@run_async +@user_admin +@loggable +def set_warn_limit(bot: Bot, update: Update, args: List[str]) -> str: + chat: Optional[Chat] = update.effective_chat + user: Optional[User] = update.effective_user + msg: Optional[Message] = update.effective_message + + if args: + if args[0].isdigit(): + if int(args[0]) < 3: + msg.reply_text("The minimum warn limit is 3!") + else: + sql.set_warn_limit(chat.id, int(args[0])) + msg.reply_text("Updated the warn limit to {}".format(args[0])) + return (f"{html.escape(chat.title)}:\n" + f"#SET_WARN_LIMIT\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Set the warn limit to {args[0]}") + else: + msg.reply_text("Give me a number as an arg!") + else: + limit, soft_warn = sql.get_warn_setting(chat.id) + + msg.reply_text("The current warn limit is {}".format(limit)) + return "" + + +@run_async +@user_admin +def set_warn_strength(bot: Bot, update: Update, args: List[str]): + chat: Optional[Chat] = update.effective_chat + user: Optional[User] = update.effective_user + msg: Optional[Message] = update.effective_message + + if args: + if args[0].lower() in ("on", "yes"): + sql.set_warn_strength(chat.id, False) + msg.reply_text("Too many warns will now result in a Ban!") + return (f"{html.escape(chat.title)}:\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Has enabled strong warns. Users will be seriously punched.(banned)") + + elif args[0].lower() in ("off", "no"): + sql.set_warn_strength(chat.id, True) + msg.reply_text("Too many warns will now result in a normal punch! Users will be able to join again after.") + return (f"{html.escape(chat.title)}:\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Has disabled strong punches. I will use normal punch on users.") + + else: + msg.reply_text("I only understand on/yes/no/off!") + else: + limit, soft_warn = sql.get_warn_setting(chat.id) + if soft_warn: + msg.reply_text("Warns are currently set to *punch* users when they exceed the limits.", + parse_mode=ParseMode.MARKDOWN) + else: + msg.reply_text("Warns are currently set to *Ban* users when they exceed the limits.", + parse_mode=ParseMode.MARKDOWN) + return "" + + +def __stats__(): + return (f"{sql.num_warns()} overall warns, across {sql.num_warn_chats()} chats.\n" + f"{sql.num_warn_filters()} warn filters, across {sql.num_warn_filter_chats()} chats.") + + +def __import_data__(chat_id, data): + for user_id, count in data.get('warns', {}).items(): + for x in range(int(count)): + sql.warn_user(user_id, chat_id) + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + num_warn_filters = sql.num_warn_chat_filters(chat_id) + limit, soft_warn = sql.get_warn_setting(chat_id) + return (f"This chat has `{num_warn_filters}` warn filters. " + f"It takes `{limit}` warns before the user gets *{'kicked' if soft_warn else 'banned'}*.") + + +__help__ = """ + - /warns : get a user's number, and reason, of warns. + - /warnlist: list of all current warning filters + +*Admins only:* + - /warn : warn a user. After 3 warns, the user will be banned from the group. 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`. + - /nowarn : stop a warning filter + - /warnlimit : set the warning limit + - /strongwarn : If set to on, exceeding the warn limit will result in a ban. Else, will just punch. +""" + +__mod_name__ = "WARNINGS" + +WARN_HANDLER = CommandHandler("warn", warn_user, pass_args=True, filters=Filters.group) +RESET_WARN_HANDLER = CommandHandler(["resetwarn", "resetwarns"], reset_warns, pass_args=True, filters=Filters.group) +CALLBACK_QUERY_HANDLER = CallbackQueryHandler(button, pattern=r"rm_warn") +MYWARNS_HANDLER = DisableAbleCommandHandler("warns", warns, pass_args=True, filters=Filters.group) +ADD_WARN_HANDLER = CommandHandler("addwarn", add_warn_filter, filters=Filters.group) +RM_WARN_HANDLER = CommandHandler(["nowarn", "stopwarn"], remove_warn_filter, filters=Filters.group) +LIST_WARN_HANDLER = DisableAbleCommandHandler(["warnlist", "warnfilters"], list_warn_filters, filters=Filters.group, + admin_ok=True) +WARN_FILTER_HANDLER = MessageHandler(CustomFilters.has_text & Filters.group, reply_filter) +WARN_LIMIT_HANDLER = CommandHandler("warnlimit", set_warn_limit, pass_args=True, filters=Filters.group) +WARN_STRENGTH_HANDLER = CommandHandler("strongwarn", set_warn_strength, pass_args=True, filters=Filters.group) + +dispatcher.add_handler(WARN_HANDLER) +dispatcher.add_handler(CALLBACK_QUERY_HANDLER) +dispatcher.add_handler(RESET_WARN_HANDLER) +dispatcher.add_handler(MYWARNS_HANDLER) +dispatcher.add_handler(ADD_WARN_HANDLER) +dispatcher.add_handler(RM_WARN_HANDLER) +dispatcher.add_handler(LIST_WARN_HANDLER) +dispatcher.add_handler(WARN_LIMIT_HANDLER) +dispatcher.add_handler(WARN_STRENGTH_HANDLER) +dispatcher.add_handler(WARN_FILTER_HANDLER, WARN_HANDLER_GROUP) diff --git a/fire_bot/modules/weather.py b/fire_bot/modules/weather.py new file mode 100644 index 0000000..603bdeb --- /dev/null +++ b/fire_bot/modules/weather.py @@ -0,0 +1,71 @@ +import pyowm +from telegram import Message, Chat, Update, Bot +from telegram.ext import run_async + +from tg_bot import dispatcher, updater, API_WEATHER +from tg_bot.modules.disable import DisableAbleCommandHandler + +@run_async +def weather(bot, update, args): + if len(args) == 0: + update.effective_message.reply_text("Write a location to check the weather.") + return + + location = " ".join(args) + if location.lower() == bot.first_name.lower(): + update.effective_message.reply_text("I will keep an eye on both happy and sad times!") + bot.send_sticker(update.effective_chat.id, BAN_STICKER) + return + + try: + owm = pyowm.OWM(API_WEATHER) + observation = owm.weather_at_place(location) + getloc = observation.get_location() + thelocation = getloc.get_name() + if thelocation == None: + thelocation = "Unknown" + theweather = observation.get_weather() + temperature = theweather.get_temperature(unit='celsius').get('temp') + if temperature == None: + temperature = "Unknown" + + # Weather symbols + status = "" + status_now = theweather.get_weather_code() + if status_now < 232: # Rain storm + status += "⛈️ " + elif status_now < 321: # Drizzle + status += "🌧️ " + elif status_now < 504: # Light rain + status += "🌦️ " + elif status_now < 531: # Cloudy rain + status += "⛈️ " + elif status_now < 622: # Snow + status += "🌨️ " + elif status_now < 781: # Atmosphere + status += "🌪️ " + elif status_now < 800: # Bright + status += "🌤️ " + elif status_now < 801: # A little cloudy + status += "⛅️ " + elif status_now < 804: # Cloudy + status += "☁️ " + status += theweather._detailed_status + + + update.message.reply_text("Today in {} is being {}, around {}°C.\n".format(thelocation, + status, temperature)) + + except pyowm.exceptions.not_found_error.NotFoundError: + update.effective_message.reply_text("Sorry, location not found.") + + +__help__ = """ + - /weather : get weather info in a particular place +""" + +__mod_name__ = "WEATHER" + +WEATHER_HANDLER = DisableAbleCommandHandler("weather", weather, pass_args=True) + +dispatcher.add_handler(WEATHER_HANDLER) diff --git a/fire_bot/modules/weebify.py b/fire_bot/modules/weebify.py new file mode 100644 index 0000000..f556fa4 --- /dev/null +++ b/fire_bot/modules/weebify.py @@ -0,0 +1,40 @@ +from typing import List + +from telegram import Bot, Update +from telegram.ext import run_async + +from tg_bot import dispatcher +from tg_bot.modules.disable import DisableAbleCommandHandler + +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 = ['卂', '乃', '匚', '刀', '乇', '下', '厶', '卄', '工', '丁', '长', '乚', '从', '𠘨', '口', '尸', '㔿', '尺', '丂', '丅', '凵', + 'リ', '山', '乂', '丫', '乙'] + + +@run_async +def weebify(bot: Bot, update: Update, args: List[str]): + string = ' '.join(args).lower() + for normiecharacter in string: + if normiecharacter in normiefont: + weebycharacter = weebyfont[normiefont.index(normiecharacter)] + string = string.replace(normiecharacter, weebycharacter) + + message = update.effective_message + if message.reply_to_message: + message.reply_to_message.reply_text(string) + else: + message.reply_text(string) + + +__help__ = """ + - /weebify : returns a weebified text + """ + +WEEBIFY_HANDLER = DisableAbleCommandHandler("weebify", weebify, pass_args=True) + +dispatcher.add_handler(WEEBIFY_HANDLER) + +__mod_name__ = "WEEBIFY" +__command_list__ = ["weebify"] +__handlers__ = [WEEBIFY_HANDLER] diff --git a/fire_bot/modules/welcome.py b/fire_bot/modules/welcome.py new file mode 100644 index 0000000..418f88c --- /dev/null +++ b/fire_bot/modules/welcome.py @@ -0,0 +1,714 @@ +import html +import random +import re +import time +from typing import List +from functools import partial + +from telegram import Update, Bot, CallbackQuery +from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.error import BadRequest +from telegram.ext import MessageHandler, Filters, CommandHandler, run_async, CallbackQueryHandler, JobQueue +from telegram.utils.helpers import mention_markdown, mention_html, escape_markdown + +import tg_bot.modules.sql.welcome_sql as sql +from tg_bot import dispatcher, OWNER_ID, DEV_USERS, SUDO_USERS, SUPPORT_USERS, TIGER_USERS, WHITELIST_USERS, LOGGER +from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_ban_protected +from tg_bot.modules.helper_funcs.misc import build_keyboard, revert_buttons +from tg_bot.modules.helper_funcs.msg_types import get_welcome_type +from tg_bot.modules.helper_funcs.string_handling import (markdown_parser, + escape_invalid_curly_brackets) +from tg_bot.modules.log_channel import loggable + +VALID_WELCOME_FORMATTERS = ['first', 'last', 'fullname', 'username', 'id', 'count', 'chatname', 'mention'] + +ENUM_FUNC_MAP = { + sql.Types.TEXT.value: dispatcher.bot.send_message, + sql.Types.BUTTON_TEXT.value: dispatcher.bot.send_message, + sql.Types.STICKER.value: dispatcher.bot.send_sticker, + sql.Types.DOCUMENT.value: dispatcher.bot.send_document, + sql.Types.PHOTO.value: dispatcher.bot.send_photo, + sql.Types.AUDIO.value: dispatcher.bot.send_audio, + sql.Types.VOICE.value: dispatcher.bot.send_voice, + sql.Types.VIDEO.value: dispatcher.bot.send_video +} + +VERIFIED_USER_WAITLIST = {} + +# do not async +def send(update, message, keyboard, backup_message): + try: + msg = update.effective_message.reply_text(message, parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard) + except BadRequest as excp: + if excp.message == "Button_url_invalid": + msg = update.effective_message.reply_text(markdown_parser(backup_message + + "\nNote: the current message has an invalid url " + "in one of its buttons. Please update."), + parse_mode=ParseMode.MARKDOWN) + elif excp.message == "Unsupported url protocol": + msg = update.effective_message.reply_text(markdown_parser(backup_message + + "\nNote: the current message has buttons which " + "use url protocols that are unsupported by " + "telegram. Please update."), + parse_mode=ParseMode.MARKDOWN) + elif excp.message == "Wrong url host": + msg = update.effective_message.reply_text(markdown_parser(backup_message + + "\nNote: the current message has some bad urls. " + "Please update."), + parse_mode=ParseMode.MARKDOWN) + LOGGER.warning(message) + LOGGER.warning(keyboard) + LOGGER.exception("Could not parse! got invalid url host errors") + else: + msg = update.effective_message.reply_text(markdown_parser(backup_message + + "\nNote: An error occured when sending the " + "custom message. Please update."), + parse_mode=ParseMode.MARKDOWN) + LOGGER.exception() + + return msg + + +@run_async +@loggable +def new_member(bot: Bot, update: Update, job_queue: JobQueue): + chat = update.effective_chat + user = update.effective_user + msg = update.effective_message + + should_welc, cust_welcome, 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_members = update.effective_message.new_chat_members + + for new_mem in new_members: + + welcome_log = None + sent = None + should_mute = True + welcome_bool = True + + if should_welc: + + # Give the owner a special welcome + if new_mem.id == OWNER_ID: + update.effective_message.reply_text("YEAH LEGDNDS IS HERE") + welcome_log = (f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"Bot Owner just joined the chat") + + # Welcome Devs + elif new_mem.id in DEV_USERS: + update.effective_message.reply_text("Whoa! A Powerful just joined!") + + # Welcome Sudos + elif new_mem.id in SUDO_USERS: + update.effective_message.reply_text("Huh! A Powered just joined! Stay Alert!") + + # Welcome Support + elif new_mem.id in SUPPORT_USERS: + update.effective_message.reply_text("Hey! A support user joined!") + + # Welcome Whitelisted + elif new_mem.id in TIGER_USERS: + update.effective_message.reply_text("Oof! A Tiger disaster just joined!") + + # Welcome Tigers + elif new_mem.id in WHITELIST_USERS: + update.effective_message.reply_text("Oof! A Wolf disaster just joined!") + + # Welcome yourself + elif new_mem.id == bot.id: + update.effective_message.reply_text("hello 😎 thanks for using me make sure you promote me then i can safe your group for spammers 🥰🥰🥰") + + else: + # If welcome message is media, send with appropriate function + if welc_type not in (sql.Types.TEXT, sql.Types.BUTTON_TEXT): + ENUM_FUNC_MAP[welc_type](chat.id, cust_welcome) + continue + + # else, move on + 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_members_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) + buttons = sql.get_welc_buttons(chat.id) + keyb = build_keyboard(buttons) + + else: + res = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format(first=escape_markdown(first_name)) + keyb = [] + + backup_message = random.choice(sql.DEFAULT_WELCOME_MESSAGES).format(first=escape_markdown(first_name)) + keyboard = InlineKeyboardMarkup(keyb) + + else: + welcome_bool = False + res = None + keyboard = None + 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, + can_send_messages=True, + can_send_media_messages=False, + can_send_other_messages=False, + can_add_web_page_previews=False, + until_date=(int(time.time() + 24 * 60 * 60))) + + if welc_mutes == "strong": + welcome_bool = False + VERIFIED_USER_WAITLIST.update({ + new_mem.id : { + "should_welc" : should_welc, + "status" : False, + "update" : update, + "res" : res, + "keyboard" : keyboard, + "backup_message" : backup_message + } + }) + new_join_mem = f"[{escape_markdown(new_mem.first_name)}](tg://user?id={user.id})" + message = msg.reply_text(f"{new_join_mem}, click the button below to prove you're human.\nYou have 160 seconds.", + reply_markup=InlineKeyboardMarkup([{InlineKeyboardButton( + text="Yes, I'm human.", + callback_data=f"user_join_({new_mem.id})")}]), + parse_mode=ParseMode.MARKDOWN) + bot.restrict_chat_member(chat.id, new_mem.id, + can_send_messages=False, + can_send_media_messages=False, + can_send_other_messages=False, + can_add_web_page_previews=False) + + job_queue.run_once( + partial( + check_not_bot, new_mem, chat.id, message.message_id + ), 160, name="welcomemute" + ) + + if welcome_bool: + sent = send(update, res, keyboard, backup_message) + + 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) + + if welcome_log: + return welcome_log + + return (f"{html.escape(chat.title)}\n" + f"#USER_JOINED\n" + f"User: {mention_html(user.id, user.first_name)}\n" + f"ID: {user.id}") + + return "" + + +def check_not_bot(member, chat_id, message_id, bot, job): + + member_dict = VERIFIED_USER_WAITLIST.pop(member.id) + member_status = member_dict.get("status") + if not member_status: + try: + bot.unban_chat_member(chat_id, member.id) + except: + pass + + try: + bot.edit_message_text("*kicks user*\nThey can always rejoin and try.", chat_id=chat_id, message_id=message_id) + except: + pass + + +@run_async +def left_member(bot: Bot, update: Update): + chat = update.effective_chat + user = update.effective_user + should_goodbye, cust_goodbye, goodbye_type = sql.get_gdbye_pref(chat.id) + + if user.id == bot.id: + return + + if should_goodbye: + left_mem = update.effective_message.left_chat_member + if left_mem: + # Ignore bot being kicked + if left_mem.id == bot.id: + return + + # Give the owner a special goodbye + if left_mem.id == OWNER_ID: + update.effective_message.reply_text("ooo sed legend is gone") + return + + # Give the devs a special goodbye + elif left_mem.id in DEV_USERS: + update.effective_message.reply_text("See you later at the Hero's Association!") + return + + # if media goodbye, use appropriate function for it + if goodbye_type != sql.Types.TEXT and goodbye_type != sql.Types.BUTTON_TEXT: + ENUM_FUNC_MAP[goodbye_type](chat.id, cust_goodbye) + return + + first_name = left_mem.first_name or "PersonWithNoName" # edge case of empty name - occurs for some bugs. + if cust_goodbye: + if cust_goodbye == sql.DEFAULT_GOODBYE: + cust_goodbye = random.choice(sql.DEFAULT_GOODBYE_MESSAGES).format(first=escape_markdown(first_name)) + if left_mem.last_name: + fullname = escape_markdown(f"{first_name} {left_mem.last_name}") + else: + fullname = escape_markdown(first_name) + count = chat.get_members_count() + mention = mention_markdown(left_mem.id, first_name) + if left_mem.username: + username = "@" + escape_markdown(left_mem.username) + else: + username = mention + + valid_format = escape_invalid_curly_brackets(cust_goodbye, VALID_WELCOME_FORMATTERS) + res = valid_format.format(first=escape_markdown(first_name), + last=escape_markdown(left_mem.last_name or first_name), + fullname=escape_markdown(fullname), username=username, mention=mention, + count=count, chatname=escape_markdown(chat.title), id=left_mem.id) + buttons = sql.get_gdbye_buttons(chat.id) + keyb = build_keyboard(buttons) + + else: + res = random.choice(sql.DEFAULT_GOODBYE_MESSAGES).format(first=first_name) + keyb = [] + + keyboard = InlineKeyboardMarkup(keyb) + + send(update, res, keyboard, random.choice(sql.DEFAULT_GOODBYE_MESSAGES).format(first=first_name)) + + +@run_async +@user_admin +def welcome(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + # if no args, show current replies. + if not args or args[0].lower() == "noformat": + noformat = True + pref, welcome_m, 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" + f"*The welcome message (not filling the {{}}) is:*", + parse_mode=ParseMode.MARKDOWN) + + if welcome_type == sql.Types.BUTTON_TEXT: + buttons = sql.get_welc_buttons(chat.id) + if noformat: + welcome_m += revert_buttons(buttons) + update.effective_message.reply_text(welcome_m) + + else: + keyb = build_keyboard(buttons) + keyboard = InlineKeyboardMarkup(keyb) + + send(update, welcome_m, keyboard, sql.DEFAULT_WELCOME) + + else: + if noformat: + ENUM_FUNC_MAP[welcome_type](chat.id, welcome_m) + + else: + ENUM_FUNC_MAP[welcome_type](chat.id, welcome_m, parse_mode=ParseMode.MARKDOWN) + + elif len(args) >= 1: + if args[0].lower() in ("on", "yes"): + sql.set_welc_preference(str(chat.id), True) + update.effective_message.reply_text("Okay! I'll greet members when they join.") + + elif args[0].lower() in ("off", "no"): + sql.set_welc_preference(str(chat.id), False) + update.effective_message.reply_text("I'll go loaf around and not welcome anyone then.") + + else: + update.effective_message.reply_text("I understand 'on/yes' or 'off/no' only!") + + +@run_async +@user_admin +def goodbye(bot: Bot, update: Update, args: List[str]): + chat = update.effective_chat + + if not args or args[0] == "noformat": + noformat = True + 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" + f"*The goodbye message (not filling the {{}}) is:*", + parse_mode=ParseMode.MARKDOWN) + + if goodbye_type == sql.Types.BUTTON_TEXT: + buttons = sql.get_gdbye_buttons(chat.id) + if noformat: + goodbye_m += revert_buttons(buttons) + update.effective_message.reply_text(goodbye_m) + + else: + keyb = build_keyboard(buttons) + keyboard = InlineKeyboardMarkup(keyb) + + send(update, goodbye_m, keyboard, sql.DEFAULT_GOODBYE) + + else: + if noformat: + ENUM_FUNC_MAP[goodbye_type](chat.id, goodbye_m) + + 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"): + sql.set_gdbye_preference(str(chat.id), True) + update.effective_message.reply_text("Ok!") + + elif args[0].lower() in ("off", "no"): + sql.set_gdbye_preference(str(chat.id), False) + update.effective_message.reply_text("Ok!") + + else: + # idek what you're writing, say yes or no + update.effective_message.reply_text("I understand 'on/yes' or 'off/no' only!") + + +@run_async +@user_admin +@loggable +def set_welcome(bot: Bot, update: Update) -> str: + chat = update.effective_chat + user = update.effective_user + msg = update.effective_message + + text, data_type, content, buttons = get_welcome_type(msg) + + if data_type is None: + msg.reply_text("You didn't specify what to reply with!") + return "" + + sql.set_custom_welcome(chat.id, content or text, data_type, buttons) + msg.reply_text("Successfully set custom welcome message!") + + return (f"{html.escape(chat.title)}:\n" + f"#SET_WELCOME\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Set the welcome message.") + + +@run_async +@user_admin +@loggable +def reset_welcome(bot: Bot, update: Update) -> str: + chat = update.effective_chat + user = update.effective_user + + sql.set_custom_welcome(chat.id, sql.DEFAULT_WELCOME, sql.Types.TEXT) + update.effective_message.reply_text("Successfully reset welcome message to default!") + + return (f"{html.escape(chat.title)}:\n" + f"#RESET_WELCOME\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Reset the welcome message to default.") + + +@run_async +@user_admin +@loggable +def set_goodbye(bot: Bot, update: Update) -> str: + chat = update.effective_chat + user = update.effective_user + msg = update.effective_message + text, data_type, content, buttons = get_welcome_type(msg) + + if data_type is None: + msg.reply_text("You didn't specify what to reply with!") + return "" + + sql.set_custom_gdbye(chat.id, content or text, data_type, buttons) + msg.reply_text("Successfully set custom goodbye message!") + return (f"{html.escape(chat.title)}:\n" + f"#SET_GOODBYE\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Set the goodbye message.") + + +@run_async +@user_admin +@loggable +def reset_goodbye(bot: Bot, update: Update) -> str: + chat = update.effective_chat + user = update.effective_user + + sql.set_custom_gdbye(chat.id, sql.DEFAULT_GOODBYE, sql.Types.TEXT) + update.effective_message.reply_text("Successfully reset goodbye message to default!") + + return (f"{html.escape(chat.title)}:\n" + f"#RESET_GOODBYE\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Reset the goodbye message.") + + +@run_async +@user_admin +@loggable +def welcomemute(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + msg = update.effective_message + + if len(args) >= 1: + if args[0].lower() in ("off", "no"): + sql.set_welcome_mutes(chat.id, False) + msg.reply_text("I will no longer mute people on joining!") + return (f"{html.escape(chat.title)}:\n" + f"#WELCOME_MUTE\n" + f"• Admin: {mention_html(user.id, user.first_name)}\n" + f"Has toggled welcome mute to OFF.") + elif args[0].lower() in ["soft"]: + sql.set_welcome_mutes(chat.id, "soft") + msg.reply_text("I will restrict users' permission to send media for 24 hours.") + return (f"{html.escape(chat.title)}:\n" + f"#WELCOME_MUTE\n" + f"• Admin: {mention_html(user.id, user.first_name)}\n" + f"Has toggled welcome mute to SOFT.") + elif args[0].lower() in ["strong"]: + sql.set_welcome_mutes(chat.id, "strong") + msg.reply_text("I will now mute people when they join until they prove they're not a bot.\nThey will have 160seconds before they get kicked.") + return (f"{html.escape(chat.title)}:\n" + f"#WELCOME_MUTE\n" + f"• Admin: {mention_html(user.id, user.first_name)}\n" + f"Has toggled welcome mute to STRONG.") + else: + msg.reply_text("Please enter `off`/`no`/`soft`/`strong`!", parse_mode=ParseMode.MARKDOWN) + return "" + else: + curr_setting = sql.welcome_mutes(chat.id) + reply = (f"\n Give me a setting!\nChoose one out of: `off`/`no` or `soft` or `strong` only! \n" + f"Current setting: `{curr_setting}`") + msg.reply_text(reply, parse_mode=ParseMode.MARKDOWN) + return "" + + +@run_async +@user_admin +@loggable +def clean_welcome(bot: Bot, update: Update, args: List[str]) -> str: + chat = update.effective_chat + user = update.effective_user + + if not args: + clean_pref = sql.get_clean_pref(chat.id) + if clean_pref: + update.effective_message.reply_text("I should be deleting welcome messages up to two days old.") + else: + update.effective_message.reply_text("I'm currently not deleting old welcome messages!") + return "" + + if args[0].lower() in ("on", "yes"): + sql.set_clean_welcome(str(chat.id), True) + update.effective_message.reply_text("I'll try to delete old welcome messages!") + return (f"{html.escape(chat.title)}:\n" + f"#CLEAN_WELCOME\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Has toggled clean welcomes to ON.") + elif args[0].lower() in ("off", "no"): + sql.set_clean_welcome(str(chat.id), False) + update.effective_message.reply_text("I won't delete old welcome messages.") + return (f"{html.escape(chat.title)}:\n" + f"#CLEAN_WELCOME\n" + f"Admin: {mention_html(user.id, user.first_name)}\n" + f"Has toggled clean welcomes to OFF.") + else: + update.effective_message.reply_text("I understand 'on/yes' or 'off/no' only!") + return "" + + +@run_async +def user_button(bot: Bot, update: Update): + chat = update.effective_chat + user = update.effective_user + query = update.callback_query + match = re.match(r"user_join_\((.+?)\)", query.data) + message = update.effective_message + join_user = int(match.group(1)) + + if join_user == user.id: + member_dict = VERIFIED_USER_WAITLIST.pop(user.id) + 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, can_send_messages=True, + can_send_media_messages=True, + can_send_other_messages=True, + can_add_web_page_previews=True) + bot.deleteMessage(chat.id, message.message_id) + if member_dict["should_welc"]: + sent = send(member_dict["update"], member_dict["res"], member_dict["keyboard"], member_dict["backup_message"]) + + 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: + query.answer(text="You're not allowed to do this!") + + +WELC_HELP_TXT = ("Your group's welcome/goodbye messages can be personalised in multiple ways. If you want the messages" + " to be individually generated, like the default welcome message is, you can use *these* variables:\n" + " - `{{first}}`: this represents the user's *first* name\n" + " - `{{last}}`: this represents the user's *last* name. Defaults to *first name* if user has no " + "last name.\n" + " - `{{fullname}}`: this represents the user's *full* name. Defaults to *first name* if user has no " + "last name.\n" + " - `{{username}}`: this represents the user's *username*. Defaults to a *mention* of the user's " + "first name if has no username.\n" + " - `{{mention}}`: this simply *mentions* a user - tagging them with their first name.\n" + " - `{{id}}`: this represents the user's *id*\n" + " - `{{count}}`: this represents the user's *member number*.\n" + " - `{{chatname}}`: this represents the *current chat name*.\n" + "\nEach variable MUST be surrounded by `{{}}` to be replaced.\n" + "Welcome messages also support markdown, so you can make any elements bold/italic/code/links. " + "Buttons are also supported, so you can make your welcomes look awesome with some nice intro " + "buttons.\n" + f"To create a button linking to your rules, use this: `[Rules](buttonurl://t.me/{dispatcher.bot.username}?start=group_id)`. " + "Simply replace `group_id` with your group's id, which can be obtained via /id, and you're good to " + "go. Note that group ids are usually preceded by a `-` sign; this is required, so please don't " + "remove it.\n" + "If you're feeling fun, you can even set images/gifs/videos/voice messages as the welcome message by " + "replying to the desired media, and calling /setwelcome.") + +WELC_MUTE_HELP_TXT = ( + "You can get the bot to mute new people who join your group and hence prevent spambots from flooding your group. " + "The following options are possible:\n" + "- `/welcomemute soft`: restricts new members from sending media for 24 hours.\n" + "- `/welcomemute strong`: mutes new members till they tap on a button thereby verifying they're human.\n" + "- `/welcomemute off`: turns off welcomemute.\n" + "`Note:` Strong mode kicks a user from the chat if they dont verify in 160seconds. They can always rejoin though" + ) + +@run_async +@user_admin +def welcome_help(bot: Bot, update: Update): + update.effective_message.reply_text(WELC_HELP_TXT, parse_mode=ParseMode.MARKDOWN) + + +@run_async +@user_admin +def welcome_mute_help(bot: Bot, update: Update): + update.effective_message.reply_text(WELC_MUTE_HELP_TXT, parse_mode=ParseMode.MARKDOWN) + + +# TODO: get welcome data from group butler snap +# def __import_data__(chat_id, data): +# welcome = data.get('info', {}).get('rules') +# welcome = welcome.replace('$username', '{username}') +# welcome = welcome.replace('$name', '{fullname}') +# welcome = welcome.replace('$id', '{id}') +# welcome = welcome.replace('$title', '{chatname}') +# welcome = welcome.replace('$surname', '{lastname}') +# welcome = welcome.replace('$rules', '{rules}') +# sql.set_custom_welcome(chat_id, welcome, sql.Types.TEXT) + + +def __migrate__(old_chat_id, new_chat_id): + sql.migrate_chat(old_chat_id, new_chat_id) + + +def __chat_settings__(chat_id, user_id): + welcome_pref, _, _ = sql.get_welc_pref(chat_id) + goodbye_pref, _, _ = sql.get_gdbye_pref(chat_id) + return "This chat has it's welcome preference set to `{}`.\n" \ + "It's goodbye preference is `{}`.".format(welcome_pref, goodbye_pref) + + +__help__ = """ +{} + +*Admin only:* + - /welcome : enable/disable welcome messages. + - /welcome: shows current welcome settings. + - /welcome noformat: shows current welcome settings, without the formatting - useful to recycle your welcome messages! + - /goodbye -> same usage and args as /welcome. + - /setwelcome : set a custom welcome message. If used replying to media, uses that media. + - /setgoodbye : set a custom goodbye message. If used replying to media, uses that media. + - /resetwelcome: reset to the default welcome message. + - /resetgoodbye: reset to the default goodbye message. + - /cleanwelcome : On new member, try to delete the previous welcome message to avoid spamming the chat. + - /welcomemutehelp: gives information about welcome mutes. + - /welcomehelp: view more formatting information for custom welcome/goodbye messages. +""".format(WELC_HELP_TXT) + +NEW_MEM_HANDLER = MessageHandler(Filters.status_update.new_chat_members, new_member, pass_job_queue=True) +LEFT_MEM_HANDLER = MessageHandler(Filters.status_update.left_chat_member, left_member) +WELC_PREF_HANDLER = CommandHandler("welcome", welcome, pass_args=True, filters=Filters.group) +GOODBYE_PREF_HANDLER = CommandHandler("goodbye", goodbye, pass_args=True, filters=Filters.group) +SET_WELCOME = CommandHandler("setwelcome", set_welcome, filters=Filters.group) +SET_GOODBYE = CommandHandler("setgoodbye", set_goodbye, filters=Filters.group) +RESET_WELCOME = CommandHandler("resetwelcome", reset_welcome, filters=Filters.group) +RESET_GOODBYE = CommandHandler("resetgoodbye", reset_goodbye, filters=Filters.group) +WELCOMEMUTE_HANDLER = CommandHandler("welcomemute", welcomemute, pass_args=True, filters=Filters.group) +CLEAN_WELCOME = CommandHandler("cleanwelcome", clean_welcome, pass_args=True, filters=Filters.group) +WELCOME_HELP = CommandHandler("welcomehelp", welcome_help) +WELCOME_MUTE_HELP = CommandHandler("welcomemutehelp", welcome_mute_help) +BUTTON_VERIFY_HANDLER = CallbackQueryHandler(user_button, pattern=r"user_join_") + +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) +dispatcher.add_handler(SET_GOODBYE) +dispatcher.add_handler(RESET_WELCOME) +dispatcher.add_handler(RESET_GOODBYE) +dispatcher.add_handler(CLEAN_WELCOME) +dispatcher.add_handler(WELCOME_HELP) +dispatcher.add_handler(WELCOMEMUTE_HANDLER) +dispatcher.add_handler(BUTTON_VERIFY_HANDLER) +dispatcher.add_handler(WELCOME_MUTE_HELP) + +__mod_name__ = "GREETINGS" +__command_list__ = ["welcome", "goodbye", "setwelcome", "setgoodbye", "resetwelcome", "resetgoodbye", + "welcomemute", "cleanwelcome", "welcomehelp", "welcomemutehelp"] +__handlers__ = [NEW_MEM_HANDLER, LEFT_MEM_HANDLER, WELC_PREF_HANDLER, GOODBYE_PREF_HANDLER, + SET_WELCOME, SET_GOODBYE, RESET_WELCOME, RESET_GOODBYE, CLEAN_WELCOME, + WELCOME_HELP, WELCOMEMUTE_HANDLER, BUTTON_VERIFY_HANDLER, WELCOME_MUTE_HELP] diff --git a/fire_bot/modules/whois.py b/fire_bot/modules/whois.py new file mode 100644 index 0000000..4cd8f10 --- /dev/null +++ b/fire_bot/modules/whois.py @@ -0,0 +1,119 @@ +import html +import json +import os +import psutil +import random +import time +import datetime +from typing import Optional, List +import re +import requests +from telegram.error import BadRequest +from telegram import Message, Chat, Update, Bot, MessageEntity +from telegram import ParseMode +from telegram.ext import CommandHandler, run_async, Filters +from telegram.utils.helpers import escape_markdown, mention_html +from tg_bot.modules.helper_funcs.chat_status import user_admin, sudo_plus, is_user_admin +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS, DEV_USERS, WHITELIST_USERS +from tg_bot.__main__ import STATS, USER_INFO, TOKEN +from tg_bot.modules.disable import DisableAbleCommandHandler, DisableAbleRegexHandler +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.helper_funcs.filters import CustomFilters +import tg_bot.modules.sql.users_sql as sql +import tg_bot.modules.helper_funcs.cas_api as cas + +@run_async +def whois(bot: Bot, update: Update, args: List[str]): + message = update.effective_message + chat = update.effective_chat + user_id = extract_user(update.effective_message, args) + + if user_id: + user = bot.get_chat(user_id) + + elif not message.reply_to_message and not args: + user = message.from_user + + elif not message.reply_to_message and (not args or ( + len(args) >= 1 and not args[0].startswith("@") and not args[0].isdigit() and not message.parse_entities( + [MessageEntity.TEXT_MENTION]))): + message.reply_text("I can't extract a user from this.") + return + + else: + return + + text = (f"User Information:\n" + f"UiD]- {user.id}\n" + f"👤Name: {html.escape(user.first_name)}") + + if user.last_name: + text += f"\n🚹Last Name: {html.escape(user.last_name)}" + + if user.username: + text += f"\n♻️Username: @{html.escape(user.username)}" + + text += f"\n☣️Permanent user link: {mention_html(user.id, 'link🚪')}" + + num_chats = sql.get_user_num_chats(user.id) + text += f"\n🌐Chat count: {num_chats}" + text += "\n🎭Number of profile pics: {}".format(bot.get_user_profile_photos(user.id).total_count) + + 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🛡This user holds the title⚜️ {custom_title} here." + except BadRequest: + pass + + + + if user.id == OWNER_ID: + text += "\n🚶🏻‍♂️Uff,This person is my Owner🤴\nI would never do anything against him!." + + elif user.id in DEV_USERS: + text += "\n🚴‍♂️Pling,This person is my dev🤷‍♂️\nI would never do anything against him!." + + elif user.id in SUDO_USERS: + text += "\n🚴‍♂️Pling,This person is one of my sudo users! " \ + "Nearly as powerful as my owner🕊so watch it.." + + elif user.id in SUPPORT_USERS: + text += "\n🚴‍♂️Pling,This person is one of my support users! " \ + "Not quite a sudo user, but can still gban you off the map." + + + + elif user.id in WHITELIST_USERS: + text += "\n🚴‍♂️Pling,This person has been whitelisted! " \ + "That means I'm not allowed to ban/kick them." + + + + text +="\n" + text += "\nCAS banned: " + result = cas.banchecker(user.id) + text += str(result) + for mod in USER_INFO: + if mod.__mod_name__ == "WHOIS": + continue + + try: + mod_info = mod.__user_info__(user.id) + except TypeError: + mod_info = mod.__user_info__(user.id, chat.id) + if mod_info: + text += "\n" + mod_info + try: + profile = bot.get_user_profile_photos(user.id).photos[0][-1] + bot.sendChatAction(chat.id, "upload_photo") + bot.send_photo(chat.id, photo=profile, caption=(text), parse_mode=ParseMode.HTML, disable_web_page_preview=True) + except IndexError: + update.effective_message.reply_text(text, parse_mode=ParseMode.HTML, disable_web_page_preview=True) + +WHOIS_HANDLER = DisableAbleCommandHandler("whois", whois, pass_args=True) +dispatcher.add_handler(WHOIS_HANDLER) diff --git a/fire_bot/modules/wiki.py b/fire_bot/modules/wiki.py new file mode 100644 index 0000000..5fa98c6 --- /dev/null +++ b/fire_bot/modules/wiki.py @@ -0,0 +1,30 @@ +from telegram import ChatAction +import html +import urllib.request +import re +import json +from typing import Optional, List +import time +import urllib +from urllib.request import urlopen, urlretrieve +from urllib.parse import quote_plus, urlencode +import requests +from telegram import Message, Chat, Update, Bot, MessageEntity +from telegram import ParseMode +from telegram.ext import CommandHandler, run_async, Filters +from tg_bot import dispatcher +from tg_bot.__main__ import STATS, USER_INFO +from tg_bot.modules.disable import DisableAbleCommandHandler +import wikipedia + +def wiki(bot: Bot, update: Update, args): + reply = " ".join(args) + summary = '{} more' + update.message.reply_text(summary.format(wikipedia.summary(reply, sentences=3), wikipedia.page(reply).url)) + +__help__ = """ + - /wiki text: Returns search from wikipedia for the input text +""" +__mod_name__ = "WIKIPEDIA" +WIKI_HANDLER = DisableAbleCommandHandler("wiki", wiki, pass_args=True) +dispatcher.add_handler(WIKI_HANDLER) diff --git a/fire_bot/modules/zal.py b/fire_bot/modules/zal.py new file mode 100644 index 0000000..aa9a35b --- /dev/null +++ b/fire_bot/modules/zal.py @@ -0,0 +1,35 @@ +from telegram import ChatAction +import html +import re +import json +import random +from random import randint +from datetime import datetime +from typing import Optional, List +import pyowm +import time +import requests +from telegram import Message, Chat, Update, Bot, MessageEntity +from telegram import ParseMode +from telegram.ext import CommandHandler, run_async, Filters +from telegram.utils.helpers import escape_markdown, mention_html +from tg_bot import dispatcher, OWNER_ID, SUDO_USERS, SUPPORT_USERS, WHITELIST_USERS, BAN_STICKER +from tg_bot.__main__ import STATS, USER_INFO +from tg_bot.modules.disable import DisableAbleCommandHandler +from tg_bot.modules.helper_funcs.extraction import extract_user +from tg_bot.modules.helper_funcs.filters import CustomFilters +from zalgo_text import zalgo + +def zal(bot: Bot, update: Update, args): + current_time = datetime.strftime(datetime.now(), "%d.%m.%Y %H:%M:%S") + if update.message.reply_to_message is not None: + args = update.message.reply_to_message.text + args = args.split(" ") + input_text = " ".join(args).lower() + if input_text == "": + update.message.reply_text("Type in some text!") + return + zalgofied_text = zalgo.zalgo().zalgofy(input_text) + update.message.reply_text(zalgofied_text) + +dispatcher.add_handler(CommandHandler('zal', zal, pass_args=True))