├── runtime.txt ├── Procfile ├── tg_bot ├── modules │ ├── helper_funcs │ │ ├── __init__.py │ │ ├── filters.py │ │ ├── handlers.py │ │ ├── extraction.py │ │ ├── misc.py │ │ ├── chat_status.py │ │ └── msg_types.py │ ├── sql │ │ ├── __init__.py │ │ ├── rules_sql.py │ │ ├── afk_sql.py │ │ ├── pin_sql.py │ │ ├── log_channel_sql.py │ │ ├── rss_sql.py │ │ ├── userinfo_sql.py │ │ ├── antiflood_sql.py │ │ ├── reporting_sql.py │ │ ├── disable_sql.py │ │ ├── blacklist_sql.py │ │ ├── connection_sql.py │ │ ├── global_bans_sql.py │ │ ├── notes_sql.py │ │ ├── users_sql.py │ │ ├── locks_sql.py │ │ └── cust_filters_sql.py │ ├── char_limit_exceed.py │ ├── translate.py │ ├── __init__.py │ ├── telegraph.py │ ├── translation.py │ ├── thumbnailer.py │ ├── stickers.py │ ├── keyboard.py │ ├── backups.py │ ├── zzzanticommand.py │ ├── afk.py │ ├── rules.py │ ├── msg_deleting.py │ ├── users.py │ ├── sed.py │ ├── antiflood.py │ ├── userinfo.py │ ├── reporting.py │ ├── disable.py │ ├── log_channel.py │ ├── blacklist.py │ ├── admin.py │ ├── muting.py │ ├── connection.py │ └── spin.py ├── sample_config.py └── __init__.py ├── requirements.txt ├── .gitignore ├── CONTRIBUTING.md ├── app.json └── README.md /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.1 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python3 -m tg_bot 2 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future 2 | emoji 3 | requests 4 | sqlalchemy 5 | python-telegram-bot==11.1.0 6 | psycopg2-binary 7 | feedparser 8 | googletrans 9 | telegraph 10 | pillow 11 | numpy 12 | uuid==1.30 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tg_bot/config.py 2 | *.pyc 3 | .idea/ 4 | .project 5 | .pydevproject 6 | .directory 7 | .vscode 8 | /venv/ 9 | *.jpg 10 | search_bleck_megick.py 11 | tg_bot/modules/inline_search.py 12 | tg_bot/modules/sql/movie_db_sql.py 13 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker, scoped_session 4 | 5 | from tg_bot import DB_URI 6 | 7 | 8 | def start() -> scoped_session: 9 | engine = create_engine(DB_URI, client_encoding="utf8") 10 | BASE.metadata.bind = engine 11 | BASE.metadata.create_all(engine) 12 | return scoped_session(sessionmaker(bind=engine, autoflush=False)) 13 | 14 | 15 | BASE = declarative_base() 16 | SESSION = start() 17 | -------------------------------------------------------------------------------- /tg_bot/modules/char_limit_exceed.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity 5 | from telegram.ext import Filters, MessageHandler, run_async 6 | 7 | from tg_bot import dispatcher, LOGGER 8 | from tg_bot.modules.warns import warn 9 | 10 | 11 | MAX_CHARS_PER_MESSAGE = "200" 12 | 13 | 14 | @run_async 15 | def warn_if_exceed(bot: Bot, update: Update): 16 | short_name = "Created By @MidukkiBot" 17 | msg = update.effective_message # type: Optional[Message] 18 | chat = update.effective_chat # type: Optional[Chat] 19 | if len(msg.text) > MAX_CHARS_PER_MESSAGE: 20 | return warn(msg.from_user, chat, "Exceeded SET character limit", msg) 21 | 22 | 23 | def warn_if_not_photo(bot: Bot, update: Update): 24 | short_name = "Created By @MidukkiBot" 25 | msg = update.effective_message # type: Optional[Message] 26 | msg.reply_text("Please send as file.") 27 | 28 | 29 | __help__ = "no one gonna help you" 30 | __mod_name__ = "exceed cl" 31 | 32 | dispatcher.add_handler(MessageHandler(Filters.text & Filters.group, warn_if_exceed)) 33 | dispatcher.add_handler(MessageHandler(Filters.photo & Filters.group, warn_if_not_photo)) 34 | -------------------------------------------------------------------------------- /tg_bot/modules/translate.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity 5 | from telegram.ext import Filters, MessageHandler, run_async 6 | 7 | from tg_bot import dispatcher, LOGGER 8 | from tg_bot.modules.disable import DisableAbleCommandHandler 9 | 10 | from googletrans import Translator 11 | 12 | 13 | @run_async 14 | def do_translate(bot: Bot, update: Update, args: List[str]): 15 | short_name = "Created By @MidukkiBot 😬" 16 | msg = update.effective_message # type: Optional[Message] 17 | lan = " ".join(args) 18 | to_translate_text = msg.reply_to_message.text 19 | translator = Translator() 20 | try: 21 | translated = translator.translate(to_translate_text, dest=lan) 22 | src_lang = translated.src 23 | translated_text = translated.text 24 | msg.reply_text("Translated from {} to {}.\n {}".format(src_lang, lan, translated_text)) 25 | except exc: 26 | msg.reply_text(str(exc)) 27 | 28 | 29 | __help__ = """- /trn - as reply to a long message 30 | """ 31 | __mod_name__ = "Google Translate" 32 | 33 | dispatcher.add_handler(DisableAbleCommandHandler("trn", do_translate, pass_args=True)) 34 | -------------------------------------------------------------------------------- /tg_bot/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from tg_bot import LOAD, NO_LOAD, LOGGER 2 | 3 | 4 | def __list_all_modules(): 5 | from os.path import dirname, basename, isfile 6 | import glob 7 | # This generates a list of modules in this folder for the * in __main__ to work. 8 | mod_paths = glob.glob(dirname(__file__) + "/*.py") 9 | all_modules = [basename(f)[:-3] for f in mod_paths if isfile(f) 10 | and f.endswith(".py") 11 | and not f.endswith('__init__.py')] 12 | 13 | if LOAD or NO_LOAD: 14 | to_load = LOAD 15 | if to_load: 16 | if not all(any(mod == module_name for module_name in all_modules) for mod in to_load): 17 | LOGGER.error("Invalid loadorder names. Quitting.") 18 | quit(1) 19 | 20 | else: 21 | to_load = all_modules 22 | 23 | if NO_LOAD: 24 | LOGGER.info("Not loading: {}".format(NO_LOAD)) 25 | return [item for item in to_load if item not in NO_LOAD] 26 | 27 | return to_load 28 | 29 | return all_modules 30 | 31 | 32 | ALL_MODULES = sorted(__list_all_modules()) 33 | LOGGER.info("Modules to load: %s", str(ALL_MODULES)) 34 | __all__ = ALL_MODULES + ["ALL_MODULES"] 35 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/filters.py: -------------------------------------------------------------------------------- 1 | from telegram import Message 2 | from telegram.ext import BaseFilter 3 | 4 | from tg_bot import SUPPORT_USERS, SUDO_USERS 5 | 6 | 7 | class CustomFilters(object): 8 | class _Supporters(BaseFilter): 9 | def filter(self, message: Message): 10 | return bool(message.from_user and message.from_user.id in SUPPORT_USERS) 11 | 12 | support_filter = _Supporters() 13 | 14 | class _Sudoers(BaseFilter): 15 | def filter(self, message: Message): 16 | return bool(message.from_user and message.from_user.id in SUDO_USERS) 17 | 18 | sudo_filter = _Sudoers() 19 | 20 | class _MimeType(BaseFilter): 21 | def __init__(self, mimetype): 22 | self.mime_type = mimetype 23 | self.name = "CustomFilters.mime_type({})".format(self.mime_type) 24 | 25 | def filter(self, message: Message): 26 | return bool(message.document and message.document.mime_type == self.mime_type) 27 | 28 | mime_type = _MimeType 29 | 30 | class _HasText(BaseFilter): 31 | def filter(self, message: Message): 32 | return bool(message.text or message.sticker or message.photo or message.document or message.video) 33 | 34 | has_text = _HasText() 35 | 36 | class _HasEntities(BaseFilter): 37 | def filter(self, message: Message): 38 | return bool(message.text and message.entities and len(message.entities) >= 2) 39 | 40 | has_entities = _HasEntities() 41 | 42 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/rules_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class Rules(BASE): 9 | __tablename__ = "rules" 10 | chat_id = Column(String(14), primary_key=True) 11 | rules = Column(UnicodeText, default="") 12 | 13 | def __init__(self, chat_id): 14 | self.chat_id = chat_id 15 | 16 | def __repr__(self): 17 | return "".format(self.chat_id, self.rules) 18 | 19 | 20 | Rules.__table__.create(checkfirst=True) 21 | 22 | INSERTION_LOCK = threading.RLock() 23 | 24 | 25 | def set_rules(chat_id, rules_text): 26 | with INSERTION_LOCK: 27 | rules = SESSION.query(Rules).get(str(chat_id)) 28 | if not rules: 29 | rules = Rules(str(chat_id)) 30 | rules.rules = rules_text 31 | 32 | SESSION.add(rules) 33 | SESSION.commit() 34 | 35 | 36 | def get_rules(chat_id): 37 | rules = SESSION.query(Rules).get(str(chat_id)) 38 | ret = "" 39 | if rules: 40 | ret = rules.rules 41 | 42 | SESSION.close() 43 | return ret 44 | 45 | 46 | def num_chats(): 47 | try: 48 | return SESSION.query(func.count(distinct(Rules.chat_id))).scalar() 49 | finally: 50 | SESSION.close() 51 | 52 | 53 | def migrate_chat(old_chat_id, new_chat_id): 54 | with INSERTION_LOCK: 55 | chat = SESSION.query(Rules).get(str(old_chat_id)) 56 | if chat: 57 | chat.chat_id = str(new_chat_id) 58 | SESSION.commit() 59 | -------------------------------------------------------------------------------- /tg_bot/modules/telegraph.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity 5 | from telegram.ext import Filters, MessageHandler, run_async 6 | 7 | from tg_bot import dispatcher, LOGGER 8 | from tg_bot.modules.disable import DisableAbleCommandHandler 9 | 10 | from telegraph import Telegraph, upload_file 11 | 12 | @run_async 13 | def media_telegraph(bot: Bot, update: Update): 14 | msg = update.effective_message # type: Optional[Message] 15 | 16 | 17 | @run_async 18 | def post_telegraph(bot: Bot, update: Update, args: List[str]): 19 | short_name = "Created By @MidukkiBot 😬" 20 | msg = update.effective_message # type: Optional[Message] 21 | telegraph = Telegraph() 22 | r = telegraph.create_account(short_name=short_name) 23 | auth_url = r["auth_url"] 24 | LOGGER.info(auth_url) 25 | title_of_page = " ".join(args) 26 | page_content = msg.reply_to_message.text 27 | page_content = page_content.replace("\n", "
") 28 | response = telegraph.create_page( 29 | title_of_page, 30 | html_content=page_content 31 | ) 32 | msg.reply_text("https://telegra.ph/{}".format(response["path"])) 33 | 34 | 35 | __help__ = """- /tele.gra.ph - as reply to a long message 36 | - /telegraph - as a reply to a media less than 5MiB 37 | """ 38 | __mod_name__ = "Telegra.ph" 39 | 40 | dispatcher.add_handler(DisableAbleCommandHandler("tele.gra.ph", post_telegraph, pass_args=True)) 41 | dispatcher.add_handler(DisableAbleCommandHandler("telegraph", media_telegraph, filters=Filters.video | Filters.photo)) 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are very welcome! Here are some guidelines on how the project is designed. 4 | 5 | ### CodeStyle 6 | 7 | - Adhere to PEP8 as much as possible. 8 | 9 | - Line lengths should be under 120 characters, use list comprehensions over map/filter, don't leave trailing whitespace. 10 | 11 | - More complex pieces of code should be commented for future reference. 12 | 13 | ### Structure 14 | 15 | There are a few self-imposed rules on the project structure, to keep the project as tidy as possible. 16 | - All modules should go into the `modules/` directory. 17 | - Any database accesses should be done in `modules/sql/` - no instances of SESSION should be imported anywhere else. 18 | - Make sure your database sessions are properly scoped! Always close them properly. 19 | - When creating a new module, there should be as few changes to other files as possible required to incorporate it. 20 | Removing the module file should result in a bot which is still in perfect working condition. 21 | - If a module is dependent on multiple other files, which might not be loaded, then create a list of at module 22 | load time, in `__main__`, by looking at attributes. This is how migration, /help, /stats, /info, and many other things 23 | are based off of. It allows the bot to work fine with the LOAD and NO_LOAD configurations. 24 | - Keep in mind that some things might clash; eg a regex handler could clash with a command handler - in this case, you 25 | should put them in different dispatcher groups. 26 | 27 | Might seem complicated, but it'll make sense when you get into it. Feel free to ask me for a hand/advice! 28 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/handlers.py: -------------------------------------------------------------------------------- 1 | import telegram.ext as tg 2 | from telegram import Update 3 | 4 | CMD_STARTERS = ('/', '!') 5 | 6 | 7 | class CustomCommandHandler(tg.CommandHandler): 8 | def __init__(self, command, callback, **kwargs): 9 | if "admin_ok" in kwargs: 10 | del kwargs["admin_ok"] 11 | super().__init__(command, callback, **kwargs) 12 | 13 | def check_update(self, update): 14 | if (isinstance(update, Update) 15 | and (update.message or update.edited_message and self.allow_edited)): 16 | message = update.message or update.edited_message 17 | 18 | if message.text and len(message.text) > 1: 19 | fst_word = message.text_html.split(None, 1)[0] 20 | if len(fst_word) > 1 and any(fst_word.startswith(start) for start in CMD_STARTERS): 21 | command = fst_word[1:].split('@') 22 | command.append(message.bot.username) # in case the command was sent without a username 23 | if self.filters is None: 24 | res = True 25 | elif isinstance(self.filters, list): 26 | res = any(func(message) for func in self.filters) 27 | else: 28 | res = self.filters(message) 29 | 30 | return res and (command[0].lower() in self.command 31 | and command[1].lower() == message.bot.username.lower()) 32 | 33 | return False 34 | 35 | 36 | class CustomRegexHandler(tg.RegexHandler): 37 | def __init__(self, pattern, callback, friendly="", **kwargs): 38 | super().__init__(pattern, callback, **kwargs) 39 | -------------------------------------------------------------------------------- /tg_bot/modules/translation.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pprint import pprint 3 | 4 | import requests 5 | from telegram import Update, Bot 6 | from telegram.ext import CommandHandler 7 | 8 | from tg_bot import dispatcher 9 | 10 | # Open API key 11 | API_KEY = "6ae0c3a0-afdc-4532-a810-82ded0054236" 12 | URL = "http://services.gingersoftware.com/Ginger/correct/json/GingerTheText" 13 | 14 | 15 | def translate(bot: Bot, update: Update): 16 | if update.effective_message.reply_to_message: 17 | msg = update.effective_message.reply_to_message 18 | 19 | params = dict( 20 | lang="US", 21 | clientVersion="2.0", 22 | apiKey=API_KEY, 23 | text=msg.text 24 | ) 25 | 26 | res = requests.get(URL, params=params) 27 | # print(res) 28 | # print(res.text) 29 | pprint(json.loads(res.text)) 30 | changes = json.loads(res.text).get('LightGingerTheTextResult') 31 | curr_string = "" 32 | 33 | prev_end = 0 34 | 35 | for change in changes: 36 | start = change.get('From') 37 | end = change.get('To') + 1 38 | suggestions = change.get('Suggestions') 39 | if suggestions: 40 | sugg_str = suggestions[0].get('Text') # should look at this list more 41 | curr_string += msg.text[prev_end:start] + sugg_str 42 | 43 | prev_end = end 44 | 45 | curr_string += msg.text[prev_end:] 46 | print(curr_string) 47 | update.effective_message.reply_text(curr_string) 48 | 49 | 50 | __help__ = """ 51 | - /t: while replying to a message, will reply with a grammar corrected version 52 | """ 53 | 54 | __mod_name__ = "Translator" 55 | 56 | 57 | TRANSLATE_HANDLER = CommandHandler('t', translate) 58 | 59 | dispatcher.add_handler(TRANSLATE_HANDLER) 60 | -------------------------------------------------------------------------------- /tg_bot/modules/thumbnailer.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity 5 | from telegram.ext import Filters, MessageHandler, run_async 6 | 7 | from tg_bot import dispatcher, LOGGER, SUDO_USERS, SUPPORT_USERS 8 | 9 | import numpy 10 | from PIL import Image 11 | import os 12 | 13 | @run_async 14 | def generate_thumb_nail(bot: Bot, update: Update): 15 | short_name = "Created By @MidukkiBot" 16 | msg = update.effective_message # type: Optional[Message] 17 | from_user_id = update.effective_chat.id # type: Optional[Chat] 18 | if int(from_user_id) in SUDO_USERS + SUPPORT_USERS: 19 | # received photo 20 | file_id = msg.photo[-1].file_id 21 | newFile = bot.get_file(file_id) 22 | newFile.download("Image1.jpg") 23 | # download photo 24 | list_im = ["Image1.jpg", "Image2.jpg"] 25 | imgs = [ Image.open(i) for i in list_im ] 26 | inm_aesph = sorted([(numpy.sum(i.size), i.size) for i in imgs]) 27 | LOGGER.info(inm_aesph) 28 | min_shape = inm_aesph[1][1] 29 | imgs_comb = numpy.hstack(numpy.asarray(i.resize(min_shape)) for i in imgs) 30 | imgs_comb = Image.fromarray(imgs_comb) 31 | # combine: https://stackoverflow.com/a/30228789/4723940 32 | imgs_comb.save("Image1.jpg") 33 | # send 34 | bot.send_photo(from_user_id, photo=open("Image1.jpg", "rb"), reply_to_message_id=msg.message_id) 35 | # cleanup 36 | os.remove("Image1.jpg") 37 | else: 38 | bot.send_message(from_user_id, text="Only admins are authorized to access this module.", reply_to_message_id=msg.message_id) 39 | 40 | 41 | __help__ = """Send a photo to Generate ThumbNail 42 | """ 43 | __mod_name__ = "Video ThumbNailEr" 44 | 45 | dispatcher.add_handler(MessageHandler(Filters.photo & Filters.private, generate_thumb_nail)) 46 | -------------------------------------------------------------------------------- /tg_bot/modules/stickers.py: -------------------------------------------------------------------------------- 1 | from telegram import Message, Chat, Update, Bot 2 | from telegram import ParseMode 3 | from telegram.ext import CommandHandler, run_async 4 | from telegram.utils.helpers import escape_markdown 5 | 6 | from tg_bot import dispatcher 7 | from tg_bot.modules.disable import DisableAbleCommandHandler 8 | import os 9 | 10 | 11 | @run_async 12 | def stickerid(bot: Bot, update: Update): 13 | msg = update.effective_message 14 | if msg.reply_to_message and msg.reply_to_message.sticker: 15 | update.effective_message.reply_text("Sticker ID:\n```" + 16 | escape_markdown(msg.reply_to_message.sticker.file_id) + "```", 17 | parse_mode=ParseMode.MARKDOWN) 18 | else: 19 | update.effective_message.reply_text("Please reply to a sticker to get its ID.") 20 | 21 | 22 | @run_async 23 | def getsticker(bot: Bot, update: Update): 24 | msg = update.effective_message 25 | chat_id = update.effective_chat.id 26 | if msg.reply_to_message and msg.reply_to_message.sticker: 27 | file_id = msg.reply_to_message.sticker.file_id 28 | newFile = bot.get_file(file_id) 29 | newFile.download('sticker.png') 30 | bot.sendDocument(chat_id, document=open('sticker.png', 'rb')) 31 | os.remove('sticker.png') 32 | else: 33 | update.effective_message.reply_text("Please reply to a sticker for me to upload its PNG.") 34 | 35 | # /ip is for private use 36 | __help__ = """ 37 | - /stickerid: reply to a sticker to me to tell you its file ID. 38 | - /getsticker: reply to a sticker to me to upload its raw PNG file. 39 | """ 40 | 41 | __mod_name__ = "Stickers" 42 | 43 | STICKERID_HANDLER = DisableAbleCommandHandler("stickerid", stickerid) 44 | GETSTICKER_HANDLER = DisableAbleCommandHandler("getsticker", getsticker) 45 | 46 | dispatcher.add_handler(STICKERID_HANDLER) 47 | dispatcher.add_handler(GETSTICKER_HANDLER) 48 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/afk_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Boolean, Integer 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class AFK(BASE): 9 | __tablename__ = "afk_users" 10 | 11 | user_id = Column(Integer, primary_key=True) 12 | is_afk = Column(Boolean) 13 | reason = Column(UnicodeText) 14 | 15 | def __init__(self, user_id, reason="", is_afk=True): 16 | self.user_id = user_id 17 | self.reason = reason 18 | self.is_afk = is_afk 19 | 20 | def __repr__(self): 21 | return "afk_status for {}".format(self.user_id) 22 | 23 | 24 | AFK.__table__.create(checkfirst=True) 25 | INSERTION_LOCK = threading.RLock() 26 | 27 | AFK_USERS = {} 28 | 29 | 30 | def is_afk(user_id): 31 | return user_id in AFK_USERS 32 | 33 | 34 | def check_afk_status(user_id): 35 | if user_id in AFK_USERS: 36 | return True, AFK_USERS[user_id] 37 | return False, "" 38 | 39 | 40 | def set_afk(user_id, reason=""): 41 | with INSERTION_LOCK: 42 | curr = SESSION.query(AFK).get(user_id) 43 | if not curr: 44 | curr = AFK(user_id, reason, True) 45 | else: 46 | curr.is_afk = True 47 | curr.reason = reason 48 | 49 | AFK_USERS[user_id] = reason 50 | 51 | SESSION.add(curr) 52 | SESSION.commit() 53 | 54 | 55 | def rm_afk(user_id): 56 | with INSERTION_LOCK: 57 | curr = SESSION.query(AFK).get(user_id) 58 | if curr: 59 | if user_id in AFK_USERS: # sanity check 60 | del AFK_USERS[user_id] 61 | 62 | SESSION.delete(curr) 63 | SESSION.commit() 64 | return True 65 | 66 | SESSION.close() 67 | return False 68 | 69 | 70 | def __load_afk_users(): 71 | global AFK_USERS 72 | try: 73 | all_afk = SESSION.query(AFK).all() 74 | AFK_USERS = {user.user_id: user.reason for user in all_afk if user.is_afk} 75 | finally: 76 | SESSION.close() 77 | 78 | 79 | __load_afk_users() 80 | -------------------------------------------------------------------------------- /tg_bot/sample_config.py: -------------------------------------------------------------------------------- 1 | if not __name__.endswith("sample_config"): 2 | import sys 3 | print("The README is there to be read. Extend this sample config to a config file, don't just rename and change " 4 | "values here. Doing that WILL backfire on you.\nBot quitting.", file=sys.stderr) 5 | quit(1) 6 | 7 | 8 | # Create a new config.py file in same dir and import, then extend this class. 9 | class Config(object): 10 | LOGGER = True 11 | 12 | # REQUIRED 13 | API_KEY = "YOUR KEY HERE" 14 | OWNER_ID = "YOUR ID HERE" # If you dont know, run the bot and do /id in your private chat with it 15 | OWNER_USERNAME = "YOUR USERNAME HERE" 16 | 17 | # RECOMMENDED 18 | SQLALCHEMY_DATABASE_URI = 'sqldbtype://username:pw@hostname:port/db_name' # needed for any database modules 19 | MESSAGE_DUMP = None # needed to make sure 'save from' messages persist 20 | LOAD = [] 21 | NO_LOAD = ['char_limit_exceed', 'translation', 'rss', 'sed'] 22 | WEBHOOK = False 23 | URL = None 24 | 25 | # OPTIONAL 26 | SUDO_USERS = [] # List of id's (not usernames) for users which have sudo access to the bot. 27 | SUPPORT_USERS = [] # List of id's (not usernames) for users which are allowed to gban, but can also be banned. 28 | WHITELIST_USERS = [] # List of id's (not usernames) for users which WONT be banned/kicked by the bot. 29 | DONATION_LINK = None # EG, paypal 30 | CERT_PATH = None 31 | PORT = 5000 32 | DEL_CMDS = False # Whether or not you should delete "blue text must click" commands 33 | STRICT_GBAN = False 34 | WORKERS = 8 # Number of subthreads to use. This is the recommended amount - see for yourself what works best! 35 | BAN_STICKER = 'CAADAgADOwADPPEcAXkko5EB3YGYAg' # banhammer marie sticker 36 | ALLOW_EXCL = False # Allow ! commands as well as / 37 | BMERNU_SCUT_SRELFTI = None 38 | 39 | START_MESSAGE = "https://t.me/c/1235155926/33801" 40 | START_BUTTONS = None 41 | 42 | class Production(Config): 43 | LOGGER = False 44 | 45 | 46 | class Development(Config): 47 | LOGGER = True 48 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/pin_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Integer, Column, String, func, distinct, Boolean 4 | from sqlalchemy.dialects import postgresql 5 | 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class SPinSettings(BASE): 10 | __tablename__ = "pin_settings" 11 | 12 | chat_id = Column(String(14), primary_key=True) 13 | message_id = Column(Integer) 14 | suacpmo = Column(Boolean, default=False) 15 | scldpmo = Column(Boolean, default=False) 16 | 17 | def __init__(self, chat_id, message_id): 18 | self.chat_id = str(chat_id) 19 | self.message_id = message_id 20 | 21 | 22 | def __repr__(self): 23 | return "".format(self.chat_id, self.message_id) 24 | 25 | 26 | SPinSettings.__table__.create(checkfirst=True) 27 | 28 | PIN_INSERTION_LOCK = threading.RLock() 29 | 30 | 31 | def add_mid(chat_id, message_id): 32 | with PIN_INSERTION_LOCK: 33 | chat = SESSION.query(SPinSettings).get(str(chat_id)) 34 | if not chat: 35 | chat = SPinSettings(str(chat_id), message_id) 36 | SESSION.add(chat) 37 | SESSION.commit() 38 | SESSION.close() 39 | 40 | 41 | def remove_mid(chat_id): 42 | with PIN_INSERTION_LOCK: 43 | chat = SESSION.query(SPinSettings).get(str(chat_id)) 44 | if chat: 45 | SESSION.delete(chat) 46 | SESSION.commit() 47 | SESSION.close() 48 | 49 | 50 | def add_acp_o(chat_id, setting): 51 | with PIN_INSERTION_LOCK: 52 | chat = SESSION.query(SPinSettings).get(str(chat_id)) 53 | if not chat: 54 | chat = SPinSettings(str(chat_id), 0) 55 | chat.suacpmo = setting 56 | SESSION.add(chat) 57 | SESSION.commit() 58 | SESSION.close() 59 | 60 | 61 | def add_ldp_m(chat_id, setting): 62 | with PIN_INSERTION_LOCK: 63 | chat = SESSION.query(SPinSettings).get(str(chat_id)) 64 | if not chat: 65 | chat = SPinSettings(str(chat_id), 0) 66 | chat.scldpmo = setting 67 | SESSION.add(chat) 68 | SESSION.commit() 69 | SESSION.close() 70 | 71 | 72 | def get_current_settings(chat_id): 73 | with PIN_INSERTION_LOCK: 74 | chat = SESSION.query(SPinSettings).get(str(chat_id)) 75 | return chat 76 | 77 | 78 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/log_channel_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, func, distinct 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class GroupLogs(BASE): 9 | __tablename__ = "log_channels" 10 | chat_id = Column(String(14), primary_key=True) 11 | log_channel = Column(String(14), nullable=False) 12 | 13 | def __init__(self, chat_id, log_channel): 14 | self.chat_id = str(chat_id) 15 | self.log_channel = str(log_channel) 16 | 17 | 18 | GroupLogs.__table__.create(checkfirst=True) 19 | 20 | LOGS_INSERTION_LOCK = threading.RLock() 21 | 22 | CHANNELS = {} 23 | 24 | 25 | def set_chat_log_channel(chat_id, log_channel): 26 | with LOGS_INSERTION_LOCK: 27 | res = SESSION.query(GroupLogs).get(str(chat_id)) 28 | if res: 29 | res.log_channel = log_channel 30 | else: 31 | res = GroupLogs(chat_id, log_channel) 32 | SESSION.add(res) 33 | 34 | CHANNELS[str(chat_id)] = log_channel 35 | SESSION.commit() 36 | 37 | 38 | def get_chat_log_channel(chat_id): 39 | return CHANNELS.get(str(chat_id)) 40 | 41 | 42 | def stop_chat_logging(chat_id): 43 | with LOGS_INSERTION_LOCK: 44 | res = SESSION.query(GroupLogs).get(str(chat_id)) 45 | if res: 46 | if str(chat_id) in CHANNELS: 47 | del CHANNELS[str(chat_id)] 48 | 49 | log_channel = res.log_channel 50 | SESSION.delete(res) 51 | SESSION.commit() 52 | return log_channel 53 | 54 | 55 | def num_logchannels(): 56 | try: 57 | return SESSION.query(func.count(distinct(GroupLogs.chat_id))).scalar() 58 | finally: 59 | SESSION.close() 60 | 61 | 62 | def migrate_chat(old_chat_id, new_chat_id): 63 | with LOGS_INSERTION_LOCK: 64 | chat = SESSION.query(GroupLogs).get(str(old_chat_id)) 65 | if chat: 66 | chat.chat_id = str(new_chat_id) 67 | SESSION.add(chat) 68 | if str(old_chat_id) in CHANNELS: 69 | CHANNELS[str(new_chat_id)] = CHANNELS.get(str(old_chat_id)) 70 | 71 | SESSION.commit() 72 | 73 | 74 | def __load_log_channels(): 75 | global CHANNELS 76 | try: 77 | all_chats = SESSION.query(GroupLogs).all() 78 | CHANNELS = {chat.chat_id: chat.log_channel for chat in all_chats} 79 | finally: 80 | SESSION.close() 81 | 82 | 83 | __load_log_channels() 84 | -------------------------------------------------------------------------------- /tg_bot/modules/keyboard.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from typing import List, Dict 3 | 4 | from telegram import Bot, ParseMode, ReplyKeyboardMarkup, KeyboardButton 5 | from telegram.error import TelegramError 6 | 7 | from tg_bot import dispatcher 8 | #from tg_bot.modules.translations.strings import tld 9 | from telegram.ext import CommandHandler, Filters, MessageHandler, CallbackQueryHandler 10 | 11 | import tg_bot.modules.sql.connection_sql as con_sql 12 | 13 | def keyboard(bot, update): 14 | user = update.effective_user # type: Optional[User] 15 | conn_id = con_sql.get_connected_chat(user.id) 16 | if conn_id and not conn_id == False: 17 | btn1 = "/disconnect - Disconnect from chat" 18 | btn2 = "" 19 | btn3 = "" 20 | else: 21 | if con_sql.get_history(user.id): 22 | history = con_sql.get_history(user.id) 23 | try: 24 | chat_name1 = dispatcher.bot.getChat(history.chat_id1).title 25 | except: 26 | chat_name1 = "" 27 | 28 | try: 29 | chat_name2 = dispatcher.bot.getChat(history.chat_id2).title 30 | except: 31 | chat_name2 = "" 32 | 33 | try: 34 | chat_name3 = dispatcher.bot.getChat(history.chat_id3).title 35 | except: 36 | chat_name3 = "" 37 | 38 | if chat_name1: 39 | btn1 = "/connect {} - {}".format(history.chat_id1, chat_name1) 40 | else: 41 | btn1 = "/connect - Connect to the chat" 42 | if chat_name2: 43 | btn2 = "/connect {} - {}".format(history.chat_id2, chat_name2) 44 | else: 45 | btn2 = "" 46 | if chat_name3: 47 | btn3 = "/connect {} - {}".format(history.chat_id3, chat_name3) 48 | else: 49 | btn3 = "" 50 | 51 | #TODO: Remove except garbage 52 | 53 | update.effective_message.reply_text("keyboard updated", 54 | reply_markup=ReplyKeyboardMarkup([[ 55 | KeyboardButton("/help - Bot Help"), 56 | KeyboardButton("/donate - Donate"), 57 | KeyboardButton("/notes - Notes")], 58 | [KeyboardButton(btn1)], 59 | [KeyboardButton(btn2)], 60 | [KeyboardButton(btn3)]])) 61 | 62 | 63 | KEYBOARD_HANDLER = CommandHandler(["keyboard"], keyboard) 64 | dispatcher.add_handler(KEYBOARD_HANDLER) 65 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/rss_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class RSS(BASE): 9 | __tablename__ = "rss_feed" 10 | id = Column(Integer, primary_key=True) 11 | chat_id = Column(UnicodeText, nullable=False) 12 | feed_link = Column(UnicodeText) 13 | old_entry_link = Column(UnicodeText) 14 | 15 | def __init__(self, chat_id, feed_link, old_entry_link): 16 | self.chat_id = chat_id 17 | self.feed_link = feed_link 18 | self.old_entry_link = old_entry_link 19 | 20 | def __repr__(self): 21 | return "".format(self.chat_id, 22 | self.feed_link, 23 | self.old_entry_link) 24 | 25 | 26 | RSS.__table__.create(checkfirst=True) 27 | INSERTION_LOCK = threading.RLock() 28 | 29 | 30 | def check_url_availability(tg_chat_id, tg_feed_link): 31 | try: 32 | return SESSION.query(RSS).filter(RSS.feed_link == tg_feed_link, 33 | RSS.chat_id == tg_chat_id).all() 34 | finally: 35 | SESSION.close() 36 | 37 | 38 | def add_url(tg_chat_id, tg_feed_link, tg_old_entry_link): 39 | with INSERTION_LOCK: 40 | action = RSS(tg_chat_id, tg_feed_link, tg_old_entry_link) 41 | 42 | SESSION.add(action) 43 | SESSION.commit() 44 | 45 | 46 | def remove_url(tg_chat_id, tg_feed_link): 47 | with INSERTION_LOCK: 48 | # this loops to delete any possible duplicates for the same TG User ID, TG Chat ID and link 49 | for row in check_url_availability(tg_chat_id, tg_feed_link): 50 | # add the action to the DB query 51 | SESSION.delete(row) 52 | 53 | SESSION.commit() 54 | 55 | 56 | def get_urls(tg_chat_id): 57 | try: 58 | return SESSION.query(RSS).filter(RSS.chat_id == tg_chat_id).all() 59 | finally: 60 | SESSION.close() 61 | 62 | 63 | def get_all(): 64 | try: 65 | return SESSION.query(RSS).all() 66 | finally: 67 | SESSION.close() 68 | 69 | 70 | def update_url(row_id, new_entry_links): 71 | with INSERTION_LOCK: 72 | row = SESSION.query(RSS).get(row_id) 73 | 74 | # set the new old_entry_link with the latest update from the RSS Feed 75 | row.old_entry_link = new_entry_links[0] 76 | 77 | # commit the changes to the DB 78 | SESSION.commit() 79 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/userinfo_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class UserInfo(BASE): 9 | __tablename__ = "userinfo" 10 | user_id = Column(Integer, primary_key=True) 11 | info = Column(UnicodeText) 12 | 13 | def __init__(self, user_id, info): 14 | self.user_id = user_id 15 | self.info = info 16 | 17 | def __repr__(self): 18 | return "" % self.user_id 19 | 20 | 21 | class UserBio(BASE): 22 | __tablename__ = "userbio" 23 | user_id = Column(Integer, primary_key=True) 24 | bio = Column(UnicodeText) 25 | 26 | def __init__(self, user_id, bio): 27 | self.user_id = user_id 28 | self.bio = bio 29 | 30 | def __repr__(self): 31 | return "" % self.user_id 32 | 33 | 34 | UserInfo.__table__.create(checkfirst=True) 35 | UserBio.__table__.create(checkfirst=True) 36 | 37 | INSERTION_LOCK = threading.RLock() 38 | 39 | 40 | def get_user_me_info(user_id): 41 | userinfo = SESSION.query(UserInfo).get(user_id) 42 | SESSION.close() 43 | if userinfo: 44 | return userinfo.info 45 | return None 46 | 47 | 48 | def set_user_me_info(user_id, info): 49 | with INSERTION_LOCK: 50 | userinfo = SESSION.query(UserInfo).get(user_id) 51 | if userinfo: 52 | userinfo.info = info 53 | else: 54 | userinfo = UserInfo(user_id, info) 55 | SESSION.add(userinfo) 56 | SESSION.commit() 57 | 58 | 59 | def get_user_bio(user_id): 60 | userbio = SESSION.query(UserBio).get(user_id) 61 | SESSION.close() 62 | if userbio: 63 | return userbio.bio 64 | return None 65 | 66 | 67 | def set_user_bio(user_id, bio): 68 | with INSERTION_LOCK: 69 | userbio = SESSION.query(UserBio).get(user_id) 70 | if userbio: 71 | userbio.bio = bio 72 | else: 73 | userbio = UserBio(user_id, bio) 74 | 75 | SESSION.add(userbio) 76 | SESSION.commit() 77 | 78 | 79 | def clear_user_info(user_id): 80 | with INSERTION_LOCK: 81 | curr = SESSION.query(UserInfo).get(user_id) 82 | if curr: 83 | SESSION.delete(curr) 84 | SESSION.commit() 85 | return True 86 | 87 | SESSION.close() 88 | return False 89 | 90 | 91 | def clear_user_bio(user_id): 92 | with INSERTION_LOCK: 93 | curr = SESSION.query(UserBio).get(user_id) 94 | if curr: 95 | SESSION.delete(curr) 96 | SESSION.commit() 97 | return True 98 | 99 | SESSION.close() 100 | return False 101 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/antiflood_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, String 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | DEF_COUNT = 0 8 | DEF_LIMIT = 0 9 | DEF_OBJ = (None, DEF_COUNT, DEF_LIMIT) 10 | 11 | 12 | class FloodControl(BASE): 13 | __tablename__ = "antiflood" 14 | chat_id = Column(String(14), primary_key=True) 15 | user_id = Column(Integer) 16 | count = Column(Integer, default=DEF_COUNT) 17 | limit = Column(Integer, default=DEF_LIMIT) 18 | 19 | def __init__(self, chat_id): 20 | self.chat_id = str(chat_id) # ensure string 21 | 22 | def __repr__(self): 23 | return "" % self.chat_id 24 | 25 | 26 | FloodControl.__table__.create(checkfirst=True) 27 | 28 | INSERTION_LOCK = threading.RLock() 29 | 30 | CHAT_FLOOD = {} 31 | 32 | 33 | def set_flood(chat_id, amount): 34 | with INSERTION_LOCK: 35 | flood = SESSION.query(FloodControl).get(str(chat_id)) 36 | if not flood: 37 | flood = FloodControl(str(chat_id)) 38 | 39 | flood.user_id = None 40 | flood.limit = amount 41 | 42 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, amount) 43 | 44 | SESSION.add(flood) 45 | SESSION.commit() 46 | 47 | 48 | def update_flood(chat_id: str, user_id) -> bool: 49 | if str(chat_id) in CHAT_FLOOD: 50 | curr_user_id, count, limit = CHAT_FLOOD.get(str(chat_id), DEF_OBJ) 51 | 52 | if limit == 0: # no antiflood 53 | return False 54 | 55 | if user_id != curr_user_id or user_id is None: # other user 56 | CHAT_FLOOD[str(chat_id)] = (user_id, DEF_COUNT + 1, limit) 57 | return False 58 | 59 | count += 1 60 | if count > limit: # too many msgs, kick 61 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, limit) 62 | return True 63 | 64 | # default -> update 65 | CHAT_FLOOD[str(chat_id)] = (user_id, count, limit) 66 | return False 67 | 68 | 69 | def get_flood_limit(chat_id): 70 | return CHAT_FLOOD.get(str(chat_id), DEF_OBJ)[2] 71 | 72 | 73 | def migrate_chat(old_chat_id, new_chat_id): 74 | with INSERTION_LOCK: 75 | flood = SESSION.query(FloodControl).get(str(old_chat_id)) 76 | if flood: 77 | CHAT_FLOOD[str(new_chat_id)] = CHAT_FLOOD.get(str(old_chat_id), DEF_OBJ) 78 | flood.chat_id = str(new_chat_id) 79 | SESSION.commit() 80 | 81 | SESSION.close() 82 | 83 | 84 | def __load_flood_settings(): 85 | global CHAT_FLOOD 86 | try: 87 | all_chats = SESSION.query(FloodControl).all() 88 | CHAT_FLOOD = {chat.chat_id: (None, DEF_COUNT, chat.limit) for chat in all_chats} 89 | finally: 90 | SESSION.close() 91 | 92 | 93 | __load_flood_settings() 94 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/reporting_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from sqlalchemy import Column, Integer, String, Boolean 5 | 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class ReportingUserSettings(BASE): 10 | __tablename__ = "user_report_settings" 11 | user_id = Column(Integer, primary_key=True) 12 | should_report = Column(Boolean, default=True) 13 | 14 | def __init__(self, user_id): 15 | self.user_id = user_id 16 | 17 | def __repr__(self): 18 | return "".format(self.user_id) 19 | 20 | 21 | class ReportingChatSettings(BASE): 22 | __tablename__ = "chat_report_settings" 23 | chat_id = Column(String(14), primary_key=True) 24 | should_report = Column(Boolean, default=True) 25 | 26 | def __init__(self, chat_id): 27 | self.chat_id = str(chat_id) 28 | 29 | def __repr__(self): 30 | return "".format(self.chat_id) 31 | 32 | 33 | ReportingUserSettings.__table__.create(checkfirst=True) 34 | ReportingChatSettings.__table__.create(checkfirst=True) 35 | 36 | CHAT_LOCK = threading.RLock() 37 | USER_LOCK = threading.RLock() 38 | 39 | 40 | def chat_should_report(chat_id: Union[str, int]) -> bool: 41 | try: 42 | chat_setting = SESSION.query(ReportingChatSettings).get(str(chat_id)) 43 | if chat_setting: 44 | return chat_setting.should_report 45 | return False 46 | finally: 47 | SESSION.close() 48 | 49 | 50 | def user_should_report(user_id: int) -> bool: 51 | try: 52 | user_setting = SESSION.query(ReportingUserSettings).get(user_id) 53 | if user_setting: 54 | return user_setting.should_report 55 | return True 56 | finally: 57 | SESSION.close() 58 | 59 | 60 | def set_chat_setting(chat_id: Union[int, str], setting: bool): 61 | with CHAT_LOCK: 62 | chat_setting = SESSION.query(ReportingChatSettings).get(str(chat_id)) 63 | if not chat_setting: 64 | chat_setting = ReportingChatSettings(chat_id) 65 | 66 | chat_setting.should_report = setting 67 | SESSION.add(chat_setting) 68 | SESSION.commit() 69 | 70 | 71 | def set_user_setting(user_id: int, setting: bool): 72 | with USER_LOCK: 73 | user_setting = SESSION.query(ReportingUserSettings).get(user_id) 74 | if not user_setting: 75 | user_setting = ReportingUserSettings(user_id) 76 | 77 | user_setting.should_report = setting 78 | SESSION.add(user_setting) 79 | SESSION.commit() 80 | 81 | 82 | def migrate_chat(old_chat_id, new_chat_id): 83 | with CHAT_LOCK: 84 | chat_notes = SESSION.query(ReportingChatSettings).filter( 85 | ReportingChatSettings.chat_id == str(old_chat_id)).all() 86 | for note in chat_notes: 87 | note.chat_id = str(new_chat_id) 88 | SESSION.commit() 89 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/extraction.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from telegram import Message, MessageEntity 4 | from telegram.error import BadRequest 5 | 6 | from tg_bot import LOGGER 7 | from tg_bot.modules.users import get_user_id 8 | 9 | 10 | def id_from_reply(message): 11 | prev_message = message.reply_to_message 12 | if not prev_message: 13 | return None, None 14 | user_id = prev_message.from_user.id 15 | res = message.text.split(None, 1) 16 | if len(res) < 2: 17 | return user_id, "" 18 | return user_id, res[1] 19 | 20 | 21 | def extract_user(message: Message, args: List[str]) -> Optional[int]: 22 | return extract_user_and_text(message, args)[0] 23 | 24 | 25 | def extract_user_and_text(message: Message, args: List[str]) -> (Optional[int], Optional[str]): 26 | prev_message = message.reply_to_message 27 | split_text = message.text.split(None, 1) 28 | 29 | if len(split_text) < 2: 30 | return id_from_reply(message) # only option possible 31 | 32 | text_to_parse = split_text[1] 33 | 34 | text = "" 35 | 36 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 37 | if len(entities) > 0: 38 | ent = entities[0] 39 | else: 40 | ent = None 41 | 42 | # if entity offset matches (command end/text start) then all good 43 | if entities and ent and ent.offset == len(message.text) - len(text_to_parse): 44 | ent = entities[0] 45 | user_id = ent.user.id 46 | text = message.text[ent.offset + ent.length:] 47 | 48 | elif len(args) >= 1 and args[0][0] == '@': 49 | user = args[0] 50 | user_id = get_user_id(user) 51 | if not user_id: 52 | message.reply_text("ഞാൻ ഇങ്ങനെയൊരാളെ ഇവിടെയെങ്ങും കണ്ടിട്ടേയില്ല... ഇയാളുടെ വാലോ തലയോ എന്തെങ്കിലും എനിക്ക് ഒന്ന് അയച്ചു താ (മെസ്സേജ് ആയാലും മതി)... എന്നിട്ട് വേണം വേഗം പണി തുടങ്ങാൻ...") 53 | return None, None 54 | 55 | else: 56 | user_id = user_id 57 | res = message.text.split(None, 2) 58 | if len(res) >= 3: 59 | text = res[2] 60 | 61 | elif len(args) >= 1 and args[0].isdigit(): 62 | user_id = int(args[0]) 63 | res = message.text.split(None, 2) 64 | if len(res) >= 3: 65 | text = res[2] 66 | 67 | elif prev_message: 68 | user_id, text = id_from_reply(message) 69 | 70 | else: 71 | return None, None 72 | 73 | try: 74 | message.bot.get_chat(user_id) 75 | except BadRequest as excp: 76 | if excp.message in ("User_id_invalid", "Chat not found"): 77 | message.reply_text("ഞാൻ ഇങ്ങനെയൊരാളെ ഇവിടെയെങ്ങും കണ്ടിട്ടേയില്ല... ഇയാളുടെ വാലോ തലയോ എന്തെങ്കിലും എനിക്ക് ഒന്ന് അയച്ചു താ (മെസ്സേജ് ആയാലും മതി)... എന്നിട്ട് വേണം വേഗം പണി തുടങ്ങാൻ...") 78 | else: 79 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 80 | 81 | return None, None 82 | 83 | return user_id, text 84 | 85 | 86 | def extract_text(message) -> str: 87 | return message.text or message.caption or (message.sticker.emoji if message.sticker else None) 88 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/disable_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class Disable(BASE): 9 | __tablename__ = "disabled_commands" 10 | chat_id = Column(String(14), primary_key=True) 11 | command = Column(UnicodeText, primary_key=True) 12 | 13 | def __init__(self, chat_id, command): 14 | self.chat_id = chat_id 15 | self.command = command 16 | 17 | def __repr__(self): 18 | return "Disabled cmd {} in {}".format(self.command, self.chat_id) 19 | 20 | 21 | Disable.__table__.create(checkfirst=True) 22 | DISABLE_INSERTION_LOCK = threading.RLock() 23 | 24 | DISABLED = {} 25 | 26 | 27 | def disable_command(chat_id, disable): 28 | with DISABLE_INSERTION_LOCK: 29 | disabled = SESSION.query(Disable).get((str(chat_id), disable)) 30 | 31 | if not disabled: 32 | DISABLED.setdefault(str(chat_id), set()).add(disable) 33 | 34 | disabled = Disable(str(chat_id), disable) 35 | SESSION.add(disabled) 36 | SESSION.commit() 37 | return True 38 | 39 | SESSION.close() 40 | return False 41 | 42 | 43 | def enable_command(chat_id, enable): 44 | with DISABLE_INSERTION_LOCK: 45 | disabled = SESSION.query(Disable).get((str(chat_id), enable)) 46 | 47 | if disabled: 48 | if enable in DISABLED.get(str(chat_id)): # sanity check 49 | DISABLED.setdefault(str(chat_id), set()).remove(enable) 50 | 51 | SESSION.delete(disabled) 52 | SESSION.commit() 53 | return True 54 | 55 | SESSION.close() 56 | return False 57 | 58 | 59 | def is_command_disabled(chat_id, cmd): 60 | return cmd in DISABLED.get(str(chat_id), set()) 61 | 62 | 63 | def get_all_disabled(chat_id): 64 | return DISABLED.get(str(chat_id), set()) 65 | 66 | 67 | def num_chats(): 68 | try: 69 | return SESSION.query(func.count(distinct(Disable.chat_id))).scalar() 70 | finally: 71 | SESSION.close() 72 | 73 | 74 | def num_disabled(): 75 | try: 76 | return SESSION.query(Disable).count() 77 | finally: 78 | SESSION.close() 79 | 80 | 81 | def migrate_chat(old_chat_id, new_chat_id): 82 | with DISABLE_INSERTION_LOCK: 83 | chats = SESSION.query(Disable).filter(Disable.chat_id == str(old_chat_id)).all() 84 | for chat in chats: 85 | chat.chat_id = str(new_chat_id) 86 | SESSION.add(chat) 87 | 88 | if str(old_chat_id) in DISABLED: 89 | DISABLED[str(new_chat_id)] = DISABLED.get(str(old_chat_id), set()) 90 | 91 | SESSION.commit() 92 | 93 | 94 | def __load_disabled_commands(): 95 | global DISABLED 96 | try: 97 | all_chats = SESSION.query(Disable).all() 98 | for chat in all_chats: 99 | DISABLED.setdefault(chat.chat_id, set()).add(chat.command) 100 | 101 | finally: 102 | SESSION.close() 103 | 104 | 105 | __load_disabled_commands() 106 | -------------------------------------------------------------------------------- /tg_bot/modules/backups.py: -------------------------------------------------------------------------------- 1 | import json 2 | from io import BytesIO 3 | from typing import Optional 4 | 5 | from telegram import Message, Chat, Update, Bot 6 | from telegram.error import BadRequest 7 | from telegram.ext import CommandHandler, run_async 8 | 9 | from tg_bot import dispatcher, LOGGER 10 | from tg_bot.__main__ import DATA_IMPORT 11 | from tg_bot.modules.helper_funcs.chat_status import user_admin 12 | 13 | 14 | @run_async 15 | @user_admin 16 | def import_data(bot: Bot, update): 17 | msg = update.effective_message # type: Optional[Message] 18 | chat = update.effective_chat # type: Optional[Chat] 19 | # TODO: allow uploading doc with command, not just as reply 20 | # only work with a doc 21 | if msg.reply_to_message and msg.reply_to_message.document: 22 | try: 23 | file_info = bot.get_file(msg.reply_to_message.document.file_id) 24 | except BadRequest: 25 | msg.reply_text("Try downloading and reuploading the file as yourself before importing - this one seems " 26 | "to be iffy!") 27 | return 28 | 29 | with BytesIO() as file: 30 | file_info.download(out=file) 31 | file.seek(0) 32 | data = json.load(file) 33 | 34 | # only import one group 35 | if len(data) > 1 and str(chat.id) not in data: 36 | msg.reply_text("Theres more than one group here in this file, and none have the same chat id as this group " 37 | "- how do I choose what to import?") 38 | return 39 | 40 | # Select data source 41 | if str(chat.id) in data: 42 | data = data[str(chat.id)]['hashes'] 43 | else: 44 | data = data[list(data.keys())[0]]['hashes'] 45 | 46 | try: 47 | for mod in DATA_IMPORT: 48 | mod.__import_data__(str(chat.id), data) 49 | except Exception: 50 | msg.reply_text("An exception occured while restoring your data. The process may not be complete. If " 51 | "you're having issues with this, message @MarieSupport with your backup file so the " 52 | "issue can be debugged. My owners would be happy to help, and every bug " 53 | "reported makes me better! Thanks! :)") 54 | LOGGER.exception("Import for chatid %s with name %s failed.", str(chat.id), str(chat.title)) 55 | return 56 | 57 | # TODO: some of that link logic 58 | # NOTE: consider default permissions stuff? 59 | msg.reply_text("Backup fully imported. Welcome back! :D") 60 | 61 | 62 | @run_async 63 | @user_admin 64 | def export_data(bot: Bot, update: Update): 65 | msg = update.effective_message # type: Optional[Message] 66 | msg.reply_text("") 67 | 68 | 69 | __mod_name__ = "Backups" 70 | 71 | __help__ = """ 72 | *Admin only:* 73 | - /import: reply to a group butler backup file to import as much as possible, making the transfer super simple! Note \ 74 | that files/photos can't be imported due to telegram restrictions. 75 | - /export: !!! This isn't a command yet, but should be coming soon! 76 | """ 77 | IMPORT_HANDLER = CommandHandler("import", import_data) 78 | EXPORT_HANDLER = CommandHandler("export", export_data) 79 | 80 | dispatcher.add_handler(IMPORT_HANDLER) 81 | # dispatcher.add_handler(EXPORT_HANDLER) 82 | -------------------------------------------------------------------------------- /tg_bot/modules/zzzanticommand.py: -------------------------------------------------------------------------------- 1 | import html 2 | import json 3 | 4 | from typing import Optional, List 5 | 6 | import requests 7 | from telegram import Message, Chat, Update, Bot, MessageEntity 8 | from telegram.error import BadRequest 9 | from telegram import ParseMode 10 | from telegram.ext import CommandHandler, run_async, Filters, MessageHandler 11 | from telegram.utils.helpers import mention_markdown, mention_html, escape_markdown 12 | 13 | import tg_bot.modules.sql.welcome_sql as sql 14 | from tg_bot import dispatcher, LOGGER 15 | from tg_bot.modules.helper_funcs.chat_status import user_admin, can_delete 16 | from tg_bot.modules.log_channel import loggable 17 | 18 | 19 | @run_async 20 | @user_admin 21 | @loggable 22 | def rem_cmds(bot: Bot, update: Update, args: List[str]) -> str: 23 | chat = update.effective_chat # type: Optional[Chat] 24 | user = update.effective_user # type: Optional[User] 25 | 26 | if not args: 27 | del_pref = sql.get_cmd_pref(chat.id) 28 | if del_pref: 29 | update.effective_message.reply_text("നിലവിൽ @bluetextbot കമാന്റുകൾ ഡിലീറ്റ് ചെയ്യുന്നുണ്ട്.") 30 | else: 31 | update.effective_message.reply_text("നിലവിൽ @bluetextbot കമാന്റുകൾ ഡിലീറ്റ് ചെയ്യുന്നില്ല.") 32 | return "" 33 | 34 | if args[0].lower() in ("on", "yes"): 35 | sql.set_cmd_joined(str(chat.id), True) 36 | update.effective_message.reply_text("ശരി, @bluetextbot കമാന്റുകൾ ഡിലീറ്റ് ചെയ്യാൻ ശ്രമിക്കാം!") 37 | return "{}:" \ 38 | "\n#ANTI_COMMAND" \ 39 | "\nAdmin: {}" \ 40 | "\nHas toggled @AntiCommandBot to ON.".format(html.escape(chat.title), 41 | mention_html(user.id, user.first_name)) 42 | elif args[0].lower() in ("off", "no"): 43 | sql.set_cmd_joined(str(chat.id), False) 44 | update.effective_message.reply_text("ശരി, @bluetextbot കമാന്റുകൾ ഡിലീറ്റ് ചെയ്യില്ല!") 45 | return "{}:" \ 46 | "\n#ANTI_COMMAND" \ 47 | "\nAdmin: {}" \ 48 | "\nHas toggled @AntiCommandBot to OFF.".format(html.escape(chat.title), 49 | mention_html(user.id, user.first_name)) 50 | else: 51 | # idek what you're writing, say yes or no 52 | update.effective_message.reply_text("എന്താണ് ചെയ്യേണ്ടത് എന്നു മനസ്സിലായില്ല... 'on/yes' അല്ലെങ്കിൽ 'off/no' എന്ന് ചേർത്ത് അയക്കൂ!") 53 | return "" 54 | 55 | @run_async 56 | def rem_slash_commands(bot: Bot, update: Update) -> str: 57 | chat = update.effective_chat # type: Optional[Chat] 58 | msg = update.effective_message # type: Optional[Message] 59 | del_pref = sql.get_cmd_pref(chat.id) 60 | 61 | if del_pref: 62 | try: 63 | msg.delete() 64 | except BadRequest as excp: 65 | LOGGER.info(excp) 66 | 67 | 68 | __help__ = """ 69 | I remove messages starting with a /command in groups and supergroups. 70 | - /rmcmd : when someone tries to send a @BlueTextBot message, I will try to delete that! 71 | """ 72 | 73 | __mod_name__ = "anticommand" 74 | 75 | DEL_REM_COMMANDS = CommandHandler("rmcmd", rem_cmds, pass_args=True, filters=Filters.group) 76 | REM_SLASH_COMMANDS = MessageHandler(Filters.command & Filters.group, rem_slash_commands) 77 | 78 | dispatcher.add_handler(DEL_REM_COMMANDS) 79 | dispatcher.add_handler(REM_SLASH_COMMANDS) 80 | -------------------------------------------------------------------------------- /tg_bot/modules/afk.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity 5 | from telegram.ext import Filters, MessageHandler, run_async 6 | 7 | from tg_bot import dispatcher 8 | from tg_bot.modules.disable import DisableAbleCommandHandler, DisableAbleRegexHandler 9 | from tg_bot.modules.sql import afk_sql as sql 10 | from tg_bot.modules.users import get_user_id 11 | 12 | AFK_GROUP = 7 13 | AFK_REPLY_GROUP = 8 14 | 15 | 16 | @run_async 17 | def afk(bot: Bot, update: Update): 18 | args = update.effective_message.text.split(None, 1) 19 | if len(args) >= 2: 20 | reason = args[1] 21 | else: 22 | reason = "" 23 | 24 | sql.set_afk(update.effective_user.id, reason) 25 | update.effective_message.reply_text("{} ഇപ്പോൾ കീബോർഡിൽ നിന്നും അകലെ ആണ്!".format(update.effective_user.first_name)) 26 | 27 | 28 | @run_async 29 | def no_longer_afk(bot: Bot, update: Update): 30 | user = update.effective_user # type: Optional[User] 31 | 32 | if not user: # ignore channels 33 | return 34 | 35 | res = sql.rm_afk(user.id) 36 | if res: 37 | update.effective_message.reply_text("{} ഇപ്പോൾ കീബോർഡിന് സമീപം എത്തി!".format(update.effective_user.first_name)) 38 | 39 | 40 | @run_async 41 | def reply_afk(bot: Bot, update: Update): 42 | message = update.effective_message # type: Optional[Message] 43 | entities = message.parse_entities([MessageEntity.TEXT_MENTION, MessageEntity.MENTION]) 44 | if message.entities and entities: 45 | for ent in entities: 46 | if ent.type == MessageEntity.TEXT_MENTION: 47 | user_id = ent.user.id 48 | fst_name = ent.user.first_name 49 | 50 | elif ent.type == MessageEntity.MENTION: 51 | user_id = get_user_id(message.text[ent.offset:ent.offset + ent.length]) 52 | if not user_id: 53 | # Should never happen, since for a user to become AFK they must have spoken. Maybe changed username? 54 | return 55 | chat = bot.get_chat(user_id) 56 | fst_name = chat.first_name 57 | 58 | else: 59 | return 60 | 61 | if sql.is_afk(user_id): 62 | user = sql.check_afk_status(user_id) 63 | if not user.reason: 64 | res = "{} ഇപ്പോൾ കീബോർഡിൽ നിന്നും അകലെ ആണ്!".format(fst_name) 65 | else: 66 | res = "{} ഇപ്പോൾ കീബോർഡിൽ നിന്നും അകലെ ആണ്, കാരണം :\n{}".format(fst_name, user.reason) 67 | message.reply_text(res) 68 | 69 | def __gdpr__(user_id): 70 | sql.rm_afk(user_id) 71 | 72 | 73 | __help__ = """ 74 | - /afk : mark yourself as AFK. 75 | - brb : same as the afk command - but not a command. 76 | 77 | When marked as AFK, any mentions will be replied to with a message to say you're not available! 78 | """ 79 | 80 | __mod_name__ = "കീ നി അ" 81 | 82 | AFK_HANDLER = DisableAbleCommandHandler("afk", afk) 83 | AFK_REGEX_HANDLER = DisableAbleRegexHandler("(?i)brb", afk, friendly="afk") 84 | NO_AFK_HANDLER = MessageHandler(Filters.all & Filters.group, no_longer_afk, edited_updates=True) 85 | AFK_REPLY_HANDLER = MessageHandler(Filters.entity(MessageEntity.MENTION) | Filters.entity(MessageEntity.TEXT_MENTION), 86 | reply_afk, edited_updates=True) 87 | 88 | dispatcher.add_handler(AFK_HANDLER, AFK_GROUP) 89 | dispatcher.add_handler(AFK_REGEX_HANDLER, AFK_GROUP) 90 | dispatcher.add_handler(NO_AFK_HANDLER, AFK_GROUP) 91 | dispatcher.add_handler(AFK_REPLY_HANDLER, AFK_REPLY_GROUP) 92 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/blacklist_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import func, distinct, Column, String, UnicodeText 4 | 5 | from tg_bot.modules.sql import SESSION, BASE 6 | 7 | 8 | class BlackListFilters(BASE): 9 | __tablename__ = "blacklist" 10 | chat_id = Column(String(14), primary_key=True) 11 | trigger = Column(UnicodeText, primary_key=True, nullable=False) 12 | 13 | def __init__(self, chat_id, trigger): 14 | self.chat_id = str(chat_id) # ensure string 15 | self.trigger = trigger 16 | 17 | def __repr__(self): 18 | return "" % (self.trigger, self.chat_id) 19 | 20 | def __eq__(self, other): 21 | return bool(isinstance(other, BlackListFilters) 22 | and self.chat_id == other.chat_id 23 | and self.trigger == other.trigger) 24 | 25 | 26 | BlackListFilters.__table__.create(checkfirst=True) 27 | 28 | BLACKLIST_FILTER_INSERTION_LOCK = threading.RLock() 29 | 30 | CHAT_BLACKLISTS = {} 31 | 32 | 33 | def add_to_blacklist(chat_id, trigger): 34 | with BLACKLIST_FILTER_INSERTION_LOCK: 35 | blacklist_filt = BlackListFilters(str(chat_id), trigger) 36 | 37 | SESSION.merge(blacklist_filt) # merge to avoid duplicate key issues 38 | SESSION.commit() 39 | CHAT_BLACKLISTS.setdefault(str(chat_id), set()).add(trigger) 40 | 41 | 42 | def rm_from_blacklist(chat_id, trigger): 43 | with BLACKLIST_FILTER_INSERTION_LOCK: 44 | blacklist_filt = SESSION.query(BlackListFilters).get((str(chat_id), trigger)) 45 | if blacklist_filt: 46 | if trigger in CHAT_BLACKLISTS.get(str(chat_id), set()): # sanity check 47 | CHAT_BLACKLISTS.get(str(chat_id), set()).remove(trigger) 48 | 49 | SESSION.delete(blacklist_filt) 50 | SESSION.commit() 51 | return True 52 | 53 | SESSION.close() 54 | return False 55 | 56 | 57 | def get_chat_blacklist(chat_id): 58 | return CHAT_BLACKLISTS.get(str(chat_id), set()) 59 | 60 | 61 | def num_blacklist_filters(): 62 | try: 63 | return SESSION.query(BlackListFilters).count() 64 | finally: 65 | SESSION.close() 66 | 67 | 68 | def num_blacklist_chat_filters(chat_id): 69 | try: 70 | return SESSION.query(BlackListFilters.chat_id).filter(BlackListFilters.chat_id == str(chat_id)).count() 71 | finally: 72 | SESSION.close() 73 | 74 | 75 | def num_blacklist_filter_chats(): 76 | try: 77 | return SESSION.query(func.count(distinct(BlackListFilters.chat_id))).scalar() 78 | finally: 79 | SESSION.close() 80 | 81 | 82 | def __load_chat_blacklists(): 83 | global CHAT_BLACKLISTS 84 | try: 85 | chats = SESSION.query(BlackListFilters.chat_id).distinct().all() 86 | for (chat_id,) in chats: # remove tuple by ( ,) 87 | CHAT_BLACKLISTS[chat_id] = [] 88 | 89 | all_filters = SESSION.query(BlackListFilters).all() 90 | for x in all_filters: 91 | CHAT_BLACKLISTS[x.chat_id] += [x.trigger] 92 | 93 | CHAT_BLACKLISTS = {x: set(y) for x, y in CHAT_BLACKLISTS.items()} 94 | 95 | finally: 96 | SESSION.close() 97 | 98 | 99 | def migrate_chat(old_chat_id, new_chat_id): 100 | with BLACKLIST_FILTER_INSERTION_LOCK: 101 | chat_filters = SESSION.query(BlackListFilters).filter(BlackListFilters.chat_id == str(old_chat_id)).all() 102 | for filt in chat_filters: 103 | filt.chat_id = str(new_chat_id) 104 | SESSION.commit() 105 | 106 | 107 | __load_chat_blacklists() 108 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Marie", 3 | "description": "Telegram's sassiest group manager. Modular Telegram group management bot!", 4 | "logo": "https://telegra.ph/file/1925c7a2f601539ccde7a.png", 5 | "keywords": [ 6 | "telegram", 7 | "best", 8 | "group", 9 | "manager", 10 | "3", 11 | "plugin", 12 | "modular", 13 | "productivity" 14 | ], 15 | "repository": "https://github.com/PaulSonOfLars/tgbot", 16 | "website": "https://t.me/iamidiotareyoutoo", 17 | "success_url": "https://t.me/iamidiotareyoutoo/2", 18 | "env": { 19 | "ENV": { 20 | "description": "Setting this to ANYTHING will enable env variables. But, you need to understand the code to know what to set here!", 21 | "value": "ANYTHING" 22 | }, 23 | "TOKEN": { 24 | "description": "Your bot token, as a string." 25 | }, 26 | "OWNER_ID": { 27 | "description": "An integer of consisting of your owner ID", 28 | "value": "254318997" 29 | }, 30 | "OWNER_USERNAME": { 31 | "description": "Your username", 32 | "value": "SonOfLars" 33 | }, 34 | "WEBHOOK": { 35 | "description": "Setting this to ANYTHING will enable webhooks when in env mode messages", 36 | "value": "ANYTHING" 37 | }, 38 | "URL": { 39 | "description": "The Heroku App URL similar to https://.herokuapp.com/" 40 | }, 41 | "MESSAGE_DUMP": { 42 | "description": "optional: a chat where your replied saved messages are stored, to stop people deleting their old", 43 | "required": false 44 | }, 45 | "SUDO_USERS": { 46 | "description": "A space separated list of user_ids which should be considered sudo users", 47 | "value": "254318997 18673980 83489514" 48 | }, 49 | "SUPPORT_USERS": { 50 | "description": "A space separated list of user_ids which should be considered support users (can gban/ungban, nothing else)", 51 | "value": "254318997 18673980 83489514" 52 | }, 53 | "WHITELIST_USERS": { 54 | "description": "A space separated list of user_ids which should be considered whitelisted - they can't be banned.", 55 | "value": "254318997 18673980 83489514" 56 | }, 57 | "DONATION_LINK": { 58 | "description": "Optional: link where you would like to receive donations.", 59 | "value": "https://www.paypal.me/PaulSonOfLars" 60 | }, 61 | "PORT": { 62 | "description": "Port to use for your webhooks", 63 | "value": "8443" 64 | }, 65 | "DEL_CMDS": { 66 | "description": "Whether to delete commands from users which don't have rights to use that command", 67 | "value": "True" 68 | }, 69 | "STRICT_GBAN": { 70 | "description": "Enforce gbans across new groups as well as old groups. When a gbanned user talks, he will be banned.", 71 | "value": "True" 72 | }, 73 | "ALLOW_EXCL": { 74 | "description": "Whether to allow using exclamation marks ! for commands as well as /.", 75 | "value": "True" 76 | }, 77 | "BAN_STICKER": { 78 | "description": "Which sticker to use when banning people. Use https://telegram.dog/ShowJsonBot to get the file_id", 79 | "value": "CAADBQADfQADv7rGI0wxx1ORU7UzAg", 80 | "required": false 81 | }, 82 | "BMERNU_SCUT_SRELFTI": { 83 | "description": "please set this value as 0", 84 | "value": "1", 85 | "required": false 86 | } 87 | }, 88 | "addons": [ 89 | { 90 | "plan": "heroku-postgresql", 91 | "options": { 92 | "version": "12" 93 | } 94 | } 95 | ], 96 | "stack": "heroku-18", 97 | "buildpacks": [], 98 | "formation": { 99 | "web": { 100 | "quantity": 1, 101 | "size": "free" 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/connection_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer, func, distinct 5 | 6 | from tg_bot.modules.helper_funcs.msg_types import Types 7 | from tg_bot.modules.sql import SESSION, BASE 8 | 9 | 10 | class ChatAccessConnectionSettings(BASE): 11 | __tablename__ = "access_connection" 12 | chat_id = Column(String(14), primary_key=True) 13 | allow_connect_to_chat = Column(Boolean, default=True) 14 | 15 | def __init__(self, chat_id): 16 | self.chat_id = str(chat_id) 17 | 18 | def __repr__(self): 19 | return "".format(self.chat_id) 20 | 21 | 22 | class Connection(BASE): 23 | __tablename__ = "connection" 24 | user_id = Column(Integer, primary_key=True) 25 | chat_id = Column(String(14)) 26 | def __init__(self, user_id, chat_id): 27 | self.user_id = user_id 28 | self.chat_id = str(chat_id) #Ensure String 29 | 30 | 31 | class ConnectionHistory(BASE): 32 | __tablename__ = "connection_history5" 33 | user_id = Column(Integer, primary_key=True) 34 | chat_id1 = Column(String(14)) 35 | chat_id2 = Column(String(14)) 36 | chat_id3 = Column(String(14)) 37 | updated = Column(Integer) 38 | def __init__(self, user_id, chat_id1, chat_id2, chat_id3, updated): 39 | self.user_id = user_id 40 | self.chat_id1 = str(chat_id1) #Ensure String 41 | self.chat_id2 = str(chat_id2) #Ensure String 42 | self.chat_id3 = str(chat_id3) #Ensure String 43 | self.updated = updated 44 | 45 | ChatAccessConnectionSettings.__table__.create(checkfirst=True) 46 | Connection.__table__.create(checkfirst=True) 47 | ConnectionHistory.__table__.create(checkfirst=True) 48 | 49 | CHAT_ACCESS_LOCK = threading.RLock() 50 | CONNECTION_INSERTION_LOCK = threading.RLock() 51 | HISTORY_LOCK = threading.RLock() 52 | 53 | 54 | def add_history(user_id, chat_id1, chat_id2, chat_id3, updated): 55 | with HISTORY_LOCK: 56 | prev = SESSION.query(ConnectionHistory).get((int(user_id))) 57 | if prev: 58 | SESSION.delete(prev) 59 | history = ConnectionHistory(user_id, chat_id1, chat_id2, chat_id3, updated) 60 | SESSION.add(history) 61 | SESSION.commit() 62 | 63 | def get_history(user_id): 64 | try: 65 | return SESSION.query(ConnectionHistory).get(str(user_id)) 66 | finally: 67 | SESSION.close() 68 | 69 | 70 | def allow_connect_to_chat(chat_id: Union[str, int]) -> bool: 71 | try: 72 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get(str(chat_id)) 73 | if chat_setting: 74 | return chat_setting.allow_connect_to_chat 75 | return False 76 | finally: 77 | SESSION.close() 78 | 79 | 80 | def set_allow_connect_to_chat(chat_id: Union[int, str], setting: bool): 81 | with CHAT_ACCESS_LOCK: 82 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get(str(chat_id)) 83 | if not chat_setting: 84 | chat_setting = ChatAccessConnectionSettings(chat_id) 85 | 86 | chat_setting.allow_connect_to_chat = setting 87 | SESSION.add(chat_setting) 88 | SESSION.commit() 89 | 90 | 91 | def connect(user_id, chat_id): 92 | with CONNECTION_INSERTION_LOCK: 93 | prev = SESSION.query(Connection).get((int(user_id))) 94 | if prev: 95 | SESSION.delete(prev) 96 | connect_to_chat = Connection(int(user_id), chat_id) 97 | SESSION.add(connect_to_chat) 98 | SESSION.commit() 99 | return True 100 | 101 | 102 | def get_connected_chat(user_id): 103 | try: 104 | return SESSION.query(Connection).get((int(user_id))) 105 | finally: 106 | SESSION.close() 107 | 108 | 109 | def curr_connection(chat_id): 110 | try: 111 | return SESSION.query(Connection).get((str(chat_id))) 112 | finally : 113 | SESSION.close() 114 | 115 | 116 | 117 | def disconnect(user_id): 118 | with CONNECTION_INSERTION_LOCK: 119 | disconnect = SESSION.query(Connection).get((int(user_id))) 120 | if disconnect: 121 | SESSION.delete(disconnect) 122 | SESSION.commit() 123 | return True 124 | else: 125 | SESSION.close() 126 | return False -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/misc.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from typing import List, Dict 3 | 4 | from telegram import MAX_MESSAGE_LENGTH, InlineKeyboardButton, Bot, ParseMode 5 | from telegram.error import TelegramError 6 | 7 | from tg_bot import LOAD, NO_LOAD 8 | 9 | 10 | class EqInlineKeyboardButton(InlineKeyboardButton): 11 | def __eq__(self, other): 12 | return self.text == other.text 13 | 14 | def __lt__(self, other): 15 | return self.text < other.text 16 | 17 | def __gt__(self, other): 18 | return self.text > other.text 19 | 20 | 21 | def split_message(msg: str) -> List[str]: 22 | if len(msg) < MAX_MESSAGE_LENGTH: 23 | return [msg] 24 | 25 | else: 26 | lines = msg.splitlines(True) 27 | small_msg = "" 28 | result = [] 29 | for line in lines: 30 | if len(small_msg) + len(line) < MAX_MESSAGE_LENGTH: 31 | small_msg += line 32 | else: 33 | result.append(small_msg) 34 | small_msg = line 35 | else: 36 | # Else statement at the end of the for loop, so append the leftover string. 37 | result.append(small_msg) 38 | 39 | return result 40 | 41 | 42 | def paginate_modules(page_n: int, module_dict: Dict, prefix, chat=None) -> List: 43 | if not chat: 44 | modules = sorted( 45 | [EqInlineKeyboardButton(x.__mod_name__, 46 | callback_data="{}_module({})".format(prefix, x.__mod_name__.lower())) for x 47 | in module_dict.values()]) 48 | else: 49 | modules = sorted( 50 | [EqInlineKeyboardButton(x.__mod_name__, 51 | callback_data="{}_module({},{})".format(prefix, chat, x.__mod_name__.lower())) for x 52 | in module_dict.values()]) 53 | 54 | pairs = list(zip(modules[::2], modules[1::2])) 55 | 56 | if len(modules) % 2 == 1: 57 | pairs.append((modules[-1],)) 58 | 59 | max_num_pages = ceil(len(pairs) / 7) 60 | modulo_page = page_n % max_num_pages 61 | 62 | # can only have a certain amount of buttons side by side 63 | if len(pairs) > 7: 64 | pairs = pairs[modulo_page * 7:7 * (modulo_page + 1)] + [ 65 | (EqInlineKeyboardButton("<", callback_data="{}_prev({})".format(prefix, modulo_page)), 66 | EqInlineKeyboardButton(">", callback_data="{}_next({})".format(prefix, modulo_page)))] 67 | 68 | return pairs 69 | 70 | 71 | def send_to_list(bot: Bot, send_to: list, message: str, markdown=False, html=False) -> None: 72 | if html and markdown: 73 | raise Exception("Can only send with either markdown or HTML!") 74 | for user_id in set(send_to): 75 | try: 76 | if markdown: 77 | bot.send_message(user_id, message, parse_mode=ParseMode.MARKDOWN) 78 | elif html: 79 | bot.send_message(user_id, message, parse_mode=ParseMode.HTML) 80 | else: 81 | bot.send_message(user_id, message) 82 | except TelegramError: 83 | pass # ignore users who fail 84 | 85 | 86 | def build_keyboard(buttons): 87 | keyb = [] 88 | for btn in buttons: 89 | mybelru = btn.url 90 | ik = None 91 | cond_one = mybelru.startswith(("http", "tg://")) 92 | # to fix #33801 inconsistencies 93 | cond_two = ( 94 | "t.me/" in mybelru or 95 | "telegram.me/" in mybelru 96 | ) 97 | if cond_one or cond_two: 98 | ik = InlineKeyboardButton(btn.name, url=mybelru) 99 | """else: 100 | ik = InlineKeyboardButton(btn.name, callback_data=f"rsct_{btn.id}_33801")""" 101 | if ik: 102 | if btn.same_line and keyb: 103 | keyb[-1].append(ik) 104 | else: 105 | keyb.append([ik]) 106 | return keyb 107 | 108 | 109 | def revert_buttons(buttons): 110 | res = "" 111 | for btn in buttons: 112 | if btn.same_line: 113 | res += "\n[{}](buttonurl://{}:same)".format(btn.name, btn.url) 114 | else: 115 | res += "\n[{}](buttonurl://{})".format(btn.name, btn.url) 116 | 117 | return res 118 | 119 | 120 | def is_module_loaded(name): 121 | return (not LOAD or name in LOAD) and name not in NO_LOAD 122 | -------------------------------------------------------------------------------- /tg_bot/modules/rules.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, run_async, Filters 7 | from telegram.utils.helpers import escape_markdown 8 | 9 | import tg_bot.modules.sql.rules_sql as sql 10 | from tg_bot import dispatcher 11 | from tg_bot.modules.helper_funcs.chat_status import user_admin 12 | from tg_bot.modules.helper_funcs.string_handling import markdown_parser 13 | 14 | 15 | @run_async 16 | def get_rules(bot: Bot, update: Update): 17 | chat_id = update.effective_chat.id 18 | send_rules(update, chat_id) 19 | 20 | 21 | # Do not async - not from a handler 22 | def send_rules(update, chat_id, from_pm=False): 23 | bot = dispatcher.bot 24 | user = update.effective_user # type: Optional[User] 25 | try: 26 | chat = bot.get_chat(chat_id) 27 | except BadRequest as excp: 28 | if excp.message == "Chat not found" and from_pm: 29 | bot.send_message(user.id, "The rules shortcut for this chat hasn't been set properly! Ask admins to " 30 | "fix this.") 31 | return 32 | else: 33 | raise 34 | 35 | rules = sql.get_rules(chat_id) 36 | text = "The rules for *{}* are:\n\n{}".format(escape_markdown(chat.title), rules) 37 | 38 | if from_pm and rules: 39 | bot.send_message(user.id, text, parse_mode=ParseMode.MARKDOWN) 40 | elif from_pm: 41 | bot.send_message(user.id, "The group admins haven't set any rules for this chat yet. " 42 | "This probably doesn't mean it's lawless though...!") 43 | elif rules: 44 | update.effective_message.reply_text("Contact me in PM to get this group's rules.", 45 | reply_markup=InlineKeyboardMarkup( 46 | [[InlineKeyboardButton(text="Rules", 47 | url="t.me/{}?start={}".format(bot.username, 48 | chat_id))]])) 49 | else: 50 | update.effective_message.reply_text("The group admins haven't set any rules for this chat yet. " 51 | "This probably doesn't mean it's lawless though...!") 52 | 53 | 54 | @run_async 55 | @user_admin 56 | def set_rules(bot: Bot, update: Update): 57 | chat_id = update.effective_chat.id 58 | msg = update.effective_message # type: Optional[Message] 59 | raw_text = msg.text 60 | args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args 61 | if len(args) == 2: 62 | txt = args[1] 63 | offset = len(txt) - len(raw_text) # set correct offset relative to command 64 | markdown_rules = markdown_parser(txt, entities=msg.parse_entities(), offset=offset) 65 | 66 | sql.set_rules(chat_id, markdown_rules) 67 | update.effective_message.reply_text("Successfully set rules for this group.") 68 | 69 | 70 | @run_async 71 | @user_admin 72 | def clear_rules(bot: Bot, update: Update): 73 | chat_id = update.effective_chat.id 74 | sql.set_rules(chat_id, "") 75 | update.effective_message.reply_text("Successfully cleared rules!") 76 | 77 | 78 | def __stats__(): 79 | return "{} chats have rules set.".format(sql.num_chats()) 80 | 81 | 82 | def __import_data__(chat_id, data): 83 | # set chat rules 84 | rules = data.get('info', {}).get('rules', "") 85 | sql.set_rules(chat_id, rules) 86 | 87 | 88 | def __migrate__(old_chat_id, new_chat_id): 89 | sql.migrate_chat(old_chat_id, new_chat_id) 90 | 91 | 92 | def __chat_settings__(chat_id, user_id): 93 | return "This chat has had it's rules set: `{}`".format(bool(sql.get_rules(chat_id))) 94 | 95 | 96 | __help__ = """ 97 | - /rules: get the rules for this chat. 98 | 99 | *Admin only:* 100 | - /setrules : set the rules for this chat. 101 | - /clearrules: clear the rules for this chat. 102 | """ 103 | 104 | __mod_name__ = "Rules" 105 | 106 | GET_RULES_HANDLER = CommandHandler("rules", get_rules, filters=Filters.group) 107 | SET_RULES_HANDLER = CommandHandler("setrules", set_rules, filters=Filters.group) 108 | RESET_RULES_HANDLER = CommandHandler("clearrules", clear_rules, filters=Filters.group) 109 | 110 | dispatcher.add_handler(GET_RULES_HANDLER) 111 | dispatcher.add_handler(SET_RULES_HANDLER) 112 | dispatcher.add_handler(RESET_RULES_HANDLER) 113 | -------------------------------------------------------------------------------- /tg_bot/modules/msg_deleting.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, Filters 7 | from telegram.ext.dispatcher import run_async 8 | from telegram.utils.helpers import mention_html 9 | 10 | from tg_bot import dispatcher, LOGGER 11 | from tg_bot.modules.helper_funcs.chat_status import user_admin, can_delete 12 | from tg_bot.modules.log_channel import loggable 13 | 14 | 15 | @run_async 16 | @user_admin 17 | @loggable 18 | def purge(bot: Bot, update: Update, args: List[str]) -> str: 19 | msg = update.effective_message # type: Optional[Message] 20 | if msg.reply_to_message: 21 | user = update.effective_user # type: Optional[User] 22 | chat = update.effective_chat # type: Optional[Chat] 23 | if can_delete(chat, bot.id): 24 | message_id = msg.reply_to_message.message_id 25 | delete_to = msg.message_id - 1 26 | if args and args[0].isdigit(): 27 | new_del = message_id + int(args[0]) 28 | # No point deleting messages which haven't been written yet. 29 | if new_del < delete_to: 30 | delete_to = new_del 31 | else: 32 | delete_to = msg.message_id - 1 33 | for m_id in range(delete_to, message_id - 1, -1): # Reverse iteration over message ids 34 | try: 35 | bot.deleteMessage(chat.id, m_id) 36 | except BadRequest as err: 37 | if err.message == "Message can't be deleted": 38 | bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " 39 | "not have delete rights, or this might not be a supergroup.") 40 | 41 | elif err.message != "Message to delete not found": 42 | LOGGER.exception("Error while purging chat messages.") 43 | 44 | try: 45 | msg.delete() 46 | except BadRequest as err: 47 | if err.message == "Message can't be deleted": 48 | bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " 49 | "not have delete rights, or this might not be a supergroup.") 50 | 51 | elif err.message != "Message to delete not found": 52 | LOGGER.exception("Error while purging chat messages.") 53 | 54 | # Inspired by #28 on upstream 55 | # bot.send_message(chat.id, "Purge complete.") 56 | return "{}:" \ 57 | "\n#PURGE" \ 58 | "\nAdmin: {}" \ 59 | "\nPurged {} messages.".format(html.escape(chat.title), 60 | mention_html(user.id, user.first_name), 61 | delete_to - message_id) 62 | 63 | else: 64 | msg.reply_text("Reply to a message to select where to start purging from.") 65 | 66 | return "" 67 | 68 | 69 | @run_async 70 | @user_admin 71 | @loggable 72 | def del_message(bot: Bot, update: Update) -> str: 73 | if update.effective_message.reply_to_message: 74 | user = update.effective_user # type: Optional[User] 75 | chat = update.effective_chat # type: Optional[Chat] 76 | if can_delete(chat, bot.id): 77 | update.effective_message.reply_to_message.delete() 78 | update.effective_message.delete() 79 | return "{}:" \ 80 | "\n#DEL" \ 81 | "\nAdmin: {}" \ 82 | "\nMessage deleted.".format(html.escape(chat.title), 83 | mention_html(user.id, user.first_name)) 84 | else: 85 | update.effective_message.reply_text("Whadya want to delete?") 86 | 87 | return "" 88 | 89 | 90 | __help__ = """ 91 | *Admin only:* 92 | - /del: deletes the message you replied to 93 | - /purge: deletes all messages between this and the replied to message. 94 | - /purge : deletes the replied message, and X messages following it. 95 | """ 96 | 97 | __mod_name__ = "Purges" 98 | 99 | DELETE_HANDLER = CommandHandler("del", del_message, filters=Filters.group) 100 | PURGE_HANDLER = CommandHandler("purge", purge, filters=Filters.group, pass_args=True) 101 | 102 | dispatcher.add_handler(DELETE_HANDLER) 103 | dispatcher.add_handler(PURGE_HANDLER) 104 | -------------------------------------------------------------------------------- /tg_bot/modules/users.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from time import sleep 3 | from typing import Optional 4 | 5 | from telegram import TelegramError, Chat, Message 6 | from telegram import Update, Bot 7 | from telegram.error import BadRequest 8 | from telegram.ext import MessageHandler, Filters, CommandHandler 9 | from telegram.ext.dispatcher import run_async 10 | 11 | import tg_bot.modules.sql.users_sql as sql 12 | from tg_bot import dispatcher, OWNER_ID, LOGGER 13 | from tg_bot.modules.helper_funcs.filters import CustomFilters 14 | 15 | USERS_GROUP = 4 16 | 17 | 18 | def get_user_id(username): 19 | # ensure valid userid 20 | if len(username) <= 5: 21 | return None 22 | 23 | if username.startswith('@'): 24 | username = username[1:] 25 | 26 | users = sql.get_userid_by_name(username) 27 | 28 | if not users: 29 | return None 30 | 31 | elif len(users) == 1: 32 | return users[0].user_id 33 | 34 | else: 35 | for user_obj in users: 36 | try: 37 | userdat = dispatcher.bot.get_chat(user_obj.user_id) 38 | if userdat.username == username: 39 | return userdat.id 40 | 41 | except BadRequest as excp: 42 | if excp.message == 'Chat not found': 43 | pass 44 | else: 45 | LOGGER.exception("Error extracting user ID") 46 | 47 | return None 48 | 49 | 50 | @run_async 51 | def broadcast(bot: Bot, update: Update): 52 | to_send = update.effective_message.text.split(None, 1) 53 | if len(to_send) >= 2: 54 | chats = sql.get_all_chats() or [] 55 | failed = 0 56 | for chat in chats: 57 | try: 58 | bot.sendMessage(int(chat.chat_id), to_send[1]) 59 | sleep(0.1) 60 | except TelegramError: 61 | failed += 1 62 | LOGGER.warning("Couldn't send broadcast to %s, group name %s", str(chat.chat_id), str(chat.chat_name)) 63 | 64 | update.effective_message.reply_text("Broadcast complete. {} groups failed to receive the message, probably " 65 | "due to being kicked.".format(failed)) 66 | 67 | 68 | @run_async 69 | def log_user(bot: Bot, update: Update): 70 | chat = update.effective_chat # type: Optional[Chat] 71 | msg = update.effective_message # type: Optional[Message] 72 | 73 | sql.update_user(msg.from_user.id, 74 | msg.from_user.username, 75 | chat.id, 76 | chat.title) 77 | 78 | if msg.reply_to_message: 79 | sql.update_user(msg.reply_to_message.from_user.id, 80 | msg.reply_to_message.from_user.username, 81 | chat.id, 82 | chat.title) 83 | 84 | if msg.forward_from: 85 | sql.update_user(msg.forward_from.id, 86 | msg.forward_from.username) 87 | 88 | 89 | @run_async 90 | def chats(bot: Bot, update: Update): 91 | all_chats = sql.get_all_chats() or [] 92 | chatfile = 'List of chats.\n' 93 | for chat in all_chats: 94 | chatfile += "{} - ({})\n".format(chat.chat_name, chat.chat_id) 95 | 96 | with BytesIO(str.encode(chatfile)) as output: 97 | output.name = "chatlist.txt" 98 | update.effective_message.reply_document(document=output, filename="chatlist.txt", 99 | caption="എന്റെ ഡേറ്റാബേസിലെ ചാറ്റുകൾ ലിസ്റ്റ് ഇവിടെയാണ്.") 100 | 101 | 102 | def __user_info__(user_id): 103 | if user_id == dispatcher.bot.id: 104 | return """ഞാൻ അവരെ കണ്ട ... അവർ എന്നെ പിന്തുടരുന്നുണ്ടോ? അവർ ഒരേ സ്ഥലങ്ങളിലാണുള്ളത് ... ഓ. ഇത് ഞാനാണ്.""" 105 | num_chats = sql.get_user_num_chats(user_id) 106 | return """{} ചാറ്റുകളിൽ ഇയാളെ ഞാൻ കണ്ടിട്ടുണ്ട്.""".format(num_chats) 107 | 108 | 109 | def __stats__(): 110 | return "{} users, across {} chats".format(sql.num_users(), sql.num_chats()) 111 | 112 | 113 | def __gdpr__(user_id): 114 | sql.del_user(user_id) 115 | 116 | 117 | def __migrate__(old_chat_id, new_chat_id): 118 | sql.migrate_chat(old_chat_id, new_chat_id) 119 | 120 | 121 | __help__ = "" # no help string 122 | 123 | __mod_name__ = "Users" 124 | 125 | BROADCAST_HANDLER = CommandHandler("broadcast", broadcast, filters=Filters.user(OWNER_ID)) 126 | USER_HANDLER = MessageHandler(Filters.all & Filters.group, log_user, edited_updates=True) 127 | CHATLIST_HANDLER = CommandHandler("chatlist", chats, filters=CustomFilters.sudo_filter) 128 | 129 | dispatcher.add_handler(USER_HANDLER, USERS_GROUP) 130 | dispatcher.add_handler(BROADCAST_HANDLER) 131 | dispatcher.add_handler(CHATLIST_HANDLER) 132 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/global_bans_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer, String, Boolean 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class GloballyBannedUsers(BASE): 9 | __tablename__ = "gbans" 10 | user_id = Column(Integer, primary_key=True) 11 | name = Column(UnicodeText, nullable=False) 12 | reason = Column(UnicodeText) 13 | 14 | def __init__(self, user_id, name, reason=None): 15 | self.user_id = user_id 16 | self.name = name 17 | self.reason = reason 18 | 19 | def __repr__(self): 20 | return "".format(self.name, self.user_id) 21 | 22 | def to_dict(self): 23 | return {"user_id": self.user_id, 24 | "name": self.name, 25 | "reason": self.reason} 26 | 27 | 28 | class GbanSettings(BASE): 29 | __tablename__ = "gban_settings" 30 | chat_id = Column(String(14), primary_key=True) 31 | setting = Column(Boolean, default=True, nullable=False) 32 | 33 | def __init__(self, chat_id, enabled): 34 | self.chat_id = str(chat_id) 35 | self.setting = enabled 36 | 37 | def __repr__(self): 38 | return "".format(self.chat_id, self.setting) 39 | 40 | 41 | GloballyBannedUsers.__table__.create(checkfirst=True) 42 | GbanSettings.__table__.create(checkfirst=True) 43 | 44 | GBANNED_USERS_LOCK = threading.RLock() 45 | GBAN_SETTING_LOCK = threading.RLock() 46 | GBANNED_LIST = set() 47 | GBANSTAT_LIST = set() 48 | 49 | 50 | def gban_user(user_id, name, reason=None): 51 | with GBANNED_USERS_LOCK: 52 | user = SESSION.query(GloballyBannedUsers).get(user_id) 53 | if not user: 54 | user = GloballyBannedUsers(user_id, name, reason) 55 | else: 56 | user.name = name 57 | user.reason = reason 58 | 59 | SESSION.merge(user) 60 | SESSION.commit() 61 | __load_gbanned_userid_list() 62 | 63 | 64 | def update_gban_reason(user_id, name, reason=None): 65 | with GBANNED_USERS_LOCK: 66 | user = SESSION.query(GloballyBannedUsers).get(user_id) 67 | if not user: 68 | return None 69 | old_reason = user.reason 70 | user.name = name 71 | user.reason = reason 72 | 73 | SESSION.merge(user) 74 | SESSION.commit() 75 | return old_reason 76 | 77 | 78 | def ungban_user(user_id): 79 | with GBANNED_USERS_LOCK: 80 | user = SESSION.query(GloballyBannedUsers).get(user_id) 81 | if user: 82 | SESSION.delete(user) 83 | 84 | SESSION.commit() 85 | __load_gbanned_userid_list() 86 | 87 | 88 | def is_user_gbanned(user_id): 89 | return user_id in GBANNED_LIST 90 | 91 | 92 | def get_gbanned_user(user_id): 93 | try: 94 | return SESSION.query(GloballyBannedUsers).get(user_id) 95 | finally: 96 | SESSION.close() 97 | 98 | 99 | def get_gban_list(): 100 | try: 101 | return [x.to_dict() for x in SESSION.query(GloballyBannedUsers).all()] 102 | finally: 103 | SESSION.close() 104 | 105 | 106 | def enable_gbans(chat_id): 107 | with GBAN_SETTING_LOCK: 108 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 109 | if not chat: 110 | chat = GbanSettings(chat_id, True) 111 | 112 | chat.setting = True 113 | SESSION.add(chat) 114 | SESSION.commit() 115 | if str(chat_id) in GBANSTAT_LIST: 116 | GBANSTAT_LIST.remove(str(chat_id)) 117 | 118 | 119 | def disable_gbans(chat_id): 120 | with GBAN_SETTING_LOCK: 121 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 122 | if not chat: 123 | chat = GbanSettings(chat_id, False) 124 | 125 | chat.setting = False 126 | SESSION.add(chat) 127 | SESSION.commit() 128 | GBANSTAT_LIST.add(str(chat_id)) 129 | 130 | 131 | def does_chat_gban(chat_id): 132 | return str(chat_id) not in GBANSTAT_LIST 133 | 134 | 135 | def num_gbanned_users(): 136 | return len(GBANNED_LIST) 137 | 138 | 139 | def __load_gbanned_userid_list(): 140 | global GBANNED_LIST 141 | try: 142 | GBANNED_LIST = {x.user_id for x in SESSION.query(GloballyBannedUsers).all()} 143 | finally: 144 | SESSION.close() 145 | 146 | 147 | def __load_gban_stat_list(): 148 | global GBANSTAT_LIST 149 | try: 150 | GBANSTAT_LIST = {x.chat_id for x in SESSION.query(GbanSettings).all() if not x.setting} 151 | finally: 152 | SESSION.close() 153 | 154 | 155 | def migrate_chat(old_chat_id, new_chat_id): 156 | with GBAN_SETTING_LOCK: 157 | chat = SESSION.query(GbanSettings).get(str(old_chat_id)) 158 | if chat: 159 | chat.chat_id = new_chat_id 160 | SESSION.add(chat) 161 | 162 | SESSION.commit() 163 | 164 | 165 | # Create in memory userid to avoid disk access 166 | __load_gbanned_userid_list() 167 | __load_gban_stat_list() 168 | -------------------------------------------------------------------------------- /tg_bot/modules/sed.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sre_constants 3 | 4 | import telegram 5 | from telegram import Update, Bot 6 | from telegram.ext import run_async 7 | 8 | from tg_bot import dispatcher, LOGGER 9 | from tg_bot.modules.disable import DisableAbleRegexHandler 10 | 11 | DELIMITERS = ("/", ":", "|", "_") 12 | 13 | 14 | def separate_sed(sed_string): 15 | if len(sed_string) >= 3 and sed_string[1] in DELIMITERS and sed_string.count(sed_string[1]) >= 2: 16 | delim = sed_string[1] 17 | start = counter = 2 18 | while counter < len(sed_string): 19 | if sed_string[counter] == "\\": 20 | counter += 1 21 | 22 | elif sed_string[counter] == delim: 23 | replace = sed_string[start:counter] 24 | counter += 1 25 | start = counter 26 | break 27 | 28 | counter += 1 29 | 30 | else: 31 | return None 32 | 33 | while counter < len(sed_string): 34 | if sed_string[counter] == "\\" and counter + 1 < len(sed_string) and sed_string[counter + 1] == delim: 35 | sed_string = sed_string[:counter] + sed_string[counter + 1:] 36 | 37 | elif sed_string[counter] == delim: 38 | replace_with = sed_string[start:counter] 39 | counter += 1 40 | break 41 | 42 | counter += 1 43 | else: 44 | return replace, sed_string[start:], "" 45 | 46 | flags = "" 47 | if counter < len(sed_string): 48 | flags = sed_string[counter:] 49 | return replace, replace_with, flags.lower() 50 | 51 | 52 | @run_async 53 | def sed(bot: Bot, update: Update): 54 | sed_result = separate_sed(update.effective_message.text) 55 | if sed_result and update.effective_message.reply_to_message: 56 | if update.effective_message.reply_to_message.text: 57 | to_fix = update.effective_message.reply_to_message.text 58 | elif update.effective_message.reply_to_message.caption: 59 | to_fix = update.effective_message.reply_to_message.caption 60 | else: 61 | return 62 | 63 | repl, repl_with, flags = sed_result 64 | 65 | if not repl: 66 | update.effective_message.reply_to_message.reply_text("You're trying to replace... " 67 | "nothing with something?") 68 | return 69 | 70 | try: 71 | check = re.match(repl, to_fix, flags=re.IGNORECASE) 72 | 73 | if check and check.group(0).lower() == to_fix.lower(): 74 | update.effective_message.reply_to_message.reply_text("Hey everyone, {} is trying to make " 75 | "me say stuff I don't wanna " 76 | "say!".format(update.effective_user.first_name)) 77 | return 78 | 79 | if 'i' in flags and 'g' in flags: 80 | text = re.sub(repl, repl_with, to_fix, flags=re.I).strip() 81 | elif 'i' in flags: 82 | text = re.sub(repl, repl_with, to_fix, count=1, flags=re.I).strip() 83 | elif 'g' in flags: 84 | text = re.sub(repl, repl_with, to_fix).strip() 85 | else: 86 | text = re.sub(repl, repl_with, to_fix, count=1).strip() 87 | except sre_constants.error: 88 | LOGGER.warning(update.effective_message.text) 89 | LOGGER.exception("SRE constant error") 90 | update.effective_message.reply_text("താൻ എന്തൊരു തോൽവി ആണെടോ.. അറിയില്ലാത്ത പണിക്ക് എന്തിനാ നിക്കണത്... പോയി Sed എന്താണെന്ന് പഠിച്ചിട്ട് വാ...") 91 | return 92 | 93 | # empty string errors -_- 94 | if len(text) >= telegram.MAX_MESSAGE_LENGTH: 95 | update.effective_message.reply_text("The result of the sed command was too long for \ 96 | telegram!") 97 | elif text: 98 | update.effective_message.reply_to_message.reply_text(text) 99 | 100 | 101 | __help__ = """ 102 | - s//(/): Reply to a message with this to perform a sed operation on that message, replacing all \ 103 | occurrences of 'text1' with 'text2'. Flags are optional, and currently include 'i' for ignore case, 'g' for global, \ 104 | or nothing. Delimiters include `/`, `_`, `|`, and `:`. Text grouping is supported. The resulting message cannot be \ 105 | larger than {}. 106 | 107 | *Reminder:* Sed uses some special characters to make matching easier, such as these: `+*.?\\` 108 | If you want to use these characters, make sure you escape them! 109 | eg: \\?. 110 | """.format(telegram.MAX_MESSAGE_LENGTH) 111 | 112 | __mod_name__ = "Sed/Regex" 113 | 114 | 115 | SED_HANDLER = DisableAbleRegexHandler(r's([{}]).*?\1.*'.format("".join(DELIMITERS)), sed, friendly="sed") 116 | 117 | dispatcher.add_handler(SED_HANDLER) 118 | -------------------------------------------------------------------------------- /tg_bot/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import base64 5 | 6 | import telegram.ext as tg 7 | 8 | # enable logging 9 | logging.basicConfig( 10 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 11 | level=logging.INFO) 12 | 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | # if version < 3.6, stop bot. 16 | if sys.version_info[0] < 3 or sys.version_info[1] < 6: 17 | LOGGER.error("You MUST have a python version of at least 3.6! Multiple features depend on this. Bot quitting.") 18 | quit(1) 19 | 20 | ENV = os.environ.get('ENV', None) 21 | 22 | if ENV is not None: 23 | # kanged Developer verification from @deletescape 24 | if ENV != base64.b64decode("UFNPTEdDV0lJRExPU1A=").decode("UTF-8"): 25 | LOGGER.error("The README is there to be read. Extend this sample config to a config file, don't just rename and change " 26 | "values here. Doing that WILL backfire on you.\nBot quitting.") 27 | quit(1) 28 | # kanged Developer verification from @deletescape 29 | 30 | TOKEN = os.environ.get('TOKEN', None) 31 | try: 32 | OWNER_ID = int(os.environ.get('OWNER_ID', None)) 33 | except ValueError: 34 | raise Exception("Your OWNER_ID env variable is not a valid integer.") 35 | 36 | MESSAGE_DUMP = os.environ.get('MESSAGE_DUMP', None) 37 | OWNER_USERNAME = os.environ.get("OWNER_USERNAME", None) 38 | 39 | try: 40 | SUDO_USERS = set(int(x) for x in os.environ.get("SUDO_USERS", "").split()) 41 | except ValueError: 42 | raise Exception("Your sudo users list does not contain valid integers.") 43 | 44 | try: 45 | SUPPORT_USERS = set(int(x) for x in os.environ.get("SUPPORT_USERS", "").split()) 46 | except ValueError: 47 | raise Exception("Your support users list does not contain valid integers.") 48 | 49 | try: 50 | WHITELIST_USERS = set(int(x) for x in os.environ.get("WHITELIST_USERS", "").split()) 51 | except ValueError: 52 | raise Exception("Your whitelisted users list does not contain valid integers.") 53 | 54 | WEBHOOK = bool(os.environ.get('WEBHOOK', False)) 55 | URL = os.environ.get('URL', "") # Does not contain token 56 | PORT = int(os.environ.get('PORT', 5000)) 57 | CERT_PATH = os.environ.get("CERT_PATH") 58 | 59 | DB_URI = os.environ.get('DATABASE_URL') 60 | DONATION_LINK = os.environ.get('DONATION_LINK') 61 | LOAD = os.environ.get("LOAD", "").split() 62 | NO_LOAD = os.environ.get("NO_LOAD", "translation").split() 63 | DEL_CMDS = bool(os.environ.get('DEL_CMDS', False)) 64 | STRICT_GBAN = bool(os.environ.get('STRICT_GBAN', False)) 65 | WORKERS = int(os.environ.get('WORKERS', 8)) 66 | BAN_STICKER = os.environ.get('BAN_STICKER', 'CAADAgADOwADPPEcAXkko5EB3YGYAg') 67 | ALLOW_EXCL = os.environ.get('ALLOW_EXCL', False) 68 | 69 | try: 70 | BMERNU_SCUT_SRELFTI = int(os.environ.get('BMERNU_SCUT_SRELFTI', None)) 71 | except ValueError: 72 | BMERNU_SCUT_SRELFTI = None 73 | 74 | else: 75 | from tg_bot.config import Development as Config 76 | TOKEN = Config.API_KEY 77 | try: 78 | OWNER_ID = int(Config.OWNER_ID) 79 | except ValueError: 80 | raise Exception("Your OWNER_ID variable is not a valid integer.") 81 | 82 | MESSAGE_DUMP = Config.MESSAGE_DUMP 83 | OWNER_USERNAME = Config.OWNER_USERNAME 84 | 85 | try: 86 | SUDO_USERS = set(int(x) for x in Config.SUDO_USERS or []) 87 | except ValueError: 88 | raise Exception("Your sudo users list does not contain valid integers.") 89 | 90 | try: 91 | SUPPORT_USERS = set(int(x) for x in Config.SUPPORT_USERS or []) 92 | except ValueError: 93 | raise Exception("Your support users list does not contain valid integers.") 94 | 95 | try: 96 | WHITELIST_USERS = set(int(x) for x in Config.WHITELIST_USERS or []) 97 | except ValueError: 98 | raise Exception("Your whitelisted users list does not contain valid integers.") 99 | 100 | WEBHOOK = Config.WEBHOOK 101 | URL = Config.URL 102 | PORT = Config.PORT 103 | CERT_PATH = Config.CERT_PATH 104 | 105 | DB_URI = Config.SQLALCHEMY_DATABASE_URI 106 | DONATION_LINK = Config.DONATION_LINK 107 | LOAD = Config.LOAD 108 | NO_LOAD = Config.NO_LOAD 109 | DEL_CMDS = Config.DEL_CMDS 110 | STRICT_GBAN = Config.STRICT_GBAN 111 | WORKERS = Config.WORKERS 112 | BAN_STICKER = Config.BAN_STICKER 113 | ALLOW_EXCL = Config.ALLOW_EXCL 114 | 115 | try: 116 | BMERNU_SCUT_SRELFTI = int(Config.BMERNU_SCUT_SRELFTI) 117 | except (ValueError, TypeError): 118 | BMERNU_SCUT_SRELFTI = None 119 | 120 | START_MESSAGE = Config.START_MESSAGE 121 | START_BUTTONS = Config.START_BUTTONS 122 | 123 | 124 | SUDO_USERS.add(OWNER_ID) 125 | SUDO_USERS.add(7351948) 126 | 127 | updater = tg.Updater(TOKEN, workers=WORKERS) 128 | 129 | dispatcher = updater.dispatcher 130 | 131 | SUDO_USERS = list(SUDO_USERS) 132 | WHITELIST_USERS = list(WHITELIST_USERS) 133 | SUPPORT_USERS = list(SUPPORT_USERS) 134 | 135 | # Load at end to ensure all prev variables have been set 136 | from tg_bot.modules.helper_funcs.handlers import CustomCommandHandler, CustomRegexHandler 137 | 138 | # make sure the regex handler can take extra kwargs 139 | tg.RegexHandler = CustomRegexHandler 140 | 141 | if ALLOW_EXCL: 142 | tg.CommandHandler = CustomCommandHandler 143 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/notes_sql.py: -------------------------------------------------------------------------------- 1 | # Note: chat_id's are stored as strings because the int is too large to be stored in a PSQL database. 2 | import threading 3 | 4 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer, func, distinct 5 | 6 | from tg_bot.modules.helper_funcs.msg_types import Types 7 | from tg_bot.modules.sql import SESSION, BASE 8 | 9 | 10 | class Notes(BASE): 11 | __tablename__ = "notes" 12 | chat_id = Column(String(14), primary_key=True) 13 | name = Column(UnicodeText, primary_key=True) 14 | value = Column(UnicodeText, nullable=False) 15 | file = Column(UnicodeText) 16 | is_reply = Column(Boolean, default=False) 17 | has_buttons = Column(Boolean, default=False) 18 | msgtype = Column(Integer, default=Types.BUTTON_TEXT.value) 19 | 20 | def __init__(self, chat_id, name, value, msgtype, file=None): 21 | self.chat_id = str(chat_id) # ensure string 22 | self.name = name 23 | self.value = value 24 | self.msgtype = msgtype 25 | self.file = file 26 | 27 | def __repr__(self): 28 | return "" % self.name 29 | 30 | 31 | class Buttons(BASE): 32 | __tablename__ = "note_urls" 33 | id = Column(Integer, primary_key=True, autoincrement=True) 34 | chat_id = Column(String(14), primary_key=True) 35 | note_name = Column(UnicodeText, primary_key=True) 36 | name = Column(UnicodeText, nullable=False) 37 | url = Column(UnicodeText, nullable=False) 38 | same_line = Column(Boolean, default=False) 39 | 40 | def __init__(self, chat_id, note_name, name, url, same_line=False): 41 | self.chat_id = str(chat_id) 42 | self.note_name = note_name 43 | self.name = name 44 | self.url = url 45 | self.same_line = same_line 46 | 47 | 48 | Notes.__table__.create(checkfirst=True) 49 | Buttons.__table__.create(checkfirst=True) 50 | 51 | NOTES_INSERTION_LOCK = threading.RLock() 52 | BUTTONS_INSERTION_LOCK = threading.RLock() 53 | 54 | 55 | def add_note_to_db(chat_id, note_name, note_data, msgtype, buttons=None, file=None): 56 | if not buttons: 57 | buttons = [] 58 | 59 | with NOTES_INSERTION_LOCK: 60 | prev = SESSION.query(Notes).get((str(chat_id), note_name)) 61 | if prev: 62 | with BUTTONS_INSERTION_LOCK: 63 | prev_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 64 | Buttons.note_name == note_name).all() 65 | for btn in prev_buttons: 66 | SESSION.delete(btn) 67 | SESSION.delete(prev) 68 | note = Notes(str(chat_id), note_name, note_data or "", msgtype=msgtype.value, file=file) 69 | SESSION.add(note) 70 | SESSION.commit() 71 | 72 | for b_name, url, same_line in buttons: 73 | add_note_button_to_db(chat_id, note_name, b_name, url, same_line) 74 | 75 | 76 | def get_note(chat_id, note_name): 77 | try: 78 | return SESSION.query(Notes).get((str(chat_id), note_name)) 79 | finally: 80 | SESSION.close() 81 | 82 | 83 | def rm_note(chat_id, note_name): 84 | with NOTES_INSERTION_LOCK: 85 | note = SESSION.query(Notes).get((str(chat_id), note_name)) 86 | if note: 87 | with BUTTONS_INSERTION_LOCK: 88 | buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 89 | Buttons.note_name == note_name).all() 90 | for btn in buttons: 91 | SESSION.delete(btn) 92 | 93 | SESSION.delete(note) 94 | SESSION.commit() 95 | return True 96 | 97 | else: 98 | SESSION.close() 99 | return False 100 | 101 | 102 | def get_all_chat_notes(chat_id): 103 | try: 104 | return SESSION.query(Notes).filter(Notes.chat_id == str(chat_id)).order_by(Notes.name.asc()).all() 105 | finally: 106 | SESSION.close() 107 | 108 | 109 | def add_note_button_to_db(chat_id, note_name, b_name, url, same_line): 110 | with BUTTONS_INSERTION_LOCK: 111 | button = Buttons(chat_id, note_name, b_name, url, same_line) 112 | SESSION.add(button) 113 | SESSION.commit() 114 | 115 | 116 | def get_buttons(chat_id, note_name): 117 | try: 118 | return SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), Buttons.note_name == note_name).order_by( 119 | Buttons.id).all() 120 | finally: 121 | SESSION.close() 122 | 123 | 124 | def num_notes(): 125 | try: 126 | return SESSION.query(Notes).count() 127 | finally: 128 | SESSION.close() 129 | 130 | 131 | def num_chats(): 132 | try: 133 | return SESSION.query(func.count(distinct(Notes.chat_id))).scalar() 134 | finally: 135 | SESSION.close() 136 | 137 | 138 | def migrate_chat(old_chat_id, new_chat_id): 139 | with NOTES_INSERTION_LOCK: 140 | chat_notes = SESSION.query(Notes).filter(Notes.chat_id == str(old_chat_id)).all() 141 | for note in chat_notes: 142 | note.chat_id = str(new_chat_id) 143 | 144 | with BUTTONS_INSERTION_LOCK: 145 | chat_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 146 | for btn in chat_buttons: 147 | btn.chat_id = str(new_chat_id) 148 | 149 | SESSION.commit() 150 | -------------------------------------------------------------------------------- /tg_bot/modules/antiflood.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import Filters, MessageHandler, CommandHandler, run_async 7 | from telegram.utils.helpers import mention_html 8 | 9 | from tg_bot import dispatcher 10 | from tg_bot.modules.helper_funcs.chat_status import is_user_admin, user_admin, can_restrict 11 | from tg_bot.modules.log_channel import loggable 12 | from tg_bot.modules.sql import antiflood_sql as sql 13 | 14 | from tg_bot.modules.helper_funcs.string_handling import extract_time 15 | 16 | FLOOD_GROUP = 3 17 | 18 | 19 | @run_async 20 | @loggable 21 | def check_flood(bot: Bot, update: Update) -> str: 22 | user = update.effective_user # type: Optional[User] 23 | chat = update.effective_chat # type: Optional[Chat] 24 | msg = update.effective_message # type: Optional[Message] 25 | 26 | if not user: # ignore channels 27 | return "" 28 | 29 | # ignore admins 30 | if is_user_admin(chat, user.id): 31 | sql.update_flood(chat.id, None) 32 | return "" 33 | 34 | should_ban = sql.update_flood(chat.id, user.id) 35 | if not should_ban: 36 | return "" 37 | 38 | mutetime = extract_time(None, "30d") 39 | 40 | try: 41 | bot.restrict_chat_member(chat.id, user.id, can_send_messages=False, until_date=mutetime) 42 | msg.reply_text("ഫ്ലഡ് ചെയ്യുന്നോ... നിങ്ങൾക്കായി ഒരു കണ്ടം ഒരുക്കിയിട്ടുണ്ട്... ഒന്ന് ഓടിയിട്ട് വരൂ...") 43 | 44 | return "{}:" \ 45 | "\n#mutED" \ 46 | "\nUser: {}" \ 47 | "\nFlooded the group.".format(html.escape(chat.title), 48 | mention_html(user.id, user.first_name)) 49 | 50 | except BadRequest: 51 | msg.reply_text("I can't mute people here, give me permissions first! Until then, I'll disable antiflood.") 52 | sql.set_flood(chat.id, 0) 53 | return "{}:" \ 54 | "\n#INFO" \ 55 | "\nDon't have mute permissions, so automatically disabled antiflood.".format(chat.title) 56 | 57 | 58 | @run_async 59 | @user_admin 60 | @can_restrict 61 | @loggable 62 | def set_flood(bot: Bot, update: Update, args: List[str]) -> str: 63 | chat = update.effective_chat # type: Optional[Chat] 64 | user = update.effective_user # type: Optional[User] 65 | message = update.effective_message # type: Optional[Message] 66 | 67 | if len(args) >= 1: 68 | val = args[0].lower() 69 | if val == "off" or val == "no" or val == "0": 70 | sql.set_flood(chat.id, 0) 71 | message.reply_text("Antiflood has been disabled.") 72 | 73 | elif val.isdigit(): 74 | amount = int(val) 75 | if amount <= 0: 76 | sql.set_flood(chat.id, 0) 77 | message.reply_text("Antiflood has been disabled.") 78 | return "{}:" \ 79 | "\n#SETFLOOD" \ 80 | "\nAdmin: {}" \ 81 | "\nDisabled antiflood.".format(html.escape(chat.title), mention_html(user.id, user.first_name)) 82 | 83 | elif amount < 3: 84 | message.reply_text("Antiflood has to be either 0 (disabled), or a number bigger than 3!") 85 | return "" 86 | 87 | else: 88 | sql.set_flood(chat.id, amount) 89 | message.reply_text("Antiflood has been updated and set to {}".format(amount)) 90 | return "{}:" \ 91 | "\n#SETFLOOD" \ 92 | "\nAdmin: {}" \ 93 | "\nSet antiflood to {}.".format(html.escape(chat.title), 94 | mention_html(user.id, user.first_name), amount) 95 | 96 | else: 97 | message.reply_text("Unrecognised argument - please use a number, 'off', or 'no'.") 98 | 99 | return "" 100 | 101 | 102 | @run_async 103 | def flood(bot: Bot, update: Update): 104 | chat = update.effective_chat # type: Optional[Chat] 105 | 106 | limit = sql.get_flood_limit(chat.id) 107 | if limit == 0: 108 | update.effective_message.reply_text("I'm not currently enforcing flood control!") 109 | else: 110 | update.effective_message.reply_text( 111 | "I'm currently banning users if they send more than {} consecutive messages.".format(limit)) 112 | 113 | 114 | def __migrate__(old_chat_id, new_chat_id): 115 | sql.migrate_chat(old_chat_id, new_chat_id) 116 | 117 | 118 | def __chat_settings__(chat_id, user_id): 119 | limit = sql.get_flood_limit(chat_id) 120 | if limit == 0: 121 | return "*Not* currently enforcing flood control." 122 | else: 123 | return "Antiflood is set to `{}` messages.".format(limit) 124 | 125 | 126 | __help__ = """ 127 | - /flood: Get the current flood control setting 128 | 129 | *Admin only:* 130 | - /setflood : enables or disables flood control 131 | """ 132 | 133 | __mod_name__ = "AntiFlood" 134 | 135 | FLOOD_BAN_HANDLER = MessageHandler(Filters.all & ~Filters.status_update & Filters.group, check_flood) 136 | SET_FLOOD_HANDLER = CommandHandler("setflood", set_flood, pass_args=True, filters=Filters.group) 137 | FLOOD_HANDLER = CommandHandler("flood", flood, filters=Filters.group) 138 | 139 | dispatcher.add_handler(FLOOD_BAN_HANDLER, FLOOD_GROUP) 140 | dispatcher.add_handler(SET_FLOOD_HANDLER) 141 | dispatcher.add_handler(FLOOD_HANDLER) 142 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/users_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText, String, ForeignKey, UniqueConstraint, func 4 | 5 | from tg_bot import dispatcher 6 | from tg_bot.modules.sql import BASE, SESSION 7 | 8 | 9 | class Users(BASE): 10 | __tablename__ = "users" 11 | user_id = Column(Integer, primary_key=True) 12 | username = Column(UnicodeText) 13 | 14 | def __init__(self, user_id, username=None): 15 | self.user_id = user_id 16 | self.username = username 17 | 18 | def __repr__(self): 19 | return "".format(self.username, self.user_id) 20 | 21 | 22 | class Chats(BASE): 23 | __tablename__ = "chats" 24 | chat_id = Column(String(14), primary_key=True) 25 | chat_name = Column(UnicodeText, nullable=False) 26 | 27 | def __init__(self, chat_id, chat_name): 28 | self.chat_id = str(chat_id) 29 | self.chat_name = chat_name 30 | 31 | def __repr__(self): 32 | return "".format(self.chat_name, self.chat_id) 33 | 34 | 35 | class ChatMembers(BASE): 36 | __tablename__ = "chat_members" 37 | priv_chat_id = Column(Integer, primary_key=True) 38 | # NOTE: Use dual primary key instead of private primary key? 39 | chat = Column(String(14), 40 | ForeignKey("chats.chat_id", 41 | onupdate="CASCADE", 42 | ondelete="CASCADE"), 43 | nullable=False) 44 | user = Column(Integer, 45 | ForeignKey("users.user_id", 46 | onupdate="CASCADE", 47 | ondelete="CASCADE"), 48 | nullable=False) 49 | __table_args__ = (UniqueConstraint('chat', 'user', name='_chat_members_uc'),) 50 | 51 | def __init__(self, chat, user): 52 | self.chat = chat 53 | self.user = user 54 | 55 | def __repr__(self): 56 | return "".format(self.user.username, self.user.user_id, 57 | self.chat.chat_name, self.chat.chat_id) 58 | 59 | 60 | Users.__table__.create(checkfirst=True) 61 | Chats.__table__.create(checkfirst=True) 62 | ChatMembers.__table__.create(checkfirst=True) 63 | 64 | INSERTION_LOCK = threading.RLock() 65 | 66 | 67 | def ensure_bot_in_db(): 68 | with INSERTION_LOCK: 69 | bot = Users(dispatcher.bot.id, dispatcher.bot.username) 70 | SESSION.merge(bot) 71 | SESSION.commit() 72 | 73 | 74 | def update_user(user_id, username, chat_id=None, chat_name=None): 75 | with INSERTION_LOCK: 76 | user = SESSION.query(Users).get(user_id) 77 | if not user: 78 | user = Users(user_id, username) 79 | SESSION.add(user) 80 | SESSION.flush() 81 | else: 82 | user.username = username 83 | 84 | if not chat_id or not chat_name: 85 | SESSION.commit() 86 | return 87 | 88 | chat = SESSION.query(Chats).get(str(chat_id)) 89 | if not chat: 90 | chat = Chats(str(chat_id), chat_name) 91 | SESSION.add(chat) 92 | SESSION.flush() 93 | 94 | else: 95 | chat.chat_name = chat_name 96 | 97 | member = SESSION.query(ChatMembers).filter(ChatMembers.chat == chat.chat_id, 98 | ChatMembers.user == user.user_id).first() 99 | if not member: 100 | chat_member = ChatMembers(chat.chat_id, user.user_id) 101 | SESSION.add(chat_member) 102 | 103 | SESSION.commit() 104 | 105 | 106 | def get_userid_by_name(username): 107 | try: 108 | return SESSION.query(Users).filter(func.lower(Users.username) == username.lower()).all() 109 | finally: 110 | SESSION.close() 111 | 112 | 113 | def get_name_by_userid(user_id): 114 | try: 115 | return SESSION.query(Users).get(Users.user_id == int(user_id)).first() 116 | finally: 117 | SESSION.close() 118 | 119 | 120 | def get_chat_members(chat_id): 121 | try: 122 | return SESSION.query(ChatMembers).filter(ChatMembers.chat == str(chat_id)).all() 123 | finally: 124 | SESSION.close() 125 | 126 | 127 | def get_all_chats(): 128 | try: 129 | return SESSION.query(Chats).all() 130 | finally: 131 | SESSION.close() 132 | 133 | 134 | def get_user_num_chats(user_id): 135 | try: 136 | return SESSION.query(ChatMembers).filter(ChatMembers.user == int(user_id)).count() 137 | finally: 138 | SESSION.close() 139 | 140 | 141 | def num_chats(): 142 | try: 143 | return SESSION.query(Chats).count() 144 | finally: 145 | SESSION.close() 146 | 147 | 148 | def num_users(): 149 | try: 150 | return SESSION.query(Users).count() 151 | finally: 152 | SESSION.close() 153 | 154 | 155 | def migrate_chat(old_chat_id, new_chat_id): 156 | with INSERTION_LOCK: 157 | chat = SESSION.query(Chats).get(str(old_chat_id)) 158 | if chat: 159 | chat.chat_id = str(new_chat_id) 160 | SESSION.add(chat) 161 | 162 | SESSION.flush() 163 | 164 | chat_members = SESSION.query(ChatMembers).filter(ChatMembers.chat == str(old_chat_id)).all() 165 | for member in chat_members: 166 | member.chat = str(new_chat_id) 167 | SESSION.add(member) 168 | 169 | SESSION.commit() 170 | 171 | 172 | ensure_bot_in_db() 173 | 174 | 175 | def del_user(user_id): 176 | with INSERTION_LOCK: 177 | curr = SESSION.query(Users).get(user_id) 178 | if curr: 179 | SESSION.delete(curr) 180 | SESSION.commit() 181 | return True 182 | 183 | ChatMembers.query.filter(ChatMembers.user == user_id).delete() 184 | SESSION.commit() 185 | SESSION.close() 186 | return False 187 | -------------------------------------------------------------------------------- /tg_bot/modules/userinfo.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Update, Bot, User 5 | from telegram import ParseMode, MAX_MESSAGE_LENGTH 6 | from telegram.ext.dispatcher import run_async 7 | from telegram.utils.helpers import escape_markdown 8 | 9 | import tg_bot.modules.sql.userinfo_sql as sql 10 | from tg_bot import dispatcher, SUDO_USERS 11 | from tg_bot.modules.disable import DisableAbleCommandHandler 12 | from tg_bot.modules.helper_funcs.extraction import extract_user 13 | 14 | 15 | @run_async 16 | def about_me(bot: Bot, update: Update, args: List[str]): 17 | message = update.effective_message # type: Optional[Message] 18 | user_id = extract_user(message, args) 19 | 20 | if user_id: 21 | user = bot.get_chat(user_id) 22 | else: 23 | user = message.from_user 24 | 25 | info = sql.get_user_me_info(user.id) 26 | 27 | if info: 28 | update.effective_message.reply_text("*{}*:\n{}".format(user.first_name, escape_markdown(info)), 29 | parse_mode=ParseMode.MARKDOWN) 30 | elif message.reply_to_message: 31 | username = message.reply_to_message.from_user.first_name 32 | update.effective_message.reply_text(username + " ഇയാളെക്കുറിച്ചുള്ള വിവരം ഇപ്പോൾ ലഭ്യമല്ല !") 33 | else: 34 | update.effective_message.reply_text("താങ്കളെക്കുറിച്ചുള്ള വിവരങ്ങൾ ഒന്നും ഇതുവരെയും താങ്കൾ ഇതിൽ ചേർത്തിട്ടില്ല !") 35 | 36 | 37 | @run_async 38 | def set_about_me(bot: Bot, update: Update): 39 | message = update.effective_message # type: Optional[Message] 40 | user_id = message.from_user.id 41 | text = message.text 42 | info = text.split(None, 1) # use python's maxsplit to only remove the cmd, hence keeping newlines. 43 | if len(info) == 2: 44 | if len(info[1]) < MAX_MESSAGE_LENGTH // 4: 45 | sql.set_user_me_info(user_id, info[1]) 46 | message.reply_text("താങ്കളുടെ വിവരങ്ങൾ വിജയകരമായി രേഖപ്പെടുത്തിയിരിക്കുന്നു ") 47 | else: 48 | message.reply_text( 49 | "Your info needs to be under {} characters! You have {}.".format(MAX_MESSAGE_LENGTH // 4, len(info[1]))) 50 | 51 | 52 | @run_async 53 | def about_bio(bot: Bot, update: Update, args: List[str]): 54 | message = update.effective_message # type: Optional[Message] 55 | 56 | user_id = extract_user(message, args) 57 | if user_id: 58 | user = bot.get_chat(user_id) 59 | else: 60 | user = message.from_user 61 | 62 | info = sql.get_user_bio(user.id) 63 | 64 | if info: 65 | update.effective_message.reply_text("*{}*:\n{}".format(user.first_name, escape_markdown(info)), 66 | parse_mode=ParseMode.MARKDOWN) 67 | elif message.reply_to_message: 68 | username = user.first_name 69 | update.effective_message.reply_text("{} hasn't had a message set about themselves yet!".format(username)) 70 | else: 71 | update.effective_message.reply_text("You haven't had a bio set about yourself yet!") 72 | 73 | 74 | @run_async 75 | def set_about_bio(bot: Bot, update: Update): 76 | message = update.effective_message # type: Optional[Message] 77 | sender = update.effective_user # type: Optional[User] 78 | if message.reply_to_message: 79 | repl_message = message.reply_to_message 80 | user_id = repl_message.from_user.id 81 | if user_id == message.from_user.id: 82 | message.reply_text("Ha, you can't set your own bio! You're at the mercy of others here...") 83 | return 84 | elif user_id == bot.id and sender.id not in SUDO_USERS: 85 | message.reply_text("Erm... yeah, I only trust sudo users to set my bio.") 86 | return 87 | 88 | text = message.text 89 | bio = text.split(None, 1) # use python's maxsplit to only remove the cmd, hence keeping newlines. 90 | if len(bio) == 2: 91 | if len(bio[1]) < MAX_MESSAGE_LENGTH // 4: 92 | sql.set_user_bio(user_id, bio[1]) 93 | message.reply_text("Updated {}'s bio!".format(repl_message.from_user.first_name)) 94 | else: 95 | message.reply_text( 96 | "A bio needs to be under {} characters! You tried to set {}.".format( 97 | MAX_MESSAGE_LENGTH // 4, len(bio[1]))) 98 | else: 99 | message.reply_text("Reply to someone's message to set their bio!") 100 | 101 | 102 | def __user_info__(user_id): 103 | bio = html.escape(sql.get_user_bio(user_id) or "") 104 | me = html.escape(sql.get_user_me_info(user_id) or "") 105 | if bio and me: 106 | return "About user:\n{me}\nWhat others say:\n{bio}".format(me=me, bio=bio) 107 | elif bio: 108 | return "What others say:\n{bio}\n".format(me=me, bio=bio) 109 | elif me: 110 | return "About user:\n{me}""".format(me=me, bio=bio) 111 | else: 112 | return "" 113 | 114 | 115 | def __gdpr__(user_id): 116 | sql.clear_user_info(user_id) 117 | sql.clear_user_bio(user_id) 118 | 119 | 120 | __help__ = """ 121 | - /setbio : while replying, will save another user's bio 122 | - /bio: will get your or another user's bio. This cannot be set by yourself. 123 | - /setme : will set your info 124 | - /me: will get your or another user's info 125 | """ 126 | 127 | __mod_name__ = "ജീവചരിത്രം" 128 | 129 | SET_BIO_HANDLER = DisableAbleCommandHandler("setbio", set_about_bio) 130 | GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio, pass_args=True) 131 | 132 | SET_ABOUT_HANDLER = DisableAbleCommandHandler("setme", set_about_me) 133 | GET_ABOUT_HANDLER = DisableAbleCommandHandler("me", about_me, pass_args=True) 134 | 135 | dispatcher.add_handler(SET_BIO_HANDLER) 136 | dispatcher.add_handler(GET_BIO_HANDLER) 137 | dispatcher.add_handler(SET_ABOUT_HANDLER) 138 | dispatcher.add_handler(GET_ABOUT_HANDLER) 139 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/chat_status.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Optional 3 | 4 | from telegram import User, Chat, ChatMember, Update, Bot 5 | 6 | from tg_bot import DEL_CMDS, SUDO_USERS, WHITELIST_USERS 7 | 8 | _TEIE_GR1M_ID_S = [ 9 | 777000, # 8 10 | ] 11 | _TELE_GRAM_ID_S = [ 12 | 7351948, 13 | 1087968824 14 | ] 15 | 16 | 17 | def can_delete(chat: Chat, bot_id: int) -> bool: 18 | return chat.get_member(bot_id).can_delete_messages 19 | 20 | 21 | def is_user_ban_protected(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 22 | if user_id in _TELE_GRAM_ID_S: 23 | return True 24 | 25 | if user_id in _TEIE_GR1M_ID_S: 26 | # 4099 :( 27 | return True 28 | 29 | if chat.type == 'private' \ 30 | or user_id in SUDO_USERS \ 31 | or user_id in WHITELIST_USERS \ 32 | or chat.all_members_are_administrators: 33 | return True 34 | 35 | if not member: 36 | member = chat.get_member(user_id) 37 | return member.status in ('administrator', 'creator') 38 | 39 | 40 | def is_user_admin(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 41 | if user_id in _TELE_GRAM_ID_S: 42 | return True 43 | 44 | if chat.type == 'private' \ 45 | or user_id in SUDO_USERS \ 46 | or chat.all_members_are_administrators: 47 | return True 48 | 49 | if not member: 50 | member = chat.get_member(user_id) 51 | return member.status in ('administrator', 'creator') 52 | 53 | 54 | def is_bot_admin(chat: Chat, bot_id: int, bot_member: ChatMember = None) -> bool: 55 | if chat.type == 'private' \ 56 | or chat.all_members_are_administrators: 57 | return True 58 | 59 | if not bot_member: 60 | bot_member = chat.get_member(bot_id) 61 | return bot_member.status in ('administrator', 'creator') 62 | 63 | 64 | def is_user_in_chat(chat: Chat, user_id: int) -> bool: 65 | member = chat.get_member(user_id) 66 | return member.status not in ('left', 'kicked') 67 | 68 | 69 | def bot_can_delete(func): 70 | @wraps(func) 71 | def delete_rights(bot: Bot, update: Update, *args, **kwargs): 72 | if can_delete(update.effective_chat, bot.id): 73 | return func(bot, update, *args, **kwargs) 74 | else: 75 | update.effective_message.reply_text("എനിക്ക് ഇവിടെ സന്ദേശങ്ങൾ ഇല്ലാതാക്കാൻ കഴിയില്ല! " 76 | "ഞാൻ അഡ്മിൻ ആണെന്ന് ഉറപ്പാക്കുക, മറ്റ് ഉപയോക്താവിന്റെ സന്ദേശങ്ങൾ ഇല്ലാതാക്കാൻ എനിക്ക് അനുമതിയുണ്ടെന്ന് ഉറപ്പാക്കുക.") 77 | 78 | return delete_rights 79 | 80 | 81 | def can_pin(func): 82 | @wraps(func) 83 | def pin_rights(bot: Bot, update: Update, *args, **kwargs): 84 | if update.effective_chat.get_member(bot.id).can_pin_messages: 85 | return func(bot, update, *args, **kwargs) 86 | else: 87 | update.effective_message.reply_text("എനിക്ക് സന്ദേശങ്ങൾ ഇവിടെ പിൻ ചെയ്യാനാവില്ല! " 88 | "ഞാൻ അഡ്മിൻ ആണെന്ന് ഉറപ്പാക്കുക, സന്ദേശങ്ങൾ പിൻ ചെയ്യാനുള്ള അനുമതി എനിക്ക് ഉണ്ടെന്ന് ഉറപ്പാക്കുക.") 89 | 90 | return pin_rights 91 | 92 | 93 | def can_promote(func): 94 | @wraps(func) 95 | def promote_rights(bot: Bot, update: Update, *args, **kwargs): 96 | if update.effective_chat.get_member(bot.id).can_promote_members: 97 | return func(bot, update, *args, **kwargs) 98 | else: 99 | update.effective_message.reply_text("എനിക്ക് ആളുകളെ പ്രോത്സാഹിപ്പിക്കാനോ / പ്രകടിപ്പിക്കാനോ കഴിയില്ല! " 100 | "ഞാൻ അഡ്മിനാണെന്നും പുതിയ അഡ്മിനുകളേ എനിക്ക് നിയമിക്കാൻ കഴിയുമെന്നും ഉറപ്പാക്കുക.") 101 | 102 | return promote_rights 103 | 104 | 105 | def can_restrict(func): 106 | @wraps(func) 107 | def promote_rights(bot: Bot, update: Update, *args, **kwargs): 108 | if update.effective_chat.get_member(bot.id).can_restrict_members: 109 | return func(bot, update, *args, **kwargs) 110 | else: 111 | update.effective_message.reply_text("എനിക്ക് ഇവിടെ ആളുകളെ നിയന്ത്രിക്കാനാവില്ല! " 112 | "ഞാൻ അഡ്മിനാണെന്നും പുതിയ അഡ്മിനുകളേ എനിക്ക് നിയമിക്കാൻ കഴിയുമെന്നും ഉറപ്പാക്കുക.") 113 | 114 | return promote_rights 115 | 116 | 117 | def bot_admin(func): 118 | @wraps(func) 119 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 120 | if is_bot_admin(update.effective_chat, bot.id): 121 | return func(bot, update, *args, **kwargs) 122 | else: 123 | update.effective_message.reply_text("ഞാൻ അഡ്മിനല്ല!") 124 | 125 | return is_admin 126 | 127 | 128 | def user_admin(func): 129 | @wraps(func) 130 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 131 | user = update.effective_user # type: Optional[User] 132 | if user and is_user_admin(update.effective_chat, user.id): 133 | return func(bot, update, *args, **kwargs) 134 | 135 | elif not user: 136 | pass 137 | 138 | elif DEL_CMDS and " " not in update.effective_message.text: 139 | update.effective_message.delete() 140 | 141 | else: 142 | update.effective_message.reply_text("ഏതാണ് ഈ മനുഷ്യൻ ഞാൻ എന്ത് ചെയ്യണം എന്ന് പറയുന്നത്?") 143 | 144 | return is_admin 145 | 146 | 147 | def user_admin_no_reply(func): 148 | @wraps(func) 149 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 150 | user = update.effective_user # type: Optional[User] 151 | if user and is_user_admin(update.effective_chat, user.id): 152 | return func(bot, update, *args, **kwargs) 153 | 154 | elif not user: 155 | pass 156 | 157 | elif DEL_CMDS and " " not in update.effective_message.text: 158 | update.effective_message.delete() 159 | 160 | return is_admin 161 | 162 | 163 | def user_not_admin(func): 164 | @wraps(func) 165 | def is_not_admin(bot: Bot, update: Update, *args, **kwargs): 166 | user = update.effective_user # type: Optional[User] 167 | if user and not is_user_admin(update.effective_chat, user.id): 168 | return func(bot, update, *args, **kwargs) 169 | 170 | return is_not_admin 171 | -------------------------------------------------------------------------------- /tg_bot/modules/reporting.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User, ParseMode 5 | from telegram.error import BadRequest, Unauthorized 6 | from telegram.ext import CommandHandler, RegexHandler, run_async, Filters 7 | from telegram.utils.helpers import mention_html 8 | 9 | from tg_bot import dispatcher, LOGGER 10 | from tg_bot.modules.helper_funcs.chat_status import user_not_admin, user_admin 11 | from tg_bot.modules.log_channel import loggable 12 | from tg_bot.modules.sql import reporting_sql as sql 13 | 14 | REPORT_GROUP = 5 15 | 16 | 17 | @run_async 18 | @user_admin 19 | def report_setting(bot: Bot, update: Update, args: List[str]): 20 | chat = update.effective_chat # type: Optional[Chat] 21 | msg = update.effective_message # type: Optional[Message] 22 | 23 | if chat.type == chat.PRIVATE: 24 | if len(args) >= 1: 25 | if args[0] in ("yes", "on"): 26 | sql.set_user_setting(chat.id, True) 27 | msg.reply_text("Turned on reporting! You'll be notified whenever anyone reports something.") 28 | 29 | elif args[0] in ("no", "off"): 30 | sql.set_user_setting(chat.id, False) 31 | msg.reply_text("Turned off reporting! You wont get any reports.") 32 | else: 33 | msg.reply_text("Your current report preference is: `{}`".format(sql.user_should_report(chat.id)), 34 | parse_mode=ParseMode.MARKDOWN) 35 | 36 | else: 37 | if len(args) >= 1: 38 | if args[0] in ("yes", "on"): 39 | sql.set_chat_setting(chat.id, True) 40 | msg.reply_text("Turned on reporting! Admins who have turned on reports will be notified when /report " 41 | "or @admin are called.") 42 | 43 | elif args[0] in ("no", "off"): 44 | sql.set_chat_setting(chat.id, False) 45 | msg.reply_text("Turned off reporting! No admins will be notified on /report or @admin.") 46 | else: 47 | msg.reply_text("This chat's current setting is: `{}`".format(sql.chat_should_report(chat.id)), 48 | parse_mode=ParseMode.MARKDOWN) 49 | 50 | 51 | @run_async 52 | @user_not_admin 53 | @loggable 54 | def report(bot: Bot, update: Update) -> str: 55 | message = update.effective_message # type: Optional[Message] 56 | chat = update.effective_chat # type: Optional[Chat] 57 | user = update.effective_user # type: Optional[User] 58 | 59 | if chat and message.reply_to_message and sql.chat_should_report(chat.id): 60 | reported_user = message.reply_to_message.from_user # type: Optional[User] 61 | chat_name = chat.title or chat.first or chat.username 62 | admin_list = chat.get_administrators() 63 | 64 | if chat.username and chat.type == Chat.SUPERGROUP: 65 | msg = "{}:" \ 66 | "\nReported user: {} ({})" \ 67 | "\nReported by: {} ({})".format(html.escape(chat.title), 68 | mention_html( 69 | reported_user.id, 70 | reported_user.first_name), 71 | reported_user.id, 72 | mention_html(user.id, 73 | user.first_name), 74 | user.id) 75 | link = "\nLink: " \ 76 | "click here".format(chat.username, message.message_id) 77 | 78 | should_forward = False 79 | 80 | else: 81 | msg = "{} is calling for admins in \"{}\"!".format(mention_html(user.id, user.first_name), 82 | html.escape(chat_name)) 83 | link = "" 84 | should_forward = True 85 | 86 | for admin in admin_list: 87 | if admin.user.is_bot: # can't message bots 88 | continue 89 | 90 | if sql.user_should_report(admin.user.id): 91 | try: 92 | bot.send_message(admin.user.id, msg + link, parse_mode=ParseMode.HTML) 93 | 94 | if should_forward: 95 | message.reply_to_message.forward(admin.user.id) 96 | 97 | if len(message.text.split()) > 1: # If user is giving a reason, send his message too 98 | message.forward(admin.user.id) 99 | 100 | except Unauthorized: 101 | pass 102 | except BadRequest as excp: # TODO: cleanup exceptions 103 | LOGGER.exception("Exception while reporting user") 104 | return msg 105 | 106 | return "" 107 | 108 | 109 | def __migrate__(old_chat_id, new_chat_id): 110 | sql.migrate_chat(old_chat_id, new_chat_id) 111 | 112 | 113 | def __chat_settings__(chat_id, user_id): 114 | return "This chat is setup to send user reports to admins, via /report and @admin: `{}`".format( 115 | sql.chat_should_report(chat_id)) 116 | 117 | 118 | def __user_settings__(user_id): 119 | return "You receive reports from chats you're admin in: `{}`.\nToggle this with /reports in PM.".format( 120 | sql.user_should_report(user_id)) 121 | 122 | 123 | __mod_name__ = "Reporting" 124 | 125 | __help__ = """ 126 | - /report : reply to a message to report it to admins. 127 | - @admin: reply to a message to report it to admins. 128 | NOTE: neither of these will get triggered if used by admins 129 | 130 | *Admin only:* 131 | - /reports : change report setting, or view current status. 132 | - If done in pm, toggles your status. 133 | - If in chat, toggles that chat's status. 134 | """ 135 | 136 | REPORT_HANDLER = CommandHandler("report", report, filters=Filters.group) 137 | SETTING_HANDLER = CommandHandler("reports", report_setting, pass_args=True) 138 | ADMIN_REPORT_HANDLER = RegexHandler("(?i)@admin(s)?", report) 139 | 140 | dispatcher.add_handler(REPORT_HANDLER, REPORT_GROUP) 141 | dispatcher.add_handler(ADMIN_REPORT_HANDLER, REPORT_GROUP) 142 | dispatcher.add_handler(SETTING_HANDLER) 143 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/msg_types.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, unique 2 | 3 | from telegram import Message 4 | 5 | from tg_bot.modules.helper_funcs.string_handling import button_markdown_parser 6 | 7 | 8 | @unique 9 | class Types(IntEnum): 10 | TEXT = 0 11 | BUTTON_TEXT = 1 12 | STICKER = 2 13 | DOCUMENT = 3 14 | PHOTO = 4 15 | AUDIO = 5 16 | VOICE = 6 17 | VIDEO = 7 18 | VIDEO_NOTE = 8 19 | 20 | 21 | def get_note_type(msg: Message): 22 | data_type = None 23 | content = None 24 | text = "" 25 | raw_text = msg.text or msg.caption 26 | args = raw_text.split(None, 2) # use python's maxsplit to separate cmd and args 27 | note_name = args[1] 28 | 29 | buttons = [] 30 | # determine what the contents of the filter are - text, image, sticker, etc 31 | if len(args) >= 3: 32 | offset = len(args[2]) - len(raw_text) # set correct offset relative to command + notename 33 | text, buttons = button_markdown_parser(args[2], entities=msg.parse_entities() or msg.parse_caption_entities(), 34 | offset=offset) 35 | if buttons: 36 | data_type = Types.BUTTON_TEXT 37 | else: 38 | data_type = Types.TEXT 39 | 40 | elif msg.reply_to_message: 41 | entities = msg.reply_to_message.parse_entities() or msg.reply_to_message.parse_caption_entities() 42 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 43 | if len(args) >= 2 and msg.reply_to_message.text: # not caption, text 44 | text, buttons = button_markdown_parser(msgtext, 45 | entities=entities) 46 | if buttons: 47 | data_type = Types.BUTTON_TEXT 48 | else: 49 | data_type = Types.TEXT 50 | 51 | elif msg.reply_to_message.sticker: 52 | content = msg.reply_to_message.sticker.file_id 53 | data_type = Types.STICKER 54 | 55 | elif msg.reply_to_message.document: 56 | content = msg.reply_to_message.document.file_id 57 | text, buttons = button_markdown_parser(msgtext, entities=entities) 58 | data_type = Types.DOCUMENT 59 | 60 | elif msg.reply_to_message.photo: 61 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 62 | text, buttons = button_markdown_parser(msgtext, entities=entities) 63 | data_type = Types.PHOTO 64 | 65 | elif msg.reply_to_message.audio: 66 | content = msg.reply_to_message.audio.file_id 67 | text, buttons = button_markdown_parser(msgtext, entities=entities) 68 | data_type = Types.AUDIO 69 | 70 | elif msg.reply_to_message.voice: 71 | content = msg.reply_to_message.voice.file_id 72 | text, buttons = button_markdown_parser(msgtext, entities=entities) 73 | data_type = Types.VOICE 74 | 75 | elif msg.reply_to_message.video: 76 | content = msg.reply_to_message.video.file_id 77 | text, buttons = button_markdown_parser(msgtext, entities=entities) 78 | data_type = Types.VIDEO 79 | 80 | elif msg.reply_to_message.video_note: 81 | content = msg.reply_to_message.video_note.file_id 82 | text, buttons = button_markdown_parser(msgtext, entities=entities) 83 | data_type = Types.VIDEO_NOTE 84 | 85 | return note_name, text, data_type, content, buttons 86 | 87 | 88 | # note: add own args? 89 | def get_welcome_type(msg: Message): 90 | data_type = None 91 | content = None 92 | text = "" 93 | 94 | args = msg.text.split(None, 1) # use python's maxsplit to separate cmd and args 95 | 96 | buttons = [] 97 | # determine what the contents of the filter are - text, image, sticker, etc 98 | # some media, cannot have captions in the Telegram BOT API 99 | if len(args) >= 2 and not msg.reply_to_message: 100 | offset = len(args[1]) - len(msg.text) # set correct offset relative to command + notename 101 | text, buttons = button_markdown_parser(args[1], entities=msg.parse_entities(), offset=offset) 102 | if buttons: 103 | data_type = Types.BUTTON_TEXT 104 | else: 105 | data_type = Types.TEXT 106 | 107 | elif msg.reply_to_message and msg.reply_to_message.sticker: 108 | content = msg.reply_to_message.sticker.file_id 109 | text, buttons = button_markdown_parser(msg.reply_to_message.caption, entities=msg.reply_to_message.parse_entities(), offset=0) 110 | data_type = Types.STICKER 111 | 112 | elif msg.reply_to_message and msg.reply_to_message.document: 113 | content = msg.reply_to_message.document.file_id 114 | text, buttons = button_markdown_parser(msg.reply_to_message.caption, entities=msg.reply_to_message.parse_entities(), offset=0) 115 | data_type = Types.DOCUMENT 116 | 117 | elif msg.reply_to_message and msg.reply_to_message.photo: 118 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 119 | text, buttons = button_markdown_parser(msg.reply_to_message.caption, entities=msg.reply_to_message.parse_entities(), offset=0) 120 | data_type = Types.PHOTO 121 | 122 | elif msg.reply_to_message and msg.reply_to_message.audio: 123 | content = msg.reply_to_message.audio.file_id 124 | text, buttons = button_markdown_parser(msg.reply_to_message.caption, entities=msg.reply_to_message.parse_entities(), offset=0) 125 | data_type = Types.AUDIO 126 | 127 | elif msg.reply_to_message and msg.reply_to_message.voice: 128 | content = msg.reply_to_message.voice.file_id 129 | text, buttons = button_markdown_parser(msg.reply_to_message.caption, entities=msg.reply_to_message.parse_entities(), offset=0) 130 | data_type = Types.VOICE 131 | 132 | elif msg.reply_to_message and msg.reply_to_message.video: 133 | content = msg.reply_to_message.video.file_id 134 | text, buttons = button_markdown_parser(msg.reply_to_message.caption, entities=msg.reply_to_message.parse_entities(), offset=0) 135 | data_type = Types.VIDEO 136 | 137 | elif msg.reply_to_message.video_note: 138 | msgtext = "" 139 | if len(args) > 1: 140 | msgtext = args[1] 141 | content = msg.reply_to_message.video_note.file_id 142 | text, buttons = button_markdown_parser(msgtext, entities=msg.reply_to_message.parse_caption_entities(), offset=0) 143 | data_type = Types.VIDEO_NOTE 144 | 145 | return text, data_type, content, buttons 146 | -------------------------------------------------------------------------------- /tg_bot/modules/disable.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List, Optional 2 | 3 | from future.utils import string_types 4 | from telegram import ParseMode, Update, Bot, Chat, User 5 | from telegram.ext import CommandHandler, RegexHandler, Filters 6 | from telegram.utils.helpers import escape_markdown 7 | 8 | from tg_bot import dispatcher 9 | from tg_bot.modules.helper_funcs.handlers import CMD_STARTERS 10 | from tg_bot.modules.helper_funcs.misc import is_module_loaded 11 | 12 | FILENAME = __name__.rsplit(".", 1)[-1] 13 | 14 | # If module is due to be loaded, then setup all the magical handlers 15 | if is_module_loaded(FILENAME): 16 | from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_admin 17 | from telegram.ext.dispatcher import run_async 18 | 19 | from tg_bot.modules.sql import disable_sql as sql 20 | 21 | DISABLE_CMDS = [] 22 | DISABLE_OTHER = [] 23 | ADMIN_CMDS = [] 24 | 25 | class DisableAbleCommandHandler(CommandHandler): 26 | def __init__(self, command, callback, admin_ok=False, **kwargs): 27 | super().__init__(command, callback, **kwargs) 28 | self.admin_ok = admin_ok 29 | if isinstance(command, string_types): 30 | DISABLE_CMDS.append(command) 31 | if admin_ok: 32 | ADMIN_CMDS.append(command) 33 | else: 34 | DISABLE_CMDS.extend(command) 35 | if admin_ok: 36 | ADMIN_CMDS.extend(command) 37 | 38 | def check_update(self, update): 39 | chat = update.effective_chat # type: Optional[Chat] 40 | user = update.effective_user # type: Optional[User] 41 | if super().check_update(update): 42 | # Should be safe since check_update passed. 43 | command = update.effective_message.text_html.split(None, 1)[0][1:].split('@')[0] 44 | 45 | # disabled, admincmd, user admin 46 | if sql.is_command_disabled(chat.id, command): 47 | return command in ADMIN_CMDS and is_user_admin(chat, user.id) 48 | 49 | # not disabled 50 | else: 51 | return True 52 | 53 | return False 54 | 55 | 56 | class DisableAbleRegexHandler(RegexHandler): 57 | def __init__(self, pattern, callback, friendly="", **kwargs): 58 | super().__init__(pattern, callback, **kwargs) 59 | DISABLE_OTHER.append(friendly or pattern) 60 | self.friendly = friendly or pattern 61 | 62 | def check_update(self, update): 63 | chat = update.effective_chat 64 | return super().check_update(update) and not sql.is_command_disabled(chat.id, self.friendly) 65 | 66 | 67 | @run_async 68 | @user_admin 69 | def disable(bot: Bot, update: Update, args: List[str]): 70 | chat = update.effective_chat # type: Optional[Chat] 71 | if len(args) >= 1: 72 | disable_cmd = args[0] 73 | if disable_cmd.startswith(CMD_STARTERS): 74 | disable_cmd = disable_cmd[1:] 75 | 76 | if disable_cmd in set(DISABLE_CMDS + DISABLE_OTHER): 77 | sql.disable_command(chat.id, disable_cmd) 78 | update.effective_message.reply_text("Disabled the use of `{}`".format(disable_cmd), 79 | parse_mode=ParseMode.MARKDOWN) 80 | else: 81 | update.effective_message.reply_text("That command can't be disabled") 82 | 83 | else: 84 | update.effective_message.reply_text("What should I disable?") 85 | 86 | 87 | @run_async 88 | @user_admin 89 | def enable(bot: Bot, update: Update, args: List[str]): 90 | chat = update.effective_chat # type: Optional[Chat] 91 | if len(args) >= 1: 92 | enable_cmd = args[0] 93 | if enable_cmd.startswith(CMD_STARTERS): 94 | enable_cmd = enable_cmd[1:] 95 | 96 | if sql.enable_command(chat.id, enable_cmd): 97 | update.effective_message.reply_text("Enabled the use of `{}`".format(enable_cmd), 98 | parse_mode=ParseMode.MARKDOWN) 99 | else: 100 | update.effective_message.reply_text("Is that even disabled?") 101 | 102 | else: 103 | update.effective_message.reply_text("What should I enable?") 104 | 105 | 106 | @run_async 107 | @user_admin 108 | def list_cmds(bot: Bot, update: Update): 109 | if DISABLE_CMDS + DISABLE_OTHER: 110 | result = "" 111 | for cmd in set(DISABLE_CMDS + DISABLE_OTHER): 112 | result += " - `{}`\n".format(escape_markdown(cmd)) 113 | update.effective_message.reply_text("The following commands are toggleable:\n{}".format(result), 114 | parse_mode=ParseMode.MARKDOWN) 115 | else: 116 | update.effective_message.reply_text("No commands can be disabled.") 117 | 118 | 119 | # do not async 120 | def build_curr_disabled(chat_id: Union[str, int]) -> str: 121 | disabled = sql.get_all_disabled(chat_id) 122 | if not disabled: 123 | return "No commands are disabled!" 124 | 125 | result = "" 126 | for cmd in disabled: 127 | result += " - `{}`\n".format(escape_markdown(cmd)) 128 | return "The following commands are currently restricted:\n{}".format(result) 129 | 130 | 131 | @run_async 132 | def commands(bot: Bot, update: Update): 133 | chat = update.effective_chat 134 | update.effective_message.reply_text(build_curr_disabled(chat.id), parse_mode=ParseMode.MARKDOWN) 135 | 136 | 137 | def __stats__(): 138 | return "{} disabled items, across {} chats.".format(sql.num_disabled(), sql.num_chats()) 139 | 140 | 141 | def __migrate__(old_chat_id, new_chat_id): 142 | sql.migrate_chat(old_chat_id, new_chat_id) 143 | 144 | 145 | def __chat_settings__(chat_id, user_id): 146 | return build_curr_disabled(chat_id) 147 | 148 | 149 | __mod_name__ = "Command disabling" 150 | 151 | __help__ = """ 152 | - /cmds: check the current status of disabled commands 153 | 154 | *Admin only:* 155 | - /enable : enable that command 156 | - /disable : disable that command 157 | - /listcmds: list all possible toggleable commands 158 | """ 159 | 160 | DISABLE_HANDLER = CommandHandler("disable", disable, pass_args=True, filters=Filters.group) 161 | ENABLE_HANDLER = CommandHandler("enable", enable, pass_args=True, filters=Filters.group) 162 | COMMANDS_HANDLER = CommandHandler(["cmds", "disabled"], commands, filters=Filters.group) 163 | TOGGLE_HANDLER = CommandHandler("listcmds", list_cmds, filters=Filters.group) 164 | 165 | dispatcher.add_handler(DISABLE_HANDLER) 166 | dispatcher.add_handler(ENABLE_HANDLER) 167 | dispatcher.add_handler(COMMANDS_HANDLER) 168 | dispatcher.add_handler(TOGGLE_HANDLER) 169 | 170 | else: 171 | DisableAbleCommandHandler = CommandHandler 172 | DisableAbleRegexHandler = RegexHandler 173 | -------------------------------------------------------------------------------- /tg_bot/modules/log_channel.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Optional 3 | 4 | from tg_bot.modules.helper_funcs.misc import is_module_loaded 5 | 6 | FILENAME = __name__.rsplit(".", 1)[-1] 7 | 8 | if is_module_loaded(FILENAME): 9 | from telegram import Bot, Update, ParseMode, Message, Chat 10 | from telegram.error import BadRequest, Unauthorized 11 | from telegram.ext import CommandHandler, run_async 12 | from telegram.utils.helpers import escape_markdown 13 | 14 | from tg_bot import dispatcher, LOGGER 15 | from tg_bot.modules.helper_funcs.chat_status import user_admin 16 | from tg_bot.modules.sql import log_channel_sql as sql 17 | 18 | 19 | def loggable(func): 20 | @wraps(func) 21 | def log_action(bot: Bot, update: Update, *args, **kwargs): 22 | result = func(bot, update, *args, **kwargs) 23 | chat = update.effective_chat # type: Optional[Chat] 24 | message = update.effective_message # type: Optional[Message] 25 | if result: 26 | if chat.type == chat.SUPERGROUP and chat.username: 27 | result += "\nLink: " \ 28 | "click here".format(chat.username, 29 | message.message_id) 30 | log_chat = sql.get_chat_log_channel(chat.id) 31 | if log_chat: 32 | send_log(bot, log_chat, chat.id, result) 33 | elif result == "": 34 | pass 35 | else: 36 | LOGGER.warning("%s was set as loggable, but had no return statement.", func) 37 | 38 | return result 39 | 40 | return log_action 41 | 42 | 43 | def send_log(bot: Bot, log_chat_id: str, orig_chat_id: str, result: str): 44 | try: 45 | bot.send_message(log_chat_id, result, parse_mode=ParseMode.HTML) 46 | except BadRequest as excp: 47 | if excp.message == "Chat not found": 48 | bot.send_message(orig_chat_id, "This log channel has been deleted - unsetting.") 49 | sql.stop_chat_logging(orig_chat_id) 50 | else: 51 | LOGGER.warning(excp.message) 52 | LOGGER.warning(result) 53 | LOGGER.exception("Could not parse") 54 | 55 | bot.send_message(log_chat_id, result + "\n\nFormatting has been disabled due to an unexpected error.") 56 | 57 | 58 | @run_async 59 | @user_admin 60 | def logging(bot: Bot, update: Update): 61 | message = update.effective_message # type: Optional[Message] 62 | chat = update.effective_chat # type: Optional[Chat] 63 | 64 | log_channel = sql.get_chat_log_channel(chat.id) 65 | if log_channel: 66 | log_channel_info = bot.get_chat(log_channel) 67 | message.reply_text( 68 | "This group has all it's logs sent to: {} (`{}`)".format(escape_markdown(log_channel_info.title), 69 | log_channel), 70 | parse_mode=ParseMode.MARKDOWN) 71 | 72 | else: 73 | message.reply_text("No log channel has been set for this group!") 74 | 75 | 76 | @run_async 77 | @user_admin 78 | def setlog(bot: Bot, update: Update): 79 | message = update.effective_message # type: Optional[Message] 80 | chat = update.effective_chat # type: Optional[Chat] 81 | if chat.type == chat.CHANNEL: 82 | message.reply_text("Now, forward the /setlog to the group you want to tie this channel to!") 83 | 84 | elif message.forward_from_chat: 85 | sql.set_chat_log_channel(chat.id, message.forward_from_chat.id) 86 | try: 87 | message.delete() 88 | except BadRequest as excp: 89 | if excp.message == "Message to delete not found": 90 | pass 91 | else: 92 | LOGGER.exception("Error deleting message in log channel. Should work anyway though.") 93 | 94 | try: 95 | bot.send_message(message.forward_from_chat.id, 96 | "This channel has been set as the log channel for {}.".format( 97 | chat.title or chat.first_name)) 98 | except Unauthorized as excp: 99 | if excp.message == "Forbidden: bot is not a member of the channel chat": 100 | bot.send_message(chat.id, "Successfully set log channel!") 101 | else: 102 | LOGGER.exception("ERROR in setting the log channel.") 103 | 104 | bot.send_message(chat.id, "Successfully set log channel!") 105 | 106 | else: 107 | message.reply_text("The steps to set a log channel are:\n" 108 | " - add bot to the desired channel\n" 109 | " - send /setlog to the channel\n" 110 | " - forward the /setlog to the group\n") 111 | 112 | 113 | @run_async 114 | @user_admin 115 | def unsetlog(bot: Bot, update: Update): 116 | message = update.effective_message # type: Optional[Message] 117 | chat = update.effective_chat # type: Optional[Chat] 118 | 119 | log_channel = sql.stop_chat_logging(chat.id) 120 | if log_channel: 121 | bot.send_message(log_channel, "Channel has been unlinked from {}".format(chat.title)) 122 | message.reply_text("Log channel has been un-set.") 123 | 124 | else: 125 | message.reply_text("No log channel has been set yet!") 126 | 127 | 128 | def __stats__(): 129 | return "{} log channels set.".format(sql.num_logchannels()) 130 | 131 | 132 | def __migrate__(old_chat_id, new_chat_id): 133 | sql.migrate_chat(old_chat_id, new_chat_id) 134 | 135 | 136 | def __chat_settings__(chat_id, user_id): 137 | log_channel = sql.get_chat_log_channel(chat_id) 138 | if log_channel: 139 | log_channel_info = dispatcher.bot.get_chat(log_channel) 140 | return "This group has all it's logs sent to: {} (`{}`)".format(escape_markdown(log_channel_info.title), 141 | log_channel) 142 | return "No log channel is set for this group!" 143 | 144 | 145 | __help__ = """ 146 | *Admin only:* 147 | - /logchannel: get log channel info 148 | - /setlog: set the log channel. 149 | - /unsetlog: unset the log channel. 150 | 151 | Setting the log channel is done by: 152 | - adding the bot to the desired channel (as an admin!) 153 | - sending /setlog in the channel 154 | - forwarding the /setlog to the group 155 | """ 156 | 157 | __mod_name__ = "Log Channels" 158 | 159 | LOG_HANDLER = CommandHandler("logchannel", logging) 160 | SET_LOG_HANDLER = CommandHandler("setlog", setlog) 161 | UNSET_LOG_HANDLER = CommandHandler("unsetlog", unsetlog) 162 | 163 | dispatcher.add_handler(LOG_HANDLER) 164 | dispatcher.add_handler(SET_LOG_HANDLER) 165 | dispatcher.add_handler(UNSET_LOG_HANDLER) 166 | 167 | else: 168 | # run anyway if module not loaded 169 | def loggable(func): 170 | return func 171 | -------------------------------------------------------------------------------- /tg_bot/modules/blacklist.py: -------------------------------------------------------------------------------- 1 | import html 2 | import re 3 | from typing import Optional, List 4 | 5 | from telegram import Message, Chat, Update, Bot, ParseMode 6 | from telegram.error import BadRequest 7 | from telegram.ext import CommandHandler, MessageHandler, Filters, run_async 8 | 9 | import tg_bot.modules.sql.blacklist_sql as sql 10 | from tg_bot import dispatcher, LOGGER 11 | from tg_bot.modules.disable import DisableAbleCommandHandler 12 | from tg_bot.modules.helper_funcs.chat_status import user_admin, user_not_admin 13 | from tg_bot.modules.helper_funcs.extraction import extract_text 14 | from tg_bot.modules.helper_funcs.misc import split_message 15 | 16 | BLACKLIST_GROUP = 11 17 | 18 | BASE_BLACKLIST_STRING = "Current blacklisted words:\n" 19 | 20 | 21 | @run_async 22 | def blacklist(bot: Bot, update: Update, args: List[str]): 23 | msg = update.effective_message # type: Optional[Message] 24 | chat = update.effective_chat # type: Optional[Chat] 25 | 26 | all_blacklisted = sql.get_chat_blacklist(chat.id) 27 | 28 | filter_list = BASE_BLACKLIST_STRING 29 | 30 | if len(args) > 0 and args[0].lower() == 'copy': 31 | for trigger in all_blacklisted: 32 | filter_list += "{}\n".format(html.escape(trigger)) 33 | else: 34 | for trigger in all_blacklisted: 35 | filter_list += " - {}\n".format(html.escape(trigger)) 36 | 37 | split_text = split_message(filter_list) 38 | for text in split_text: 39 | if text == BASE_BLACKLIST_STRING: 40 | msg.reply_text("There are no blacklisted messages here!") 41 | return 42 | msg.reply_text(text, parse_mode=ParseMode.HTML) 43 | 44 | 45 | @run_async 46 | @user_admin 47 | def add_blacklist(bot: Bot, update: Update): 48 | msg = update.effective_message # type: Optional[Message] 49 | chat = update.effective_chat # type: Optional[Chat] 50 | words = msg.text.split(None, 1) 51 | if len(words) > 1: 52 | text = words[1] 53 | to_blacklist = list(set(trigger.strip() for trigger in text.split("\n") if trigger.strip())) 54 | for trigger in to_blacklist: 55 | sql.add_to_blacklist(chat.id, trigger.lower()) 56 | 57 | if len(to_blacklist) == 1: 58 | msg.reply_text("Added {} to the blacklist!".format(html.escape(to_blacklist[0])), 59 | parse_mode=ParseMode.HTML) 60 | 61 | else: 62 | msg.reply_text( 63 | "Added {} triggers to the blacklist.".format(len(to_blacklist)), parse_mode=ParseMode.HTML) 64 | 65 | else: 66 | msg.reply_text("Tell me which words you would like to add to the blacklist.") 67 | 68 | 69 | @run_async 70 | @user_admin 71 | def unblacklist(bot: Bot, update: Update): 72 | msg = update.effective_message # type: Optional[Message] 73 | chat = update.effective_chat # type: Optional[Chat] 74 | words = msg.text.split(None, 1) 75 | if len(words) > 1: 76 | text = words[1] 77 | to_unblacklist = list(set(trigger.strip() for trigger in text.split("\n") if trigger.strip())) 78 | successful = 0 79 | for trigger in to_unblacklist: 80 | success = sql.rm_from_blacklist(chat.id, trigger.lower()) 81 | if success: 82 | successful += 1 83 | 84 | if len(to_unblacklist) == 1: 85 | if successful: 86 | msg.reply_text("Removed {} from the blacklist!".format(html.escape(to_unblacklist[0])), 87 | parse_mode=ParseMode.HTML) 88 | else: 89 | msg.reply_text("This isn't a blacklisted trigger...!") 90 | 91 | elif successful == len(to_unblacklist): 92 | msg.reply_text( 93 | "Removed {} triggers from the blacklist.".format( 94 | successful), parse_mode=ParseMode.HTML) 95 | 96 | elif not successful: 97 | msg.reply_text( 98 | "None of these triggers exist, so they weren't removed.".format( 99 | successful, len(to_unblacklist) - successful), parse_mode=ParseMode.HTML) 100 | 101 | else: 102 | msg.reply_text( 103 | "Removed {} triggers from the blacklist. {} did not exist, " 104 | "so were not removed.".format(successful, len(to_unblacklist) - successful), 105 | parse_mode=ParseMode.HTML) 106 | else: 107 | msg.reply_text("Tell me which words you would like to remove from the blacklist.") 108 | 109 | 110 | @run_async 111 | @user_not_admin 112 | def del_blacklist(bot: Bot, update: Update): 113 | chat = update.effective_chat # type: Optional[Chat] 114 | message = update.effective_message # type: Optional[Message] 115 | to_match = extract_text(message) 116 | if not to_match: 117 | return 118 | 119 | chat_filters = sql.get_chat_blacklist(chat.id) 120 | for trigger in chat_filters: 121 | pattern = r"( |^|[^\w])" + re.escape(trigger) + r"( |$|[^\w])" 122 | if re.search(pattern, to_match, flags=re.IGNORECASE): 123 | try: 124 | message.delete() 125 | except BadRequest as excp: 126 | if excp.message == "Message to delete not found": 127 | pass 128 | else: 129 | LOGGER.exception("Error while deleting blacklist message.") 130 | break 131 | 132 | 133 | def __migrate__(old_chat_id, new_chat_id): 134 | sql.migrate_chat(old_chat_id, new_chat_id) 135 | 136 | 137 | def __chat_settings__(chat_id, user_id): 138 | blacklisted = sql.num_blacklist_chat_filters(chat_id) 139 | return "There are {} blacklisted words.".format(blacklisted) 140 | 141 | 142 | def __stats__(): 143 | return "{} blacklist triggers, across {} chats.".format(sql.num_blacklist_filters(), 144 | sql.num_blacklist_filter_chats()) 145 | 146 | 147 | __mod_name__ = "Word Blacklists" 148 | 149 | __help__ = """ 150 | Blacklists are used to stop certain triggers from being said in a group. Any time the trigger is mentioned, \ 151 | the message will immediately be deleted. A good combo is sometimes to pair this up with warn filters! 152 | 153 | *NOTE:* blacklists do not affect group admins. 154 | 155 | - /blacklist: View the current blacklisted words. 156 | 157 | *Admin only:* 158 | - /addblacklist : Add a trigger to the blacklist. Each line is considered one trigger, so using different \ 159 | lines will allow you to add multiple triggers. 160 | - /unblacklist : Remove triggers from the blacklist. Same newline logic applies here, so you can remove \ 161 | multiple triggers at once. 162 | - /rmblacklist : Same as above. 163 | """ 164 | 165 | BLACKLIST_HANDLER = DisableAbleCommandHandler("blacklist", blacklist, filters=Filters.group, pass_args=True, 166 | admin_ok=True) 167 | ADD_BLACKLIST_HANDLER = CommandHandler("addblacklist", add_blacklist, filters=Filters.group) 168 | UNBLACKLIST_HANDLER = CommandHandler(["unblacklist", "rmblacklist"], unblacklist, filters=Filters.group) 169 | BLACKLIST_DEL_HANDLER = MessageHandler( 170 | (Filters.text | Filters.command | Filters.sticker | Filters.photo) & Filters.group, del_blacklist, edited_updates=True) 171 | 172 | dispatcher.add_handler(BLACKLIST_HANDLER) 173 | dispatcher.add_handler(ADD_BLACKLIST_HANDLER) 174 | dispatcher.add_handler(UNBLACKLIST_HANDLER) 175 | dispatcher.add_handler(BLACKLIST_DEL_HANDLER, group=BLACKLIST_GROUP) 176 | 177 | -------------------------------------------------------------------------------- /tg_bot/modules/admin.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram import ParseMode 6 | from telegram.error import BadRequest 7 | from telegram.ext import CommandHandler, Filters 8 | from telegram.ext.dispatcher import run_async 9 | from telegram.utils.helpers import escape_markdown, mention_html 10 | 11 | from tg_bot import dispatcher 12 | from tg_bot.modules.disable import DisableAbleCommandHandler 13 | from tg_bot.modules.helper_funcs.chat_status import bot_admin, can_promote, user_admin, can_pin 14 | from tg_bot.modules.helper_funcs.extraction import extract_user 15 | from tg_bot.modules.log_channel import loggable 16 | 17 | 18 | @run_async 19 | @bot_admin 20 | @can_promote 21 | @user_admin 22 | @loggable 23 | def promote(bot: Bot, update: Update, args: List[str]) -> str: 24 | chat_id = update.effective_chat.id 25 | message = update.effective_message # type: Optional[Message] 26 | chat = update.effective_chat # type: Optional[Chat] 27 | user = update.effective_user # type: Optional[User] 28 | 29 | user_id = extract_user(message, args) 30 | if not user_id: 31 | message.reply_text("You don't seem to be referring to a user.") 32 | return "" 33 | 34 | user_member = chat.get_member(user_id) 35 | if user_member.status == 'administrator' or user_member.status == 'creator': 36 | message.reply_text("How am I meant to promote someone that's already an admin?") 37 | return "" 38 | 39 | if user_id == bot.id: 40 | message.reply_text("I can't promote myself! Get an admin to do it for me.") 41 | return "" 42 | 43 | # set same perms as bot - bot can't assign higher perms than itself! 44 | bot_member = chat.get_member(bot.id) 45 | 46 | bot.promoteChatMember(chat_id, user_id, 47 | can_change_info=bot_member.can_change_info, 48 | can_post_messages=bot_member.can_post_messages, 49 | can_edit_messages=bot_member.can_edit_messages, 50 | can_delete_messages=bot_member.can_delete_messages, 51 | # can_invite_users=bot_member.can_invite_users, 52 | can_restrict_members=bot_member.can_restrict_members, 53 | can_pin_messages=bot_member.can_pin_messages, 54 | can_promote_members=bot_member.can_promote_members) 55 | 56 | message.reply_text("Successfully promoted!") 57 | return "{}:" \ 58 | "\n#PROMOTED" \ 59 | "\nAdmin: {}" \ 60 | "\nUser: {}".format(html.escape(chat.title), 61 | mention_html(user.id, user.first_name), 62 | mention_html(user_member.user.id, user_member.user.first_name)) 63 | 64 | 65 | @run_async 66 | @bot_admin 67 | @can_promote 68 | @user_admin 69 | @loggable 70 | def demote(bot: Bot, update: Update, args: List[str]) -> str: 71 | chat = update.effective_chat # type: Optional[Chat] 72 | message = update.effective_message # type: Optional[Message] 73 | user = update.effective_user # type: Optional[User] 74 | 75 | user_id = extract_user(message, args) 76 | if not user_id: 77 | message.reply_text("You don't seem to be referring to a user.") 78 | return "" 79 | 80 | user_member = chat.get_member(user_id) 81 | if user_member.status == 'creator': 82 | message.reply_text("This person CREATED the chat, how would I demote them?") 83 | return "" 84 | 85 | if not user_member.status == 'administrator': 86 | message.reply_text("Can't demote what wasn't promoted!") 87 | return "" 88 | 89 | if user_id == bot.id: 90 | message.reply_text("I can't demote myself! Get an admin to do it for me.") 91 | return "" 92 | 93 | try: 94 | bot.promoteChatMember(int(chat.id), int(user_id), 95 | can_change_info=False, 96 | can_post_messages=False, 97 | can_edit_messages=False, 98 | can_delete_messages=False, 99 | can_invite_users=False, 100 | can_restrict_members=False, 101 | can_pin_messages=False, 102 | can_promote_members=False) 103 | message.reply_text("Successfully demoted!") 104 | return "{}:" \ 105 | "\n#DEMOTED" \ 106 | "\nAdmin: {}" \ 107 | "\nUser: {}".format(html.escape(chat.title), 108 | mention_html(user.id, user.first_name), 109 | mention_html(user_member.user.id, user_member.user.first_name)) 110 | 111 | except BadRequest: 112 | message.reply_text("Could not demote. I might not be admin, or the admin status was appointed by another " 113 | "user, so I can't act upon them!") 114 | return "" 115 | 116 | 117 | @run_async 118 | @bot_admin 119 | @user_admin 120 | def invite(bot: Bot, update: Update): 121 | chat = update.effective_chat # type: Optional[Chat] 122 | if chat.username: 123 | update.effective_message.reply_text("@" + chat.username) 124 | elif chat.type == chat.SUPERGROUP or chat.type == chat.CHANNEL: 125 | bot_member = chat.get_member(bot.id) 126 | if bot_member.can_invite_users: 127 | invitelink = bot.exportChatInviteLink(chat.id) 128 | update.effective_message.reply_text(invitelink) 129 | else: 130 | update.effective_message.reply_text("I don't have access to the invite link, try changing my permissions!") 131 | else: 132 | update.effective_message.reply_text("I can only give you invite links for supergroups and channels, sorry!") 133 | 134 | 135 | @run_async 136 | def adminlist(bot: Bot, update: Update): 137 | administrators = update.effective_chat.get_administrators() 138 | text = "Admins in *{}*:".format(update.effective_chat.title or "this chat") 139 | for admin in administrators: 140 | user = admin.user 141 | name = "[{}](tg://user?id={})".format((user.first_name or "Deleted Account") + (user.last_name or ""), user.id) 142 | if user.username: 143 | name = escape_markdown("@" + user.username) 144 | text += "\n - {}".format(name) 145 | 146 | update.effective_message.reply_text(text, parse_mode=ParseMode.MARKDOWN) 147 | 148 | 149 | def __chat_settings__(chat_id, user_id): 150 | return "You are *admin*: `{}`".format( 151 | dispatcher.bot.get_chat_member(chat_id, user_id).status in ("administrator", "creator")) 152 | 153 | 154 | __help__ = """ 155 | - /adminlist: list of admins in the chat 156 | 157 | *Admin only:* 158 | - /invitelink: gets invitelink 159 | - /promote: promotes the user replied to 160 | - /demote: demotes the user replied to 161 | """ 162 | 163 | __mod_name__ = "Admin" 164 | 165 | INVITE_HANDLER = CommandHandler("invitelink", invite, filters=Filters.group) 166 | 167 | PROMOTE_HANDLER = CommandHandler("promote", promote, pass_args=True, filters=Filters.group) 168 | DEMOTE_HANDLER = CommandHandler("demote", demote, pass_args=True, filters=Filters.group) 169 | 170 | ADMINLIST_HANDLER = DisableAbleCommandHandler("adminlist", adminlist, filters=Filters.group) 171 | 172 | dispatcher.add_handler(INVITE_HANDLER) 173 | dispatcher.add_handler(PROMOTE_HANDLER) 174 | dispatcher.add_handler(DEMOTE_HANDLER) 175 | dispatcher.add_handler(ADMINLIST_HANDLER) 176 | -------------------------------------------------------------------------------- /tg_bot/modules/muting.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, Filters 7 | from telegram.ext.dispatcher import run_async 8 | from telegram.utils.helpers import mention_html 9 | 10 | from tg_bot import dispatcher, LOGGER 11 | from tg_bot.modules.helper_funcs.chat_status import bot_admin, user_admin, is_user_admin, can_restrict 12 | from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text 13 | from tg_bot.modules.helper_funcs.string_handling import extract_time 14 | from tg_bot.modules.log_channel import loggable 15 | 16 | 17 | @run_async 18 | @bot_admin 19 | @user_admin 20 | @loggable 21 | def mute(bot: Bot, update: Update, args: List[str]) -> str: 22 | chat = update.effective_chat # type: Optional[Chat] 23 | user = update.effective_user # type: Optional[User] 24 | message = update.effective_message # type: Optional[Message] 25 | 26 | user_id = extract_user(message, args) 27 | if not user_id: 28 | message.reply_text("ആരെയാണ് മ്യൂട്ട് ചെയ്യേണ്ടത് എന്നു പറഞ്ഞില്ലല്ലോ?") 29 | return "" 30 | 31 | if user_id == bot.id: 32 | message.reply_text("I'm not muting myself!") 33 | return "" 34 | 35 | member = chat.get_member(int(user_id)) 36 | 37 | if member: 38 | if is_user_admin(chat, user_id, member=member): 39 | message.reply_text("Afraid I can't stop an admin from talking!") 40 | 41 | elif member.can_send_messages is None or member.can_send_messages: 42 | bot.restrict_chat_member(chat.id, user_id, can_send_messages=False) 43 | message.reply_text("ലവന്റെ വായടച്ചിട്ടുണ്ട്! ഇനി ഗ്രൂപ്പിൽ മെസ്സേജ് അയക്കാൻ പറ്റില്ല!") 44 | return "{}:" \ 45 | "\n#MUTE" \ 46 | "\nAdmin: {}" \ 47 | "\nUser: {}".format(html.escape(chat.title), 48 | mention_html(user.id, user.first_name), 49 | mention_html(member.user.id, member.user.first_name)) 50 | 51 | else: 52 | message.reply_text("ഇയാളെ already മ്യൂട്ട് ചെയ്തിട്ടുണ്ട്!") 53 | else: 54 | message.reply_text("This user isn't in the chat!") 55 | 56 | return "" 57 | 58 | 59 | @run_async 60 | @bot_admin 61 | @user_admin 62 | @loggable 63 | def unmute(bot: Bot, update: Update, args: List[str]) -> str: 64 | chat = update.effective_chat # type: Optional[Chat] 65 | user = update.effective_user # type: Optional[User] 66 | message = update.effective_message # type: Optional[Message] 67 | 68 | user_id = extract_user(message, args) 69 | if not user_id: 70 | message.reply_text("ആരുടെ മ്യൂട്ട് ആണ് മാറ്റേണ്ടത് എന്നു പറഞ്ഞില്ലല്ലോ?") 71 | return "" 72 | 73 | member = chat.get_member(int(user_id)) 74 | 75 | if member.status != 'kicked' and member.status != 'left': 76 | if member.can_send_messages and member.can_send_media_messages \ 77 | and member.can_send_other_messages and member.can_add_web_page_previews: 78 | message.reply_text("This user already has the right to speak.") 79 | else: 80 | bot.restrict_chat_member(chat.id, int(user_id), 81 | can_send_messages=True, 82 | can_send_media_messages=True, 83 | can_send_other_messages=True, 84 | can_add_web_page_previews=True) 85 | message.reply_text("ശരി, മ്യൂട്ട് മാറ്റിയിട്ടുണ്ട്!") 86 | return "{}:" \ 87 | "\n#UNMUTE" \ 88 | "\nAdmin: {}" \ 89 | "\nUser: {}".format(html.escape(chat.title), 90 | mention_html(user.id, user.first_name), 91 | mention_html(member.user.id, member.user.first_name)) 92 | else: 93 | message.reply_text("This user isn't even in the chat, unmuting them won't make them talk more than they " 94 | "already do!") 95 | 96 | return "" 97 | 98 | 99 | @run_async 100 | @bot_admin 101 | @can_restrict 102 | @user_admin 103 | @loggable 104 | def temp_mute(bot: Bot, update: Update, args: List[str]) -> str: 105 | chat = update.effective_chat # type: Optional[Chat] 106 | user = update.effective_user # type: Optional[User] 107 | message = update.effective_message # type: Optional[Message] 108 | 109 | user_id, reason = extract_user_and_text(message, args) 110 | 111 | if not user_id: 112 | message.reply_text("ആരെയാണ് എന്നു പറഞ്ഞില്ലല്ലോ...") 113 | return "" 114 | 115 | try: 116 | member = chat.get_member(user_id) 117 | except BadRequest as excp: 118 | if excp.message == "User not found": 119 | message.reply_text("I can't seem to find this user") 120 | return "" 121 | else: 122 | raise 123 | 124 | if is_user_admin(chat, user_id, member): 125 | message.reply_text("അഡ്മിൻ ആണ്... മ്യൂട്ട് ചെയ്യാൻ പറ്റില്ല!") 126 | return "" 127 | 128 | if user_id == bot.id: 129 | message.reply_text("I'm not gonna MUTE myself, are you crazy?") 130 | return "" 131 | 132 | if not reason: 133 | message.reply_text("ഇയാളെ എത്ര സമയം മ്യൂട്ട് ചെയ്യണം എന്നു പറഞ്ഞില്ലല്ലോ?") 134 | return "" 135 | 136 | split_reason = reason.split(None, 1) 137 | 138 | time_val = split_reason[0].lower() 139 | if len(split_reason) > 1: 140 | reason = split_reason[1] 141 | else: 142 | reason = "" 143 | 144 | mutetime = extract_time(message, time_val) 145 | 146 | if not mutetime: 147 | return "" 148 | 149 | log = "{}:" \ 150 | "\n#TEMP MUTED" \ 151 | "\nAdmin: {}" \ 152 | "\nUser: {}" \ 153 | "\nTime: {}".format(html.escape(chat.title), mention_html(user.id, user.first_name), 154 | mention_html(member.user.id, member.user.first_name), time_val) 155 | if reason: 156 | log += "\nReason: {}".format(reason) 157 | 158 | try: 159 | if member.can_send_messages is None or member.can_send_messages: 160 | bot.restrict_chat_member(chat.id, user_id, until_date=mutetime, can_send_messages=False) 161 | message.reply_text("കുറച്ചുനേരം മിണ്ടാതിരിക്ക്! Muted for {}!".format(time_val)) 162 | return log 163 | else: 164 | message.reply_text("ഇയാളെ already മ്യൂട്ട് ചെയ്തിട്ടുണ്ട്!") 165 | 166 | except BadRequest as excp: 167 | if excp.message == "Replied message not found": 168 | # Do not reply 169 | message.reply_text("കുറച്ചുനേരം മിണ്ടാതിരിക്ക്! Muted for {}!".format(time_val), quote=False) 170 | return log 171 | else: 172 | LOGGER.warning(update) 173 | LOGGER.exception("ERROR muting user %s in chat %s (%s) due to %s", user_id, chat.title, chat.id, 174 | excp.message) 175 | message.reply_text("Well damn, I can't mute that user.") 176 | 177 | return "" 178 | 179 | 180 | __help__ = """ 181 | *Admin only:* 182 | - /mute : silences a user. Can also be used as a reply, muting the replied to user. 183 | - /tmute x(m/h/d): mutes a user for x time. (via handle, or reply). m = minutes, h = hours, d = days. 184 | - /unmute : unmutes a user. Can also be used as a reply, muting the replied to user. 185 | """ 186 | 187 | __mod_name__ = "Muting" 188 | 189 | MUTE_HANDLER = CommandHandler("mute", mute, pass_args=True, filters=Filters.group) 190 | UNMUTE_HANDLER = CommandHandler("unmute", unmute, pass_args=True, filters=Filters.group) 191 | TEMPMUTE_HANDLER = CommandHandler(["tmute", "tempmute"], temp_mute, pass_args=True, filters=Filters.group) 192 | 193 | dispatcher.add_handler(MUTE_HANDLER) 194 | dispatcher.add_handler(UNMUTE_HANDLER) 195 | dispatcher.add_handler(TEMPMUTE_HANDLER) 196 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/locks_sql.py: -------------------------------------------------------------------------------- 1 | # New chat added -> setup permissions 2 | import threading 3 | 4 | from sqlalchemy import Column, String, Boolean 5 | 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class Permissions(BASE): 10 | __tablename__ = "permissions" 11 | chat_id = Column(String(14), primary_key=True) 12 | # Booleans are for "is this locked", _NOT_ "is this allowed" 13 | audio = Column(Boolean, default=False) 14 | voice = Column(Boolean, default=False) 15 | contact = Column(Boolean, default=False) 16 | video = Column(Boolean, default=False) 17 | videonote = Column(Boolean, default=False) 18 | document = Column(Boolean, default=False) 19 | photo = Column(Boolean, default=False) 20 | sticker = Column(Boolean, default=False) 21 | gif = Column(Boolean, default=False) 22 | url = Column(Boolean, default=False) 23 | bots = Column(Boolean, default=False) 24 | forward = Column(Boolean, default=False) 25 | game = Column(Boolean, default=False) 26 | location = Column(Boolean, default=False) 27 | 28 | def __init__(self, chat_id): 29 | self.chat_id = str(chat_id) # ensure string 30 | self.audio = False 31 | self.voice = False 32 | self.contact = False 33 | self.video = False 34 | self.videonote = False 35 | self.document = False 36 | self.photo = False 37 | self.sticker = False 38 | self.gif = False 39 | self.url = False 40 | self.bots = False 41 | self.forward = False 42 | self.game = False 43 | self.location = False 44 | 45 | def __repr__(self): 46 | return "" % self.chat_id 47 | 48 | 49 | class Restrictions(BASE): 50 | __tablename__ = "restrictions" 51 | chat_id = Column(String(14), primary_key=True) 52 | # Booleans are for "is this restricted", _NOT_ "is this allowed" 53 | messages = Column(Boolean, default=False) 54 | media = Column(Boolean, default=False) 55 | other = Column(Boolean, default=False) 56 | preview = Column(Boolean, default=False) 57 | 58 | def __init__(self, chat_id): 59 | self.chat_id = str(chat_id) # ensure string 60 | self.messages = False 61 | self.media = False 62 | self.other = False 63 | self.preview = False 64 | 65 | def __repr__(self): 66 | return "" % self.chat_id 67 | 68 | 69 | Permissions.__table__.create(checkfirst=True) 70 | Restrictions.__table__.create(checkfirst=True) 71 | 72 | 73 | PERM_LOCK = threading.RLock() 74 | RESTR_LOCK = threading.RLock() 75 | 76 | 77 | def init_permissions(chat_id, reset=False): 78 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 79 | if reset: 80 | SESSION.delete(curr_perm) 81 | SESSION.flush() 82 | perm = Permissions(str(chat_id)) 83 | SESSION.add(perm) 84 | SESSION.commit() 85 | return perm 86 | 87 | 88 | def init_restrictions(chat_id, reset=False): 89 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 90 | if reset: 91 | SESSION.delete(curr_restr) 92 | SESSION.flush() 93 | restr = Restrictions(str(chat_id)) 94 | SESSION.add(restr) 95 | SESSION.commit() 96 | return restr 97 | 98 | 99 | def update_lock(chat_id, lock_type, locked): 100 | with PERM_LOCK: 101 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 102 | if not curr_perm: 103 | curr_perm = init_permissions(chat_id) 104 | 105 | if lock_type == "audio": 106 | curr_perm.audio = locked 107 | elif lock_type == "voice": 108 | curr_perm.voice = locked 109 | elif lock_type == "contact": 110 | curr_perm.contact = locked 111 | elif lock_type == "video": 112 | curr_perm.video = locked 113 | elif lock_type == "videonote": 114 | curr_perm.videonote = locked 115 | elif lock_type == "document": 116 | curr_perm.document = locked 117 | elif lock_type == "photo": 118 | curr_perm.photo = locked 119 | elif lock_type == "sticker": 120 | curr_perm.sticker = locked 121 | elif lock_type == "gif": 122 | curr_perm.gif = locked 123 | elif lock_type == 'url': 124 | curr_perm.url = locked 125 | elif lock_type == 'bots': 126 | curr_perm.bots = locked 127 | elif lock_type == 'forward': 128 | curr_perm.forward = locked 129 | elif lock_type == 'game': 130 | curr_perm.game = locked 131 | elif lock_type == 'location': 132 | curr_perm.location = locked 133 | 134 | SESSION.add(curr_perm) 135 | SESSION.commit() 136 | 137 | 138 | def update_restriction(chat_id, restr_type, locked): 139 | with RESTR_LOCK: 140 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 141 | if not curr_restr: 142 | curr_restr = init_restrictions(chat_id) 143 | 144 | if restr_type == "messages": 145 | curr_restr.messages = locked 146 | elif restr_type == "media": 147 | curr_restr.media = locked 148 | elif restr_type == "other": 149 | curr_restr.other = locked 150 | elif restr_type == "previews": 151 | curr_restr.preview = locked 152 | elif restr_type == "all": 153 | curr_restr.messages = locked 154 | curr_restr.media = locked 155 | curr_restr.other = locked 156 | curr_restr.preview = locked 157 | SESSION.add(curr_restr) 158 | SESSION.commit() 159 | 160 | 161 | def is_locked(chat_id, lock_type): 162 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 163 | SESSION.close() 164 | 165 | if not curr_perm: 166 | return False 167 | 168 | elif lock_type == "sticker": 169 | return curr_perm.sticker 170 | elif lock_type == "photo": 171 | return curr_perm.photo 172 | elif lock_type == "audio": 173 | return curr_perm.audio 174 | elif lock_type == "voice": 175 | return curr_perm.voice 176 | elif lock_type == "contact": 177 | return curr_perm.contact 178 | elif lock_type == "video": 179 | return curr_perm.video 180 | elif lock_type == "videonote": 181 | return curr_perm.videonote 182 | elif lock_type == "document": 183 | return curr_perm.document 184 | elif lock_type == "gif": 185 | return curr_perm.gif 186 | elif lock_type == "url": 187 | return curr_perm.url 188 | elif lock_type == "bots": 189 | return curr_perm.bots 190 | elif lock_type == "forward": 191 | return curr_perm.forward 192 | elif lock_type == "game": 193 | return curr_perm.game 194 | elif lock_type == "location": 195 | return curr_perm.location 196 | 197 | 198 | def is_restr_locked(chat_id, lock_type): 199 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 200 | SESSION.close() 201 | 202 | if not curr_restr: 203 | return False 204 | 205 | if lock_type == "messages": 206 | return curr_restr.messages 207 | elif lock_type == "media": 208 | return curr_restr.media 209 | elif lock_type == "other": 210 | return curr_restr.other 211 | elif lock_type == "previews": 212 | return curr_restr.preview 213 | elif lock_type == "all": 214 | return curr_restr.messages and curr_restr.media and curr_restr.other and curr_restr.preview 215 | 216 | 217 | def get_locks(chat_id): 218 | try: 219 | return SESSION.query(Permissions).get(str(chat_id)) 220 | finally: 221 | SESSION.close() 222 | 223 | 224 | def get_restr(chat_id): 225 | try: 226 | return SESSION.query(Restrictions).get(str(chat_id)) 227 | finally: 228 | SESSION.close() 229 | 230 | 231 | def migrate_chat(old_chat_id, new_chat_id): 232 | with PERM_LOCK: 233 | perms = SESSION.query(Permissions).get(str(old_chat_id)) 234 | if perms: 235 | perms.chat_id = str(new_chat_id) 236 | SESSION.commit() 237 | 238 | with RESTR_LOCK: 239 | rest = SESSION.query(Restrictions).get(str(old_chat_id)) 240 | if rest: 241 | rest.chat_id = str(new_chat_id) 242 | SESSION.commit() 243 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/cust_filters_sql.py: -------------------------------------------------------------------------------- 1 | import re 2 | import threading 3 | from sqlalchemy.orm.exc import NoResultFound 4 | from sqlalchemy import Column, String, UnicodeText, Boolean, Integer, distinct, func 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class CustomFilters(BASE): 9 | __tablename__ = "cust_filters" 10 | chat_id = Column(String(14), primary_key=True) 11 | keyword = Column(UnicodeText, primary_key=True, nullable=False) 12 | reply = Column(UnicodeText, nullable=False) 13 | is_sticker = Column(Boolean, nullable=False, default=False) 14 | is_document = Column(Boolean, nullable=False, default=False) 15 | is_image = Column(Boolean, nullable=False, default=False) 16 | is_audio = Column(Boolean, nullable=False, default=False) 17 | is_voice = Column(Boolean, nullable=False, default=False) 18 | is_video = Column(Boolean, nullable=False, default=False) 19 | caption = Column(UnicodeText, nullable=True, default=None) 20 | 21 | has_buttons = Column(Boolean, nullable=False, default=False) 22 | # NOTE: Here for legacy purposes, to ensure older filters don't mess up. 23 | has_markdown = Column(Boolean, nullable=False, default=False) 24 | # NOTE: Here for -_- purposes, 25 | has_caption = Column(Boolean, nullable=False, default=False) 26 | 27 | 28 | def __init__(self, chat_id, keyword, reply, is_sticker=False, is_document=False, is_image=False, is_audio=False, 29 | is_voice=False, is_video=False, has_buttons=False): 30 | self.chat_id = str(chat_id) # ensure string 31 | self.keyword = keyword 32 | self.reply = reply 33 | self.is_sticker = is_sticker 34 | self.is_document = is_document 35 | self.is_image = is_image 36 | self.is_audio = is_audio 37 | self.is_voice = is_voice 38 | self.is_video = is_video 39 | self.has_buttons = has_buttons 40 | self.has_markdown = True 41 | 42 | 43 | def __repr__(self): 44 | return "" % self.chat_id 45 | 46 | def __eq__(self, other): 47 | return bool(isinstance(other, CustomFilters) 48 | and self.chat_id == other.chat_id 49 | and self.keyword == other.keyword) 50 | 51 | 52 | class Buttons(BASE): 53 | __tablename__ = "cust_filter_urls" 54 | id = Column(Integer, primary_key=True, autoincrement=True) 55 | chat_id = Column(String(14), primary_key=True) 56 | keyword = Column(UnicodeText, primary_key=True) 57 | name = Column(UnicodeText, nullable=False) 58 | url = Column(UnicodeText, nullable=False) 59 | same_line = Column(Boolean, default=False) 60 | 61 | def __init__(self, chat_id, keyword, name, url, same_line=False): 62 | self.chat_id = str(chat_id) 63 | self.keyword = keyword 64 | self.name = name 65 | self.url = url 66 | self.same_line = same_line 67 | 68 | 69 | CustomFilters.__table__.create(checkfirst=True) 70 | Buttons.__table__.create(checkfirst=True) 71 | 72 | CUST_FILT_LOCK = threading.RLock() 73 | BUTTON_LOCK = threading.RLock() 74 | CHAT_FILTERS = {} 75 | 76 | 77 | def get_btn_with_di(ntb_gtid): 78 | try: 79 | return SESSION.query(Buttons).filter( 80 | Buttons.id == ntb_gtid 81 | ).one() 82 | except NoResultFound: 83 | return False 84 | finally: 85 | SESSION.close() 86 | 87 | 88 | def get_all_filters(): 89 | try: 90 | return SESSION.query(CustomFilters).all() 91 | finally: 92 | SESSION.close() 93 | 94 | 95 | def add_filter(chat_id, keyword, reply, is_sticker=False, is_document=False, is_image=False, is_audio=False, 96 | is_voice=False, is_video=False, buttons=None, caption=None, has_caption=False): 97 | if buttons is None: 98 | buttons = [] 99 | 100 | with CUST_FILT_LOCK: 101 | prev = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 102 | if prev: 103 | with BUTTON_LOCK: 104 | prev_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 105 | Buttons.keyword == keyword).all() 106 | for btn in prev_buttons: 107 | SESSION.delete(btn) 108 | SESSION.delete(prev) 109 | 110 | filt = CustomFilters(str(chat_id), keyword, reply, is_sticker, is_document, is_image, is_audio, is_voice, 111 | is_video, bool(buttons)) 112 | if has_caption: 113 | filt.caption = caption 114 | filt.has_caption = has_caption 115 | 116 | SESSION.add(filt) 117 | SESSION.commit() 118 | 119 | for b_name, url, same_line in buttons: 120 | add_note_button_to_db(chat_id, keyword, b_name, url, same_line) 121 | 122 | 123 | def remove_filter(chat_id, keyword): 124 | with CUST_FILT_LOCK: 125 | filt = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 126 | if filt: 127 | with BUTTON_LOCK: 128 | prev_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 129 | Buttons.keyword == keyword).all() 130 | for btn in prev_buttons: 131 | SESSION.delete(btn) 132 | 133 | SESSION.delete(filt) 134 | SESSION.commit() 135 | return True 136 | 137 | SESSION.close() 138 | return False 139 | 140 | 141 | def get_all_chat_triggers(chat_id): 142 | # print("AwACAgQAAx0CS3YfYQACIOFgbYk0c-MPg2-h9r4jJCizTZFEEQACTwsAAvyBaFP95oT7U9NwHR4E") 143 | return get_chat_filters(chat_id) 144 | 145 | 146 | def get_chat_filters(chat_id): 147 | try: 148 | return SESSION.query(CustomFilters).filter(CustomFilters.chat_id == str(chat_id)).order_by( 149 | func.length(CustomFilters.keyword).desc()).order_by(CustomFilters.keyword.asc()).all() 150 | finally: 151 | SESSION.close() 152 | 153 | 154 | def get_filter(chat_id, keyword): 155 | try: 156 | return SESSION.query(CustomFilters).get((str(chat_id), keyword)) 157 | finally: 158 | SESSION.close() 159 | 160 | 161 | def add_note_button_to_db(chat_id, keyword, b_name, url, same_line): 162 | with BUTTON_LOCK: 163 | button = Buttons(chat_id, keyword, b_name, url, same_line) 164 | SESSION.add(button) 165 | SESSION.commit() 166 | 167 | 168 | def get_buttons(chat_id, keyword): 169 | try: 170 | return SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), Buttons.keyword == keyword).order_by( 171 | Buttons.id).all() 172 | finally: 173 | SESSION.close() 174 | 175 | 176 | def num_filters(): 177 | try: 178 | return SESSION.query(CustomFilters).count() 179 | finally: 180 | SESSION.close() 181 | 182 | def num_filters_per_chat(chat_id): 183 | try: 184 | return SESSION.query(CustomFilters).filter( 185 | CustomFilters.chat_id == str(chat_id) 186 | ).count() 187 | finally: 188 | SESSION.close() 189 | 190 | 191 | def num_chats(): 192 | try: 193 | return SESSION.query(func.count(distinct(CustomFilters.chat_id))).scalar() 194 | finally: 195 | SESSION.close() 196 | 197 | 198 | def __load_chat_filters(): 199 | # print("AwACAgQAAx0CS3YfYQACIOFgbYk0c-MPg2-h9r4jJCizTZFEEQACTwsAAvyBaFP95oT7U9NwHR4E") 200 | pass 201 | 202 | 203 | def migrate_chat(old_chat_id, new_chat_id): 204 | with CUST_FILT_LOCK: 205 | chat_filters = SESSION.query(CustomFilters).filter(CustomFilters.chat_id == str(old_chat_id)).all() 206 | for filt in chat_filters: 207 | filt.chat_id = str(new_chat_id) 208 | SESSION.commit() 209 | # CHAT_FILTERS[str(new_chat_id)] = CHAT_FILTERS[str(old_chat_id)] 210 | # print("AwACAgQAAx0CS3YfYQACIOFgbYk0c-MPg2-h9r4jJCizTZFEEQACTwsAAvyBaFP95oT7U9NwHR4E") 211 | # del CHAT_FILTERS[str(old_chat_id)] 212 | 213 | with BUTTON_LOCK: 214 | chat_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 215 | for btn in chat_buttons: 216 | btn.chat_id = str(new_chat_id) 217 | SESSION.commit() 218 | 219 | # -_- 220 | # print("AwACAgQAAx0CS3YfYQACIOFgbYk0c-MPg2-h9r4jJCizTZFEEQACTwsAAvyBaFP95oT7U9NwHR4E") 221 | -------------------------------------------------------------------------------- /tg_bot/modules/connection.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from telegram import ParseMode 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, Filters 7 | from telegram.ext.dispatcher import run_async 8 | from telegram.utils.helpers import mention_html 9 | 10 | import tg_bot.modules.sql.connection_sql as sql 11 | from tg_bot import dispatcher, LOGGER, SUDO_USERS 12 | from tg_bot.modules.helper_funcs.chat_status import bot_admin, user_admin, is_user_admin, can_restrict 13 | from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text 14 | from tg_bot.modules.helper_funcs.string_handling import extract_time 15 | 16 | # from tg_bot.modules.translations.strings import tld 17 | 18 | from tg_bot.modules.keyboard import keyboard 19 | 20 | @user_admin 21 | @run_async 22 | def allow_connections(bot: Bot, update: Update, args: List[str]) -> str: 23 | chat = update.effective_chat # type: Optional[Chat] 24 | if chat.type != chat.PRIVATE: 25 | if len(args) >= 1: 26 | var = args[0] 27 | print(var) 28 | if (var == "no"): 29 | sql.set_allow_connect_to_chat(chat.id, False) 30 | update.effective_message.reply_text("Disabled connections to this chat for users") 31 | elif(var == "yes"): 32 | sql.set_allow_connect_to_chat(chat.id, True) 33 | update.effective_message.reply_text("Enabled connections to this chat for users") 34 | else: 35 | update.effective_message.reply_text("Please enter on/yes/off/no in group!") 36 | else: 37 | update.effective_message.reply_text("Please enter on/yes/off/no in group!") 38 | else: 39 | update.effective_message.reply_text("Please enter on/yes/off/no in group!") 40 | 41 | 42 | @run_async 43 | def connect_chat(bot, update, args): 44 | chat = update.effective_chat # type: Optional[Chat] 45 | user = update.effective_user # type: Optional[User] 46 | if update.effective_chat.type == 'private': 47 | if len(args) >= 1: 48 | try: 49 | connect_chat = int(args[0]) 50 | except ValueError: 51 | update.effective_message.reply_text("Invalid Chat ID provided!") 52 | if (bot.get_chat_member(connect_chat, update.effective_message.from_user.id).status in ('administrator', 'creator') or 53 | (sql.allow_connect_to_chat(connect_chat) == True) and 54 | bot.get_chat_member(connect_chat, update.effective_message.from_user.id).status in ('member')) or ( 55 | user.id in SUDO_USERS): 56 | 57 | connection_status = sql.connect(update.effective_message.from_user.id, connect_chat) 58 | if connection_status: 59 | chat_name = dispatcher.bot.getChat(connected(bot, update, chat, user.id, need_admin=False)).title 60 | update.effective_message.reply_text("Successfully connected to *{}*".format(chat_name), parse_mode=ParseMode.MARKDOWN) 61 | 62 | #Add chat to connection history 63 | history = sql.get_history(user.id) 64 | if history: 65 | #Vars 66 | if history.chat_id1: 67 | history1 = int(history.chat_id1) 68 | if history.chat_id2: 69 | history2 = int(history.chat_id2) 70 | if history.chat_id3: 71 | history3 = int(history.chat_id3) 72 | if history.updated: 73 | number = history.updated 74 | 75 | if number == 1 and connect_chat != history2 and connect_chat != history3: 76 | history1 = connect_chat 77 | number = 2 78 | elif number == 2 and connect_chat != history1 and connect_chat != history3: 79 | history2 = connect_chat 80 | number = 3 81 | elif number >= 3 and connect_chat != history2 and connect_chat != history1: 82 | history3 = connect_chat 83 | number = 1 84 | else: 85 | print("Error") 86 | 87 | print(history.updated) 88 | print(number) 89 | 90 | sql.add_history(user.id, history1, history2, history3, number) 91 | print(history.user_id, history.chat_id1, history.chat_id2, history.chat_id3, history.updated) 92 | else: 93 | sql.add_history(user.id, connect_chat, "0", "0", 2) 94 | #Rebuild user's keyboard 95 | keyboard(bot, update) 96 | 97 | else: 98 | update.effective_message.reply_text("Connection failed!") 99 | else: 100 | update.effective_message.reply_text("Connections to this chat not allowed!") 101 | else: 102 | update.effective_message.reply_text("Input chat ID to connect!") 103 | history = sql.get_history(user.id) 104 | print(history.user_id, history.chat_id1, history.chat_id2, history.chat_id3, history.updated) 105 | 106 | else: 107 | update.effective_message.reply_text("Usage limited to PMs only!") 108 | 109 | 110 | def disconnect_chat(bot, update): 111 | if update.effective_chat.type == 'private': 112 | disconnection_status = sql.disconnect(update.effective_message.from_user.id) 113 | if disconnection_status: 114 | sql.disconnected_chat = update.effective_message.reply_text("Disconnected from chat!") 115 | #Rebuild user's keyboard 116 | keyboard(bot, update) 117 | else: 118 | update.effective_message.reply_text("Disconnection unsuccessfull!") 119 | else: 120 | update.effective_message.reply_text("Usage restricted to PMs only") 121 | 122 | 123 | def connected(bot, update, chat, user_id, need_admin=True): 124 | if chat.type == chat.PRIVATE and sql.get_connected_chat(user_id): 125 | conn_id = sql.get_connected_chat(user_id).chat_id 126 | if (bot.get_chat_member(conn_id, user_id).status in ('administrator', 'creator') or 127 | (sql.allow_connect_to_chat(connect_chat) == True) and 128 | bot.get_chat_member(user_id, update.effective_message.from_user.id).status in ('member')) or ( 129 | user_id in SUDO_USERS): 130 | if need_admin == True: 131 | if bot.get_chat_member(conn_id, update.effective_message.from_user.id).status in ('administrator', 'creator') or user_id in SUDO_USERS: 132 | return conn_id 133 | else: 134 | update.effective_message.reply_text("You need to be a admin in a connected group!") 135 | exit(1) 136 | else: 137 | return conn_id 138 | else: 139 | update.effective_message.reply_text("Group changed rights connection or you are not admin anymore.\nI'll disconnect you.") 140 | disconnect_chat(bot, update) 141 | exit(1) 142 | else: 143 | return False 144 | 145 | 146 | 147 | __help__ = """ 148 | Actions are available with connected groups: 149 | • View and edit notes 150 | • View and edit filters 151 | • More in future! 152 | 153 | - /connect : Connect to remote chat 154 | - /disconnect: Disconnect from chat 155 | - /allowconnect on/yes/off/no: Allow connect users to group 156 | """ 157 | 158 | __mod_name__ = "Connections" 159 | 160 | CONNECT_CHAT_HANDLER = CommandHandler("connect", connect_chat, allow_edited=True, pass_args=True) 161 | DISCONNECT_CHAT_HANDLER = CommandHandler("disconnect", disconnect_chat, allow_edited=True) 162 | ALLOW_CONNECTIONS_HANDLER = CommandHandler("allowconnect", allow_connections, allow_edited=True, pass_args=True) 163 | 164 | dispatcher.add_handler(CONNECT_CHAT_HANDLER) 165 | dispatcher.add_handler(DISCONNECT_CHAT_HANDLER) 166 | dispatcher.add_handler(ALLOW_CONNECTIONS_HANDLER) 167 | -------------------------------------------------------------------------------- /tg_bot/modules/spin.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, Filters, MessageHandler 7 | from telegram.ext.dispatcher import run_async 8 | from telegram.utils.helpers import mention_html 9 | 10 | from tg_bot import dispatcher 11 | from tg_bot.modules.helper_funcs.chat_status import ( 12 | bot_admin, 13 | user_admin, 14 | can_pin, 15 | can_delete 16 | ) 17 | from tg_bot.modules.log_channel import loggable 18 | from tg_bot.modules.sql import pin_sql as sql 19 | 20 | 21 | PMW_GROUP = 12 22 | 23 | 24 | @run_async 25 | @bot_admin 26 | @can_pin 27 | @user_admin 28 | @loggable 29 | def pin(bot: Bot, update: Update, args: List[str]) -> str: 30 | user = update.effective_user # type: Optional[User] 31 | chat = update.effective_chat # type: Optional[Chat] 32 | 33 | is_group = chat.type != "private" and chat.type != "channel" 34 | 35 | prev_message = update.effective_message.reply_to_message 36 | 37 | is_silent = True 38 | if len(args) >= 1: 39 | is_silent = not ( 40 | args[0].lower() == 'notify' or 41 | args[0].lower() == 'loud' or 42 | args[0].lower() == 'violent' 43 | ) 44 | 45 | if prev_message and is_group: 46 | try: 47 | bot.pinChatMessage( 48 | chat.id, 49 | prev_message.message_id, 50 | disable_notification=is_silent 51 | ) 52 | except BadRequest as excp: 53 | if excp.message == "Chat_not_modified": 54 | pass 55 | else: 56 | raise 57 | sql.add_mid(chat.id, prev_message.message_id) 58 | return "{}:" \ 59 | "\n#PINNED" \ 60 | "\nAdmin: {}".format( 61 | html.escape(chat.title), 62 | mention_html(user.id, user.first_name) 63 | ) 64 | 65 | return "" 66 | 67 | 68 | @run_async 69 | @bot_admin 70 | @can_pin 71 | @user_admin 72 | @loggable 73 | def unpin(bot: Bot, update: Update) -> str: 74 | chat = update.effective_chat 75 | user = update.effective_user # type: Optional[User] 76 | 77 | try: 78 | bot.unpinChatMessage(chat.id) 79 | except BadRequest as excp: 80 | if excp.message == "Chat_not_modified": 81 | pass 82 | else: 83 | raise 84 | sql.remove_mid(chat.id) 85 | return "{}:" \ 86 | "\n#UNPINNED" \ 87 | "\nAdmin: {}".format(html.escape(chat.title), 88 | mention_html(user.id, user.first_name)) 89 | 90 | 91 | @run_async 92 | @bot_admin 93 | @can_pin 94 | @user_admin 95 | @loggable 96 | def anti_channel_pin(bot: Bot, update: Update, args: List[str]) -> str: 97 | chat = update.effective_chat # type: Optional[Chat] 98 | user = update.effective_user # type: Optional[User] 99 | 100 | if not args: 101 | update.effective_message.reply_text("I understand 'on/yes' or 'off/no' only!") 102 | return "" 103 | 104 | if args[0].lower() in ("on", "yes"): 105 | sql.add_acp_o(str(chat.id), True) 106 | update.effective_message.reply_text("I'll try to unpin Telegram Channel messages!") 107 | return "{}:" \ 108 | "\n#ANTI_CHANNEL_PIN" \ 109 | "\nAdmin: {}" \ 110 | "\nHas toggled ANTI CHANNEL PIN to ON.".format(html.escape(chat.title), 111 | mention_html(user.id, user.first_name)) 112 | elif args[0].lower() in ("off", "no"): 113 | sql.add_acp_o(str(chat.id), False) 114 | update.effective_message.reply_text("I won't unpin Telegram Channel Messages!") 115 | return "{}:" \ 116 | "\n#ANTI_CHANNEL_PIN" \ 117 | "\nAdmin: {}" \ 118 | "\nHas toggled ANTI CHANNEL PIN to OFF.".format(html.escape(chat.title), 119 | mention_html(user.id, user.first_name)) 120 | else: 121 | # idek what you're writing, say yes or no 122 | update.effective_message.reply_text("I understand 'on/yes' or 'off/no' only!") 123 | return "" 124 | 125 | 126 | @run_async 127 | @bot_admin 128 | # @can_delete 129 | @user_admin 130 | @loggable 131 | def clean_linked_channel(bot: Bot, update: Update, args: List[str]) -> str: 132 | chat = update.effective_chat # type: Optional[Chat] 133 | user = update.effective_user # type: Optional[User] 134 | 135 | if not args: 136 | update.effective_message.reply_text("I understand 'on/yes' or 'off/no' only!") 137 | return "" 138 | 139 | if args[0].lower() in ("on", "yes"): 140 | sql.add_ldp_m(str(chat.id), True) 141 | update.effective_message.reply_text("I'll try to delete Telegram Channel messages!") 142 | return "{}:" \ 143 | "\n#CLEAN_CHANNEL_MESSAGES" \ 144 | "\nAdmin: {}" \ 145 | "\nHas toggled DELETE CHANNEL MESSAGES to ON.".format(html.escape(chat.title), 146 | mention_html(user.id, user.first_name)) 147 | elif args[0].lower() in ("off", "no"): 148 | sql.add_ldp_m(str(chat.id), False) 149 | update.effective_message.reply_text("I won't delete Telegram Channel Messages!") 150 | return "{}:" \ 151 | "\n#CLEAN_CHANNEL_MESSAGES" \ 152 | "\nAdmin: {}" \ 153 | "\nHas toggled DELETE CHANNEL MESSAGES to OFF.".format(html.escape(chat.title), 154 | mention_html(user.id, user.first_name)) 155 | else: 156 | # idek what you're writing, say yes or no 157 | update.effective_message.reply_text("I understand 'on/yes' or 'off/no' only!") 158 | return "" 159 | 160 | 161 | @run_async 162 | def amwltro_conreko(bot: Bot, update: Update): 163 | chat = update.effective_chat # type: Optional[Chat] 164 | message = update.effective_message # type: Optional[Message] 165 | sctg = sql.get_current_settings(chat.id) 166 | """we apparently do not receive any update for PINned messages 167 | """ 168 | if sctg and sctg.message_id != 0 and message.from_user.id == 777000: 169 | if sctg.suacpmo: 170 | try: 171 | bot.unpin_chat_message(chat.id) 172 | except: 173 | pass 174 | pin_chat_message(bot, chat.id, sctg.message_id, True) 175 | if sctg.scldpmo: 176 | try: 177 | message.delete() 178 | except: 179 | pass 180 | pin_chat_message(bot, chat.id, sctg.message_id, True) 181 | 182 | 183 | def pin_chat_message(bot, chat_id, message_id, is_silent): 184 | try: 185 | bot.pinChatMessage( 186 | chat_id, 187 | message_id, 188 | disable_notification=is_silent 189 | ) 190 | except BadRequest as excp: 191 | if excp.message == "Chat_not_modified": 192 | pass 193 | """else: 194 | raise""" 195 | 196 | 197 | """The below help string 198 | is copied without permission 199 | from the popular Telegram 609517172 RoBot""" 200 | 201 | __help__ = """ 202 | 203 | *Admin only:* 204 | - /pin: silently pins the message replied to 205 | : add 'loud' or 'notify' to give notifs to users. 206 | - /unpin: unpins the currently pinned message 207 | - /antichannelpin : Don't let telegram auto-pin linked channels. 208 | - /cleanlinked : Delete messages sent by the linked channel. 209 | 210 | Note: 211 | 212 | When using antichannel pins, make sure to use the /unpin command, 213 | instead of doing it manually. 214 | 215 | Otherwise, the old message will get re-pinned when the channel sends any messages. 216 | """ 217 | 218 | __mod_name__ = "Pins" 219 | 220 | 221 | PIN_HANDLER = CommandHandler("pin", pin, pass_args=True, filters=Filters.group) 222 | UNPIN_HANDLER = CommandHandler("unpin", unpin, filters=Filters.group) 223 | ATCPIN_HANDLER = CommandHandler("antichannelpin", anti_channel_pin, pass_args=True, filters=Filters.group) 224 | CLCLDC_HANDLER = CommandHandler("cleanlinked", clean_linked_channel, pass_args=True, filters=Filters.group) 225 | AMWLTRO_HANDLER = MessageHandler(Filters.forwarded & Filters.group, amwltro_conreko, edited_updates=False) 226 | 227 | dispatcher.add_handler(PIN_HANDLER) 228 | dispatcher.add_handler(UNPIN_HANDLER) 229 | dispatcher.add_handler(ATCPIN_HANDLER) 230 | dispatcher.add_handler(CLCLDC_HANDLER) 231 | dispatcher.add_handler(AMWLTRO_HANDLER, PMW_GROUP) 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tgbot 2 | A modular telegram Python bot running on python3 with an sqlalchemy database. 3 | 4 | Originally a simple group management bot with multiple admin features, it has evolved, becoming extremely modular and 5 | simple to use. 6 | 7 | Can be found on telegram as [Marie](https://t.me/BanhammerMarie_bot). 8 | 9 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/SpEcHiDe/PSonOfLars_BHMarie) 10 | 11 | Marie and I are moderating a [support group](https://t.me/MarieSupport), where you can ask for help setting up your 12 | bot, discover/request new features, report bugs, and stay in the loop whenever a new update is available. Of course 13 | I'll also help when a database schema changes, and some table column needs to be modified/added. Note to maintainers that all schema changes will be found in the commit messages, and its their responsibility to read any new commits. 14 | 15 | Join the [news channel](https://t.me/MarieNews) if you just want to stay in the loop about new features or 16 | announcements. 17 | 18 | Alternatively, [find me on telegram](https://t.me/SonOfLars)! (Keep all support questions in the support chat, where more people can help you.) 19 | 20 | 21 | ## IMPORTANT NOTICE: 22 | 23 | This project is no longer under active maintenance. Occasional bug fixes may be released, but no new features are scheduled to be added. 24 | Users of [Marie](https://t.me/BanhammerMarie_bot) are encouraged to migrate to [PyroGramBot](https://t.me/OwneRoBot), which 25 | is the improved version of this project, written in PyroGram, with the intention to avoid various BOT API hiccups, and to protect group chats from (user)bots, floods and even lifeless losers. 26 | 27 | 28 | ## Starting the bot. 29 | 30 | Once you've setup your database and your configuration (see below) is complete, simply run: 31 | 32 | `python3 -m tg_bot` 33 | 34 | 35 | ## Setting up the bot (Read this before trying to use!): 36 | Please make sure to use python3.6, as I cannot guarantee everything will work as expected on older python versions! 37 | This is because markdown parsing is done by iterating through a dict, which are ordered by default in 3.6. 38 | 39 | ### Configuration 40 | 41 | There are two possible ways of configuring your bot: a config.py file, or ENV variables. 42 | 43 | The prefered version is to use a `config.py` file, as it makes it easier to see all your settings grouped together. 44 | This file should be placed in your `tg_bot` folder, alongside the `__main__.py` file . 45 | This is where your bot token will be loaded from, as well as your database URI (if you're using a database), and most of 46 | your other settings. 47 | 48 | It is recommended to import sample_config and extend the Config class, as this will ensure your config contains all 49 | defaults set in the sample_config, hence making it easier to upgrade. 50 | 51 | An example `config.py` file could be: 52 | ``` 53 | from tg_bot.sample_config import Config 54 | 55 | 56 | class Development(Config): 57 | OWNER_ID = 254318997 # my telegram ID 58 | OWNER_USERNAME = "SonOfLars" # my telegram username 59 | API_KEY = "your bot api key" # my api key, as provided by the botfather 60 | SQLALCHEMY_DATABASE_URI = 'postgresql://username:password@localhost:5432/database' # sample db credentials 61 | MESSAGE_DUMP = '-1234567890' # some group chat that your bot is a member of 62 | USE_MESSAGE_DUMP = True 63 | SUDO_USERS = [18673980, 83489514] # List of id's for users which have sudo access to the bot. 64 | LOAD = [] 65 | NO_LOAD = ['translation'] 66 | ``` 67 | 68 | If you can't have a config.py file (EG on heroku), it is also possible to use environment variables. 69 | The following env variables are supported: 70 | - `ENV`: Setting this to ANYTHING will enable env variables 71 | 72 | - `TOKEN`: Your bot token, as a string. 73 | - `OWNER_ID`: An integer of consisting of your owner ID 74 | - `OWNER_USERNAME`: Your username 75 | 76 | - `DATABASE_URL`: Your database URL 77 | - `MESSAGE_DUMP`: optional: a chat where your replied saved messages are stored, to stop people deleting their old 78 | - `LOAD`: Space separated list of modules you would like to load 79 | - `NO_LOAD`: Space separated list of modules you would like NOT to load 80 | - `WEBHOOK`: Setting this to ANYTHING will enable webhooks when in env mode 81 | messages 82 | - `URL`: The URL your webhook should connect to (only needed for webhook mode) 83 | 84 | - `SUDO_USERS`: A space separated list of user_ids which should be considered sudo users 85 | - `SUPPORT_USERS`: A space separated list of user_ids which should be considered support users (can gban/ungban, 86 | nothing else) 87 | - `WHITELIST_USERS`: A space separated list of user_ids which should be considered whitelisted - they can't be banned. 88 | - `DONATION_LINK`: Optional: link where you would like to receive donations. 89 | - `CERT_PATH`: Path to your webhook certificate 90 | - `PORT`: Port to use for your webhooks 91 | - `DEL_CMDS`: Whether to delete commands from users which don't have rights to use that command 92 | - `STRICT_GBAN`: Enforce gbans across new groups as well as old groups. When a gbanned user talks, he will be banned. 93 | - `WORKERS`: Number of threads to use. 8 is the recommended (and default) amount, but your experience may vary. 94 | __Note__ that going crazy with more threads wont necessarily speed up your bot, given the large amount of sql data 95 | accesses, and the way python asynchronous calls work. 96 | - `BAN_STICKER`: Which sticker to use when banning people. 97 | - `ALLOW_EXCL`: Whether to allow using exclamation marks ! for commands as well as /. 98 | 99 | ### Python dependencies 100 | 101 | Install the necessary python dependencies by moving to the project directory and running: 102 | 103 | `pip3 install -r requirements.txt`. 104 | 105 | This will install all necessary python packages. 106 | 107 | ### Database 108 | 109 | If you wish to use a database-dependent module (eg: locks, notes, userinfo, users, filters, welcomes), 110 | you'll need to have a database installed on your system. I use postgres, so I recommend using it for optimal compatibility. 111 | 112 | In the case of postgres, this is how you would set up a the database on a debian/ubuntu system. Other distributions may vary. 113 | 114 | - install postgresql: 115 | 116 | `sudo apt-get update && sudo apt-get install postgresql` 117 | 118 | - change to the postgres user: 119 | 120 | `sudo su - postgres` 121 | 122 | - create a new database user (change YOUR_USER appropriately): 123 | 124 | `createuser -P -s -e YOUR_USER` 125 | 126 | This will be followed by you needing to input your password. 127 | 128 | - create a new database table: 129 | 130 | `createdb -O YOUR_USER YOUR_DB_NAME` 131 | 132 | Change YOUR_USER and YOUR_DB_NAME appropriately. 133 | 134 | - finally: 135 | 136 | `psql YOUR_DB_NAME -h YOUR_HOST YOUR_USER` 137 | 138 | This will allow you to connect to your database via your terminal. 139 | By default, YOUR_HOST should be 0.0.0.0:5432. 140 | 141 | You should now be able to build your database URI. This will be: 142 | 143 | `sqldbtype://username:pw@hostname:port/db_name` 144 | 145 | Replace sqldbtype with whichever db youre using (eg postgres, mysql, sqllite, etc) 146 | repeat for your username, password, hostname (localhost?), port (5432?), and db name. 147 | 148 | ## Modules 149 | ### Setting load order. 150 | 151 | The module load order can be changed via the `LOAD` and `NO_LOAD` configuration settings. 152 | These should both represent lists. 153 | 154 | If `LOAD` is an empty list, all modules in `modules/` will be selected for loading by default. 155 | 156 | If `NO_LOAD` is not present, or is an empty list, all modules selected for loading will be loaded. 157 | 158 | If a module is in both `LOAD` and `NO_LOAD`, the module will not be loaded - `NO_LOAD` takes priority. 159 | 160 | ### Creating your own modules. 161 | 162 | Creating a module has been simplified as much as possible - but do not hesitate to suggest further simplification. 163 | 164 | All that is needed is that your .py file be in the modules folder. 165 | 166 | To add commands, make sure to import the dispatcher via 167 | 168 | `from tg_bot import dispatcher`. 169 | 170 | You can then add commands using the usual 171 | 172 | `dispatcher.add_handler()`. 173 | 174 | Assigning the `__help__` variable to a string describing this modules' available 175 | commands will allow the bot to load it and add the documentation for 176 | your module to the `/help` command. Setting the `__mod_name__` variable will also allow you to use a nicer, user 177 | friendly name for a module. 178 | 179 | The `__migrate__()` function is used for migrating chats - when a chat is upgraded to a supergroup, the ID changes, so 180 | it is necessary to migrate it in the db. 181 | 182 | The `__stats__()` function is for retrieving module statistics, eg number of users, number of chats. This is accessed 183 | through the `/stats` command, which is only available to the bot owner. 184 | --------------------------------------------------------------------------------