├── runtime.txt ├── tg_bot ├── modules │ ├── helper_funcs │ │ ├── __init__.py │ │ ├── handlers.py │ │ ├── perms.py │ │ ├── cas_api.py │ │ ├── git_api.py │ │ ├── filters.py │ │ ├── extraction.py │ │ ├── misc.py │ │ ├── chat_status.py │ │ └── msg_types.py │ ├── sql │ │ ├── __init__.py │ │ ├── rules_sql.py │ │ ├── antiarabic_sql.py │ │ ├── github_sql.py │ │ ├── afk_sql.py │ │ ├── global_kicks_sql.py │ │ ├── rss_sql.py │ │ ├── log_channel_sql.py │ │ ├── userinfo_sql.py │ │ ├── reporting_sql.py │ │ ├── disable_sql.py │ │ ├── antiflood_sql.py │ │ ├── blacklist_sql.py │ │ ├── global_bans_sql.py │ │ ├── users_sql.py │ │ ├── notes_sql.py │ │ ├── cust_filters_sql.py │ │ ├── locks_sql.py │ │ └── warns_sql.py │ ├── __init__.py │ ├── shout.py │ ├── ud.py │ ├── getlink.py │ ├── stickers.py │ ├── miui.py │ ├── systools.py │ ├── leave.py │ ├── antiarabic.py │ ├── users.py │ ├── sed.py │ ├── msg_deleting.py │ ├── rules.py │ ├── reporting.py │ ├── webtools.py │ ├── userinfo.py │ ├── global_kicks.py │ ├── log_channel.py │ ├── disable.py │ └── blacklist.py ├── sample_config.py └── __init__.py ├── Procfile ├── .gitignore ├── requirements.txt ├── Dockerfile ├── docker-compose.yml ├── CONTRIBUTING.md └── README.md /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.6 2 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python -m tg_bot 2 | web: python -m tg_bot 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tg_bot/config.py 2 | *.pyc 3 | .idea/ 4 | .project 5 | .pydevproject 6 | .directory 7 | .vscode 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future 2 | emoji 3 | requests 4 | sqlalchemy==1.4 5 | python-telegram-bot==13.9 6 | psycopg2-binary==2.9.5 7 | pyowm 8 | feedparser 9 | geopy 10 | speedtest-cli>=2.0.2 11 | bs4 12 | lxml 13 | pytz 14 | pyyaml 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | WORKDIR /tgbot 4 | COPY . /tgbot 5 | RUN pip install --no-cache-dir -r requirements.txt 6 | 7 | RUN apt-get update -y 8 | RUN apt-get install -y iputils-ping 9 | 10 | CMD ["python", "-m", "tg_bot"] 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:latest 4 | container_name: tgbot_db 5 | restart: always 6 | environment: 7 | POSTGRES_USER: postgres 8 | POSTGRES_PASSWORD: changethis 9 | POSTGRES_DB: tgbot 10 | healthcheck: 11 | test: ["CMD-SHELL", "pg_isready -U postgres"] 12 | interval: 1s 13 | timeout: 5s 14 | retries: 10 15 | tgbot: 16 | restart: always 17 | container_name: tgbot 18 | build: . 19 | depends_on: 20 | postgres: 21 | condition: service_healthy 22 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/handlers.py: -------------------------------------------------------------------------------- 1 | import telegram.ext as tg 2 | from telegram import Update 3 | from tg_bot import ALLOW_EXCL 4 | 5 | CMD_STARTERS = ('/', '!') 6 | 7 | 8 | class CustomCommandHandler(tg.CommandHandler): 9 | def __init__(self, command, callback, **kwargs): 10 | if "admin_ok" in kwargs: 11 | del kwargs["admin_ok"] 12 | super().__init__(command, callback, **kwargs) 13 | 14 | 15 | class CustomRegexHandler(tg.MessageHandler): 16 | def __init__(self, pattern, callback, friendly="", **kwargs): 17 | super().__init__(tg.Filters.regex(pattern), callback, **kwargs) 18 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/perms.py: -------------------------------------------------------------------------------- 1 | from telegram import ChatMember, Update 2 | from tg_bot import SUDO_USERS 3 | 4 | ADMIN_PERMS = [ 5 | "can_delete_messages", "can_restrict_members", "can_pin_messages", 6 | "can_promote_members" 7 | ] 8 | 9 | MESSAGES = [ 10 | "You don't have sufficient permissions to delete messages!", 11 | "You don't have sufficient permissions to restrict users!", 12 | "You don't have sufficient permissions to pin messages!", 13 | "You don't have sufficient permissions to promote users!" 14 | ] 15 | 16 | 17 | def check_perms(update: Update, type: str): 18 | chat = update.effective_chat 19 | user = update.effective_user 20 | 21 | admin = chat.get_member(int(user.id)) 22 | admin_perms = admin[ADMIN_PERMS[type]] if admin[ 23 | "status"] != "creator" and user.id not in SUDO_USERS else True 24 | 25 | if not admin_perms: 26 | update.effective_message.reply_text(MESSAGES[type]) 27 | return False 28 | 29 | return True 30 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/cas_api.py: -------------------------------------------------------------------------------- 1 | import urllib.request as url 2 | import json 3 | import datetime 4 | import requests 5 | 6 | VERSION = "1.3.3" 7 | CAS_QUERY_URL = "https://api.cas.chat/check?user_id=" 8 | DL_DIR = "./csvExports" 9 | 10 | 11 | def get_user_data(user_id): 12 | with requests.request('GET', CAS_QUERY_URL + str(user_id)) as userdata_raw: 13 | userdata = json.loads(userdata_raw.text) 14 | return userdata 15 | 16 | 17 | def isbanned(userdata): 18 | return userdata['ok'] 19 | 20 | 21 | def banchecker(user_id): 22 | return isbanned(get_user_data(user_id)) 23 | 24 | 25 | def vercheck() -> str: 26 | return str(VERSION) 27 | 28 | 29 | def offenses(user_id): 30 | userdata = get_user_data(user_id) 31 | try: 32 | offenses = userdata['result']['offenses'] 33 | return str(offenses) 34 | except: 35 | return None 36 | 37 | 38 | def timeadded(user_id): 39 | userdata = get_user_data(user_id) 40 | try: 41 | timeEp = userdata['result']['time_added'] 42 | timeHuman = datetime.datetime.utcfromtimestamp(timeEp).strftime( 43 | '%H:%M:%S, %d-%m-%Y') 44 | return timeHuman 45 | except: 46 | return None 47 | -------------------------------------------------------------------------------- /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 = [ 10 | basename(f)[:-3] for f in mod_paths 11 | if isfile(f) and f.endswith(".py") and not f.endswith('__init__.py') 12 | ] 13 | 14 | if LOAD or NO_LOAD: 15 | to_load = LOAD 16 | if to_load: 17 | if not all( 18 | any(mod == module_name for module_name in all_modules) 19 | for mod in to_load): 20 | LOGGER.error("Invalid loadorder names. Quitting.") 21 | quit(1) 22 | 23 | else: 24 | to_load = all_modules 25 | 26 | if NO_LOAD: 27 | LOGGER.info("Not loading: {}".format(NO_LOAD)) 28 | return [item for item in to_load if item not in NO_LOAD] 29 | 30 | return to_load 31 | 32 | return all_modules 33 | 34 | 35 | ALL_MODULES = sorted(__list_all_modules()) 36 | LOGGER.info("Modules to load: %s", str(ALL_MODULES)) 37 | __all__ = ALL_MODULES 38 | -------------------------------------------------------------------------------- /tg_bot/modules/shout.py: -------------------------------------------------------------------------------- 1 | from telegram import Update, Bot 2 | from telegram.ext import run_async 3 | 4 | from tg_bot.modules.disable import DisableAbleCommandHandler 5 | from tg_bot import dispatcher, CallbackContext 6 | 7 | 8 | def shout(update: Update, context: CallbackContext): 9 | args = context.args 10 | msg = "```" 11 | text = " ".join(args) 12 | result = [] 13 | result.append(' '.join([s for s in text])) 14 | for pos, symbol in enumerate(text[1:]): 15 | result.append(symbol + ' ' + ' ' * pos + symbol) 16 | result = list("\n".join(result)) 17 | result[0] = text[0] 18 | result = "".join(result) 19 | msg = "```\n" + result + "```" 20 | return update.effective_message.reply_text(msg, parse_mode="MARKDOWN") 21 | 22 | 23 | __help__ = """ 24 | A little piece of fun wording! Give a loud shout out in the chatroom. 25 | 26 | i.e /shout HELP, bot replies with huge coded HELP letters within the square. 27 | 28 | - /shout : write anything you want to give loud shout. 29 | ``` 30 | t e s t 31 | e e 32 | s s 33 | t t 34 | ``` 35 | """ 36 | 37 | __mod_name__ = "Shout" 38 | 39 | SHOUT_HANDLER = DisableAbleCommandHandler("shout", shout, run_async=True) 40 | 41 | dispatcher.add_handler(SHOUT_HANDLER) 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/ud.py: -------------------------------------------------------------------------------- 1 | from telegram import Update, Bot 2 | from telegram.ext import run_async 3 | 4 | from tg_bot.modules.disable import DisableAbleCommandHandler 5 | from tg_bot import dispatcher, CallbackContext 6 | 7 | from requests import get 8 | 9 | 10 | def ud(update: Update, context: CallbackContext): 11 | bot = context.bot 12 | try: 13 | message = update.effective_message 14 | text = message.text[len('/ud '):] 15 | results = get( 16 | f'http://api.urbandictionary.com/v0/define?term={text}').json() 17 | reply_text = f'Word: {text}\nDefinition: {results["list"][0]["definition"]}' 18 | except IndexError: 19 | reply_text = f'Word: {text}\nDefinition: 404 definition not found' 20 | return message.reply_text(reply_text) 21 | 22 | 23 | __help__ = """ 24 | Type the word or expression you want to search use in Urban dictionary. 25 | 26 | Usage: 27 | - /ud 28 | 29 | i.e. `/ud telegram` 30 | Word: Telegram 31 | Definition: A once-popular system of telecommunications, in which the sender would contact the telegram service and speak their [message] over the [phone]. The person taking the message would then send it, via a teletype machine, to a telegram office near the receiver's [address]. The message would then be hand-delivered to the addressee. From 1851 until it discontinued the service in 2006, Western Union was the best-known telegram service in the world. 32 | 33 | """ 34 | 35 | __mod_name__ = "Urban dictionary" 36 | 37 | ud_handle = DisableAbleCommandHandler("ud", ud, run_async=True) 38 | 39 | dispatcher.add_handler(ud_handle) 40 | -------------------------------------------------------------------------------- /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/sql/antiarabic_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 AntiArabicChatSettings(BASE): 10 | __tablename__ = "chat_antiarabic_settings" 11 | chat_id = Column(String(14), primary_key=True) 12 | antiarabic = Column(Boolean, default=True) 13 | 14 | def __init__(self, chat_id): 15 | self.chat_id = str(chat_id) 16 | 17 | def __repr__(self): 18 | return "".format(self.chat_id) 19 | 20 | 21 | AntiArabicChatSettings.__table__.create(checkfirst=True) 22 | 23 | CHAT_LOCK = threading.RLock() 24 | 25 | 26 | def chat_antiarabic(chat_id: Union[str, int]) -> bool: 27 | try: 28 | chat_setting = SESSION.query(AntiArabicChatSettings).get(str(chat_id)) 29 | if chat_setting: 30 | return chat_setting.antiarabic 31 | return False 32 | finally: 33 | SESSION.close() 34 | 35 | 36 | def set_chat_setting(chat_id: Union[int, str], setting: bool): 37 | with CHAT_LOCK: 38 | chat_setting = SESSION.query(AntiArabicChatSettings).get(str(chat_id)) 39 | if not chat_setting: 40 | chat_setting = AntiArabicChatSettings(chat_id) 41 | 42 | chat_setting.antiarabic = setting 43 | SESSION.add(chat_setting) 44 | SESSION.commit() 45 | 46 | 47 | def migrate_chat(old_chat_id, new_chat_id): 48 | with CHAT_LOCK: 49 | chat_notes = SESSION.query(AntiArabicChatSettings).filter( 50 | AntiArabicChatSettings.chat_id == str(old_chat_id)).all() 51 | for note in chat_notes: 52 | note.chat_id = str(new_chat_id) 53 | SESSION.commit() 54 | -------------------------------------------------------------------------------- /tg_bot/modules/getlink.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from telegram import Update, Bot, Chat, Message, User 4 | from telegram.error import BadRequest 5 | from telegram.ext import CommandHandler, Filters 6 | from telegram.ext.dispatcher import run_async 7 | from tg_bot.modules.helper_funcs.chat_status import bot_admin 8 | from tg_bot.modules.helper_funcs.filters import CustomFilters 9 | 10 | from tg_bot import dispatcher, CallbackContext 11 | import random, re 12 | 13 | 14 | @bot_admin 15 | def getlink(update: Update, context: CallbackContext): 16 | bot, args = context.bot, context.args 17 | message = update.effective_message 18 | if args: 19 | pattern = re.compile(r'-\d+') 20 | else: 21 | message.reply_text("You don't seem to be referring to any chats.") 22 | links = "Invite link(s):\n" 23 | for chat_id in pattern.findall(message.text): 24 | try: 25 | chat = bot.getChat(chat_id) 26 | bot_member = chat.get_member(bot.id) 27 | if bot_member.can_invite_users: 28 | invitelink = bot.exportChatInviteLink(chat_id) 29 | links += str(chat_id) + ":\n" + invitelink + "\n" 30 | else: 31 | links += str( 32 | chat_id 33 | ) + ":\nI don't have access to the invite link." + "\n" 34 | except BadRequest as excp: 35 | links += str(chat_id) + ":\n" + excp.message + "\n" 36 | except TelegramError as excp: 37 | links += str(chat_id) + ":\n" + excp.message + "\n" 38 | 39 | message.reply_text(links) 40 | 41 | 42 | GETLINK_HANDLER = CommandHandler("getlink", 43 | getlink, 44 | run_async=True, 45 | filters=CustomFilters.sudo_filter) 46 | 47 | dispatcher.add_handler(GETLINK_HANDLER) 48 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/github_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct, Integer 4 | 5 | from tg_bot.modules.helper_funcs.msg_types import Types 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class GitHub(BASE): 10 | __tablename__ = "github" 11 | chat_id = Column( 12 | String(14), primary_key=True 13 | ) #string because int is too large to be stored in a PSQL database. 14 | name = Column(UnicodeText, primary_key=True) 15 | value = Column(UnicodeText, nullable=False) 16 | backoffset = Column(Integer, nullable=False, default=0) 17 | 18 | def __init__(self, chat_id, name, value, backoffset): 19 | self.chat_id = str(chat_id) 20 | self.name = name 21 | self.value = value 22 | self.backoffset = backoffset 23 | 24 | def __repr__(self): 25 | return "" % self.name 26 | 27 | 28 | GitHub.__table__.create(checkfirst=True) 29 | 30 | GIT_LOCK = threading.RLock() 31 | 32 | 33 | def add_repo_to_db(chat_id, name, value, backoffset): 34 | with GIT_LOCK: 35 | prev = SESSION.query(GitHub).get((str(chat_id), name)) 36 | if prev: 37 | SESSION.delete(prev) 38 | repo = GitHub(str(chat_id), name, value, backoffset) 39 | SESSION.add(repo) 40 | SESSION.commit() 41 | 42 | 43 | def get_repo(chat_id, name): 44 | try: 45 | return SESSION.query(GitHub).get((str(chat_id), name)) 46 | finally: 47 | SESSION.close() 48 | 49 | 50 | def rm_repo(chat_id, name): 51 | with GIT_LOCK: 52 | repo = SESSION.query(GitHub).get((str(chat_id), name)) 53 | if repo: 54 | SESSION.delete(repo) 55 | SESSION.commit() 56 | return True 57 | else: 58 | SESSION.close() 59 | return False 60 | 61 | 62 | def get_all_repos(chat_id): 63 | try: 64 | return SESSION.query(GitHub).filter( 65 | GitHub.chat_id == str(chat_id)).order_by(GitHub.name.asc()).all() 66 | finally: 67 | SESSION.close() 68 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/git_api.py: -------------------------------------------------------------------------------- 1 | import urllib.request as url 2 | import json 3 | import datetime 4 | 5 | VERSION = "1.0.2" 6 | APIURL = "http://api.github.com/repos/" 7 | 8 | 9 | def vercheck() -> str: 10 | return str(VERSION) 11 | 12 | 13 | #Repo-wise stuff 14 | 15 | 16 | def getData(repoURL): 17 | try: 18 | with url.urlopen(APIURL + repoURL + "/releases") as data_raw: 19 | repoData = json.loads(data_raw.read().decode()) 20 | return repoData 21 | except: 22 | return None 23 | 24 | 25 | def getReleaseData(repoData, index): 26 | if index < len(repoData): 27 | return repoData[index] 28 | else: 29 | return None 30 | 31 | 32 | #Release-wise stuff 33 | 34 | 35 | def getAuthor(releaseData): 36 | if releaseData is None: 37 | return None 38 | return releaseData['author']['login'] 39 | 40 | 41 | def getAuthorUrl(releaseData): 42 | if releaseData is None: 43 | return None 44 | return releaseData['author']['html_url'] 45 | 46 | 47 | def getReleaseName(releaseData): 48 | if releaseData is None: 49 | return None 50 | return releaseData['name'] 51 | 52 | 53 | def getReleaseDate(releaseData): 54 | if releaseData is None: 55 | return None 56 | return releaseData['published_at'] 57 | 58 | 59 | def getAssetsSize(releaseData): 60 | if releaseData is None: 61 | return None 62 | return len(releaseData['assets']) 63 | 64 | 65 | def getAssets(releaseData): 66 | if releaseData is None: 67 | return None 68 | return releaseData['assets'] 69 | 70 | 71 | def getBody(releaseData): #changelog stuff 72 | if releaseData is None: 73 | return None 74 | return releaseData['body'] 75 | 76 | 77 | #Asset-wise stuff 78 | 79 | 80 | def getReleaseFileName(asset): 81 | return asset['name'] 82 | 83 | 84 | def getReleaseFileURL(asset): 85 | return asset['browser_download_url'] 86 | 87 | 88 | def getDownloadCount(asset): 89 | return asset['download_count'] 90 | 91 | 92 | def getSize(asset): 93 | return asset['size'] 94 | -------------------------------------------------------------------------------- /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 = { 75 | user.user_id: user.reason 76 | for user in all_afk if user.is_afk 77 | } 78 | finally: 79 | SESSION.close() 80 | 81 | 82 | __load_afk_users() 83 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/global_kicks_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, BigInteger, Integer, String, Boolean 4 | 5 | from tg_bot.modules.sql import BASE, SESSION 6 | 7 | 8 | class GloballyKickedUsers(BASE): 9 | __tablename__ = "gkicks" 10 | user_id = Column(BigInteger, primary_key=True) 11 | name = Column(UnicodeText, nullable=False) 12 | times = Column(Integer) 13 | 14 | def __init__(self, user_id, name, times): 15 | self.user_id = user_id 16 | self.name = name 17 | self.times = times 18 | 19 | def to_dict(self): 20 | return { 21 | "user_id": self.user_id, 22 | "name": self.name, 23 | "times": self.times 24 | } 25 | 26 | 27 | GloballyKickedUsers.__table__.create(checkfirst=True) 28 | 29 | 30 | def gkick_user(user_id, name, increment): 31 | user = SESSION.query(GloballyKickedUsers).get(user_id) 32 | if not user: 33 | user = GloballyKickedUsers(user_id, name, 0) 34 | user.name = name 35 | user.times += increment 36 | 37 | SESSION.merge(user) 38 | SESSION.commit() 39 | __load_gkick_userid_list() 40 | 41 | 42 | def gkick_setvalue(user_id, name, value): 43 | user = SESSION.query(GloballyKickedUsers).get(user_id) 44 | if user: 45 | user.times = value 46 | if not user: 47 | user = GloballyKickedUsers(user_id, name, value) 48 | SESSION.merge(user) 49 | SESSION.commit() 50 | __load_gkick_userid_list() 51 | 52 | 53 | def gkick_reset(user_id): 54 | user = SESSION.query(GloballyKickedUsers).get(user_id) 55 | if user: 56 | user.times = 0 57 | SESSION.delete(user) 58 | SESSION.commit() 59 | __load_gkick_userid_list() 60 | 61 | 62 | def get_times(user_id): 63 | user = SESSION.query(GloballyKickedUsers).get(user_id) 64 | if not user: 65 | return 0 66 | return user.times 67 | 68 | 69 | def __load_gkick_userid_list(): 70 | global GKICK_LIST 71 | try: 72 | GKICK_LIST = { 73 | x.user_id 74 | for x in SESSION.query(GloballyKickedUsers).all() 75 | } 76 | finally: 77 | SESSION.close() 78 | -------------------------------------------------------------------------------- /tg_bot/modules/stickers.py: -------------------------------------------------------------------------------- 1 | import os 2 | from telegram import Message, Chat, Update, Bot 3 | from telegram import ParseMode 4 | from telegram.ext import CommandHandler, run_async 5 | from telegram.utils.helpers import escape_markdown 6 | 7 | from tg_bot import dispatcher, CallbackContext 8 | from tg_bot.modules.disable import DisableAbleCommandHandler 9 | from tg_bot.modules.helper_funcs.filters import CustomFilters 10 | 11 | 12 | def stickerid(update: Update, context: CallbackContext): 13 | bot = context.bot 14 | msg = update.effective_message 15 | if msg.reply_to_message and msg.reply_to_message.sticker: 16 | update.effective_message.reply_text( 17 | "Sticker ID:\n```" + msg.reply_to_message.sticker.file_id + "```", 18 | parse_mode=ParseMode.MARKDOWN) 19 | else: 20 | update.effective_message.reply_text( 21 | "Please reply to a sticker to get its ID.") 22 | 23 | 24 | def getsticker(update: Update, context: CallbackContext): 25 | bot = context.bot 26 | msg = update.effective_message 27 | chat_id = update.effective_chat.id 28 | if msg.reply_to_message and msg.reply_to_message.sticker: 29 | file_id = msg.reply_to_message.sticker.file_id 30 | newFile = bot.get_file(file_id) 31 | newFile.download('sticker.png') 32 | bot.sendDocument(chat_id, document=open('sticker.png', 'rb')) 33 | os.remove("sticker.png") 34 | 35 | else: 36 | update.effective_message.reply_text( 37 | "Please reply to a sticker for me to upload its PNG.") 38 | 39 | 40 | __help__ = """ 41 | Fetching ID of stickers is made easy! With this stickers command you simply can \ 42 | fetch ID of sticker. 43 | 44 | - /stickerid: reply to a sticker to me to tell you its file ID. 45 | """ 46 | 47 | __mod_name__ = "Stickers" 48 | 49 | STICKERID_HANDLER = DisableAbleCommandHandler("stickerid", 50 | stickerid, 51 | run_async=True) 52 | GETSTICKER_HANDLER = DisableAbleCommandHandler( 53 | "getsticker", 54 | getsticker, 55 | filters=CustomFilters.sudo_filter, 56 | run_async=True) 57 | 58 | dispatcher.add_handler(STICKERID_HANDLER) 59 | dispatcher.add_handler(GETSTICKER_HANDLER) 60 | -------------------------------------------------------------------------------- /tg_bot/sample_config.py: -------------------------------------------------------------------------------- 1 | if not __name__.endswith("sample_config"): 2 | import sys 3 | print( 4 | "The README is there to be read. Extend this sample config to a config file, don't just rename and change " 5 | "values here. Doing that WILL backfire on you.\nBot quitting.", 6 | file=sys.stderr) 7 | quit(1) 8 | 9 | 10 | # Create a new config.py file in same dir and import, then extend this class. 11 | class Config(object): 12 | LOGGER = True 13 | 14 | # REQUIRED 15 | API_KEY = "YOUR KEY HERE" 16 | OWNER_ID = "YOUR ID HERE" # If you dont know, run the bot and do /id in your private chat with it 17 | OWNER_USERNAME = "YOUR USERNAME HERE" 18 | 19 | # RECOMMENDED 20 | SQLALCHEMY_DATABASE_URI = 'sqldbtype://username:pw@hostname:port/db_name' # needed for any database modules 21 | MESSAGE_DUMP = None # needed to make sure 'save from' messages persist 22 | LOAD = [] 23 | # sed has been disabled after the discovery that certain long-running sed commands maxed out cpu usage 24 | # and killed the bot. Be careful re-enabling it! 25 | NO_LOAD = ['translation', 'rss', 'weather', 'sed'] 26 | WEBHOOK = False 27 | URL = None 28 | 29 | # OPTIONAL 30 | SUDO_USERS = [ 31 | ] # List of id's (not usernames) for users which have sudo access to the bot. 32 | SUPPORT_USERS = [ 33 | ] # List of id's (not usernames) for users which are allowed to gban, but can also be banned. 34 | WHITELIST_USERS = [ 35 | ] # List of id's (not usernames) for users which WONT be banned/kicked by the bot. 36 | DONATION_LINK = None # EG, paypal 37 | CERT_PATH = None 38 | PORT = 5000 39 | DEL_CMDS = False # Whether or not you should delete "blue text must click" commands 40 | STRICT_GBAN = False 41 | STRICT_GMUTE = False 42 | WORKERS = 8 # Number of subthreads to use. This is the recommended amount - see for yourself what works best! 43 | BAN_STICKER = 'CAADAgADOwADPPEcAXkko5EB3YGYAg' # ban sticker 44 | START_STICKER = False #add a START_STICKER_ID = 'stickerid' in your config.py if you use this as true 45 | START_STICKER_ID = 'CAADAgAD0QMAAjq5FQKizo2AiTQCBQI' #putin hand sticker 46 | ALLOW_EXCL = False # Allow ! commands as well as / 47 | API_OPENWEATHER = None # OpenWeather API 48 | 49 | 50 | class Production(Config): 51 | LOGGER = False 52 | 53 | 54 | class Development(Config): 55 | LOGGER = True 56 | -------------------------------------------------------------------------------- /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( 22 | self.chat_id, self.feed_link, self.old_entry_link) 23 | 24 | 25 | RSS.__table__.create(checkfirst=True) 26 | INSERTION_LOCK = threading.RLock() 27 | 28 | 29 | def check_url_availability(tg_chat_id, tg_feed_link): 30 | try: 31 | return SESSION.query(RSS).filter(RSS.feed_link == tg_feed_link, 32 | RSS.chat_id == tg_chat_id).all() 33 | finally: 34 | SESSION.close() 35 | 36 | 37 | def add_url(tg_chat_id, tg_feed_link, tg_old_entry_link): 38 | with INSERTION_LOCK: 39 | action = RSS(tg_chat_id, tg_feed_link, tg_old_entry_link) 40 | 41 | SESSION.add(action) 42 | SESSION.commit() 43 | 44 | 45 | def remove_url(tg_chat_id, tg_feed_link): 46 | with INSERTION_LOCK: 47 | # this loops to delete any possible duplicates for the same TG User ID, TG Chat ID and link 48 | for row in check_url_availability(tg_chat_id, tg_feed_link): 49 | # add the action to the DB query 50 | SESSION.delete(row) 51 | 52 | SESSION.commit() 53 | 54 | 55 | def get_urls(tg_chat_id): 56 | try: 57 | return SESSION.query(RSS).filter(RSS.chat_id == tg_chat_id).all() 58 | finally: 59 | SESSION.close() 60 | 61 | 62 | def get_all(): 63 | try: 64 | return SESSION.query(RSS).all() 65 | finally: 66 | SESSION.close() 67 | 68 | 69 | def update_url(row_id, new_entry_links): 70 | with INSERTION_LOCK: 71 | row = SESSION.query(RSS).get(row_id) 72 | 73 | # set the new old_entry_link with the latest update from the RSS Feed 74 | row.old_entry_link = new_entry_links[0] 75 | 76 | # commit the changes to the DB 77 | SESSION.commit() 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/miui.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from requests import get 3 | from yaml import load, Loader 4 | from telegram import Update, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton 5 | 6 | from tg_bot import dispatcher, updater, CallbackContext 7 | from tg_bot.modules.disable import DisableAbleCommandHandler 8 | 9 | URL = "https://raw.githubusercontent.com/XiaomiFirmwareUpdater/miui-updates-tracker/master/data/latest.yml" 10 | 11 | 12 | def delete(msg, delmsg, timer): 13 | sleep(timer) 14 | try: 15 | msg.delete() 16 | delmsg.delete() 17 | except: 18 | return 19 | 20 | 21 | def miui(update: Update, context: CallbackContext): 22 | args = context.args 23 | msg = update.effective_message 24 | 25 | codename = args[0] if len(args) > 0 else False 26 | 27 | if not codename: 28 | delmsg = msg.reply_text("Provide a codename bruh!") 29 | delete(msg, delmsg, 5) 30 | return 31 | 32 | yaml_data = load(get(URL).content, Loader=Loader) 33 | data = [i for i in yaml_data if codename in i['codename']] 34 | 35 | if len(data) < 1: 36 | delmsg = msg.reply_text("Provide a valid codename bruh!") 37 | delete(msg, delmsg, 5) 38 | return 39 | 40 | markup = [] 41 | for fw in data: 42 | av = fw['android'] 43 | branch = fw['branch'] 44 | method = fw['method'] 45 | link = fw['link'] 46 | fname = fw['name'] 47 | version = fw['version'] 48 | 49 | btn = fname + ' | ' + branch + ' | ' + method + ' | ' + version 50 | markup.append([InlineKeyboardButton(text=btn, url=link)]) 51 | 52 | device = fname.split(" ") 53 | device.pop() 54 | device = " ".join(device) 55 | delmsg = msg.reply_text(f"The latest firmwares for *{device}* are:", 56 | reply_markup=InlineKeyboardMarkup(markup), 57 | parse_mode=ParseMode.MARKDOWN) 58 | delete(msg, delmsg, 60) 59 | 60 | 61 | __help__ = """ 62 | *MiUI related commands:* 63 | 64 | - /miui codename - fetches latest firmware info for 65 | 66 | *Examples:* 67 | /miui lmi 68 | 69 | *Note:* The messages are auto deleted to prevent group flooding. Incorrect codenames are deleted after 5 seconds and correct codenames are deleted after 60 seconds. 70 | """ 71 | 72 | __mod_name__ = "MiUI" 73 | 74 | MIUI_HANDLER = DisableAbleCommandHandler("miui", miui, run_async=True) 75 | 76 | dispatcher.add_handler(MIUI_HANDLER) 77 | -------------------------------------------------------------------------------- /tg_bot/modules/helper_funcs/filters.py: -------------------------------------------------------------------------------- 1 | from telegram import Message 2 | from telegram.ext import MessageFilter 3 | from emoji import UNICODE_EMOJI 4 | 5 | from tg_bot import SUPPORT_USERS, SUDO_USERS 6 | 7 | 8 | class CustomFilters(object): 9 | class _Supporters(MessageFilter): 10 | def filter(self, message: Message): 11 | return bool(message.from_user 12 | and message.from_user.id in SUPPORT_USERS) 13 | 14 | support_filter = _Supporters() 15 | 16 | class _Sudoers(MessageFilter): 17 | def filter(self, message: Message): 18 | return bool(message.from_user 19 | and message.from_user.id in SUDO_USERS) 20 | 21 | sudo_filter = _Sudoers() 22 | 23 | class _MimeType(MessageFilter): 24 | def __init__(self, mimetype): 25 | self.mime_type = mimetype 26 | self.name = "CustomFilters.mime_type({})".format(self.mime_type) 27 | 28 | def filter(self, message: Message): 29 | return bool(message.document 30 | and message.document.mime_type == self.mime_type) 31 | 32 | mime_type = _MimeType 33 | 34 | class _HasText(MessageFilter): 35 | def filter(self, message: Message): 36 | return bool(message.text or message.sticker or message.photo 37 | or message.document or message.video) 38 | 39 | has_text = _HasText() 40 | 41 | class _HasEmoji(MessageFilter): 42 | def filter(self, message: Message): 43 | text = "" 44 | if (message.text): 45 | text = message.text 46 | for emoji in UNICODE_EMOJI: 47 | for letter in text: 48 | if (letter == emoji): 49 | return True 50 | return False 51 | 52 | has_emoji = _HasEmoji() 53 | 54 | class _IsEmoji(MessageFilter): 55 | def filter(self, message: Message): 56 | if (message.text and len(message.text) == 1): 57 | for emoji in UNICODE_EMOJI: 58 | for letter in message.text: 59 | if (letter == emoji): 60 | return True 61 | return False 62 | 63 | is_emoji = _IsEmoji() 64 | 65 | class _IsAnonChannel(MessageFilter): 66 | def filter(self, message: Message): 67 | if (message.from_user and message.from_user.id == 136817688 ): 68 | return True 69 | return False 70 | 71 | is_anon_channel = _IsAnonChannel() 72 | -------------------------------------------------------------------------------- /tg_bot/modules/systools.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | 4 | import tg_bot.modules.helper_funcs.cas_api as cas 5 | import tg_bot.modules.helper_funcs.git_api as git 6 | 7 | from platform import python_version 8 | from telegram import Update, Bot, Message, Chat, ParseMode 9 | from telegram.ext import CommandHandler, run_async, Filters 10 | 11 | from tg_bot import dispatcher, CallbackContext, OWNER_ID, SUDO_USERS, SUPPORT_USERS 12 | from tg_bot.modules.helper_funcs.filters import CustomFilters 13 | from tg_bot.modules.disable import DisableAbleCommandHandler 14 | 15 | 16 | def pingme(): 17 | out = "" 18 | under = False 19 | if os.name == 'nt': 20 | output = subprocess.check_output("ping -n 1 1.0.0.1 | findstr time*", 21 | shell=True).decode() 22 | outS = output.splitlines() 23 | out = outS[0] 24 | else: 25 | out = subprocess.check_output("ping -c 1 1.0.0.1 | grep time=", 26 | shell=True).decode() 27 | splitOut = out.split(' ') 28 | stringtocut = "" 29 | for line in splitOut: 30 | if (line.startswith('time=') or line.startswith('time<')): 31 | stringtocut = line 32 | break 33 | newstra = stringtocut.split('=') 34 | if len(newstra) == 1: 35 | under = True 36 | newstra = stringtocut.split('<') 37 | newstr = "" 38 | if os.name == 'nt': 39 | newstr = newstra[1].split('ms') 40 | else: 41 | newstr = newstra[1].split( 42 | ' ') #redundant split, but to try and not break windows ping 43 | ping_time = float(newstr[0]) 44 | return ping_time 45 | 46 | 47 | def status(update: Update, context: CallbackContext): 48 | user_id = update.effective_user.id 49 | reply = "*System Status:* `operational`\n\n" 50 | reply += "*Python version:* `" + python_version() + "`\n" 51 | #if user_id in SUDO_USERS or user_id in SUPPORT_USERS or user_id == OWNER_ID: removed ping to make heroku more compatible 52 | #pingSpeed = pingme() 53 | #reply += "*Ping speed:* `"+str(pingSpeed)+"ms`\n" 54 | reply += "*python-telegram-bot:* `" + str( 55 | subprocess.check_output( 56 | "pip show python-telegram-bot | grep Version\:", 57 | shell=True).decode()).split()[1] + "`\n" 58 | reply += "*CAS API version:* `" + str(cas.vercheck()) + "`\n" 59 | reply += "*GitHub API version:* `" + str(git.vercheck()) + "`\n" 60 | update.effective_message.reply_text(reply, parse_mode=ParseMode.MARKDOWN) 61 | 62 | 63 | STATUS_HANDLER = CommandHandler("status", status, run_async=True) 64 | 65 | dispatcher.add_handler(STATUS_HANDLER) 66 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/userinfo_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, BigInteger, 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(BigInteger, 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(BigInteger, 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/reporting_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from sqlalchemy import Column, BigInteger, 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(BigInteger, 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/leave.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from telegram import TelegramError, Chat, Message 4 | from telegram import Update, Bot 5 | from telegram.error import BadRequest 6 | from telegram.ext import MessageHandler, Filters, CommandHandler 7 | from telegram.ext.dispatcher import run_async 8 | from typing import List 9 | from tg_bot.modules.helper_funcs.filters import CustomFilters 10 | from tg_bot.modules.sql.users_sql import get_all_chats 11 | 12 | import telegram 13 | from tg_bot import dispatcher, CallbackContext, OWNER_ID 14 | 15 | MESSAGE_1 = "And when the lamb broke the seventh seal, there was silence in heaven." 16 | MESSAGE_2 = "I saw the seven angels who stood before God, and to them were given seven trumpets." 17 | MESSAGE_3 = "And another angel came and stood at the altar, having a golden censer to which was given too much insence." 18 | MESSAGE_4 = "And the smoke of the incense, which came with the prayers of the saints, ascended up before God." 19 | MESSAGE_5 = "And the angel took the censer, and filled it with fire of the altar, and casted it into the earth, and there were voices, and thunderings, and lightnings, and an earthquake." 20 | MESSAGE_6 = "The seven angels which had the seven trumpets prepared themselves to sound." 21 | MESSAGE_7 = "And I heard a great voice out of the temple saying to the seven angels, Go your ways, and pour out the vials of the wrath of God upon the earth." 22 | 23 | 24 | def leave(update: Update, context: CallbackContext): 25 | bot, args = context.bot, context.args 26 | if args: 27 | chat_id = str(args[0]) 28 | del args[0] 29 | try: 30 | bot.leave_chat(int(chat_id)) 31 | update.effective_message.reply_text("Left the group successfully!") 32 | except telegram.TelegramError: 33 | update.effective_message.reply_text("Attempt failed.") 34 | else: 35 | update.effective_message.reply_text("Give me a valid chat id") 36 | 37 | 38 | """ Don't want anyone to accidentally trigger this lmao, so it's commented 39 | def selfDestroy(bot: Bot, update: Update): 40 | chats = get_all_chats() 41 | for chat in chats: 42 | try: 43 | bot.sendMessage(MESSAGE_1, int(chat.chat_id)) 44 | bot.sendMessage(MESSAGE_2, int(chat.chat_id)) 45 | bot.sendMessage(MESSAGE_3, int(chat.chat_id)) 46 | bot.sendMessage(MESSAGE_4, int(chat.chat_id)) 47 | bot.sendMessage(MESSAGE_5, int(chat.chat_id)) 48 | bot.sendMessage(MESSAGE_6, int(chat.chat_id)) 49 | bot.sendMessage(MESSAGE_7, int(chat.chat_id)) 50 | sleep(0.1) 51 | bot.leave_chat(int(chat.chat_id)) 52 | except: 53 | pass 54 | """ 55 | 56 | __help__ = "" 57 | 58 | __mod_name__ = "Leave" 59 | 60 | LEAVE_HANDLER = CommandHandler("leave", 61 | leave, 62 | run_async=True, 63 | filters=Filters.user(OWNER_ID)) 64 | dispatcher.add_handler(LEAVE_HANDLER) 65 | -------------------------------------------------------------------------------- /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( 84 | Disable.chat_id == str(old_chat_id)).all() 85 | for chat in chats: 86 | chat.chat_id = str(new_chat_id) 87 | SESSION.add(chat) 88 | 89 | if str(old_chat_id) in DISABLED: 90 | DISABLED[str(new_chat_id)] = DISABLED.get(str(old_chat_id), set()) 91 | 92 | SESSION.commit() 93 | 94 | 95 | def __load_disabled_commands(): 96 | global DISABLED 97 | try: 98 | all_chats = SESSION.query(Disable).all() 99 | for chat in all_chats: 100 | DISABLED.setdefault(chat.chat_id, set()).add(chat.command) 101 | 102 | finally: 103 | SESSION.close() 104 | 105 | 106 | __load_disabled_commands() 107 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/antiflood_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, BigInteger, Integer, String, Boolean 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(BigInteger) 16 | count = Column(Integer, default=DEF_COUNT) 17 | limit = Column(Integer, default=DEF_LIMIT) 18 | soft_flood = Column(Boolean, default=False) 19 | 20 | def __init__(self, chat_id, soft_flood=False): 21 | self.chat_id = str(chat_id) # ensure string 22 | self.soft_flood = soft_flood 23 | 24 | def __repr__(self): 25 | return "" % self.chat_id 26 | 27 | 28 | FloodControl.__table__.create(checkfirst=True) 29 | 30 | INSERTION_LOCK = threading.RLock() 31 | 32 | CHAT_FLOOD = {} 33 | 34 | 35 | def set_flood(chat_id, amount): 36 | with INSERTION_LOCK: 37 | flood = SESSION.query(FloodControl).get(str(chat_id)) 38 | if not flood: 39 | flood = FloodControl(str(chat_id)) 40 | 41 | flood.user_id = None 42 | flood.limit = amount 43 | 44 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, amount) 45 | 46 | SESSION.add(flood) 47 | SESSION.commit() 48 | 49 | 50 | def set_flood_strength(chat_id, soft_flood): 51 | with INSERTION_LOCK: 52 | flood = SESSION.query(FloodControl).get(str(chat_id)) 53 | if not flood: 54 | flood = FloodControl(chat_id, soft_flood=soft_flood) 55 | 56 | flood.soft_flood = soft_flood 57 | 58 | SESSION.add(flood) 59 | SESSION.commit() 60 | 61 | 62 | def update_flood(chat_id: str, user_id) -> bool: 63 | if str(chat_id) in CHAT_FLOOD: 64 | curr_user_id, count, limit = CHAT_FLOOD.get(str(chat_id), DEF_OBJ) 65 | 66 | if limit == 0: # no antiflood 67 | return False 68 | 69 | if user_id != curr_user_id or user_id is None: # other user 70 | CHAT_FLOOD[str(chat_id)] = (user_id, DEF_COUNT + 1, limit) 71 | return False 72 | 73 | count += 1 74 | if count > limit: # too many msgs, kick 75 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, limit) 76 | return True 77 | 78 | # default -> update 79 | CHAT_FLOOD[str(chat_id)] = (user_id, count, limit) 80 | return False 81 | 82 | 83 | def get_flood_limit(chat_id): 84 | return CHAT_FLOOD.get(str(chat_id), DEF_OBJ)[2] 85 | 86 | 87 | def get_flood_strength(chat_id): 88 | try: 89 | soft_flood = SESSION.query(FloodControl).get(str(chat_id)) 90 | if soft_flood: 91 | return soft_flood.soft_flood 92 | else: 93 | return 3, False 94 | 95 | finally: 96 | SESSION.close() 97 | 98 | 99 | def migrate_chat(old_chat_id, new_chat_id): 100 | with INSERTION_LOCK: 101 | flood = SESSION.query(FloodControl).get(str(old_chat_id)) 102 | if flood: 103 | CHAT_FLOOD[str(new_chat_id)] = CHAT_FLOOD.get( 104 | str(old_chat_id), DEF_OBJ) 105 | flood.chat_id = str(new_chat_id) 106 | SESSION.commit() 107 | 108 | SESSION.close() 109 | 110 | 111 | def __load_flood_settings(): 112 | global CHAT_FLOOD 113 | try: 114 | all_chats = SESSION.query(FloodControl).all() 115 | CHAT_FLOOD = { 116 | chat.chat_id: (None, DEF_COUNT, chat.limit) 117 | for chat in all_chats 118 | } 119 | finally: 120 | SESSION.close() 121 | 122 | 123 | __load_flood_settings() 124 | -------------------------------------------------------------------------------- /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( 22 | isinstance(other, BlackListFilters) 23 | and self.chat_id == other.chat_id 24 | and self.trigger == other.trigger) 25 | 26 | 27 | BlackListFilters.__table__.create(checkfirst=True) 28 | 29 | BLACKLIST_FILTER_INSERTION_LOCK = threading.RLock() 30 | 31 | CHAT_BLACKLISTS = {} 32 | 33 | 34 | def add_to_blacklist(chat_id, trigger): 35 | with BLACKLIST_FILTER_INSERTION_LOCK: 36 | blacklist_filt = BlackListFilters(str(chat_id), trigger) 37 | 38 | SESSION.merge(blacklist_filt) # merge to avoid duplicate key issues 39 | SESSION.commit() 40 | CHAT_BLACKLISTS.setdefault(str(chat_id), set()).add(trigger) 41 | 42 | 43 | def rm_from_blacklist(chat_id, trigger): 44 | with BLACKLIST_FILTER_INSERTION_LOCK: 45 | blacklist_filt = SESSION.query(BlackListFilters).get( 46 | (str(chat_id), trigger)) 47 | if blacklist_filt: 48 | if trigger in CHAT_BLACKLISTS.get(str(chat_id), 49 | set()): # sanity check 50 | CHAT_BLACKLISTS.get(str(chat_id), set()).remove(trigger) 51 | 52 | SESSION.delete(blacklist_filt) 53 | SESSION.commit() 54 | return True 55 | 56 | SESSION.close() 57 | return False 58 | 59 | 60 | def get_chat_blacklist(chat_id): 61 | return CHAT_BLACKLISTS.get(str(chat_id), set()) 62 | 63 | 64 | def num_blacklist_filters(): 65 | try: 66 | return SESSION.query(BlackListFilters).count() 67 | finally: 68 | SESSION.close() 69 | 70 | 71 | def num_blacklist_chat_filters(chat_id): 72 | try: 73 | return SESSION.query(BlackListFilters.chat_id).filter( 74 | BlackListFilters.chat_id == str(chat_id)).count() 75 | finally: 76 | SESSION.close() 77 | 78 | 79 | def num_blacklist_filter_chats(): 80 | try: 81 | return SESSION.query(func.count(distinct( 82 | BlackListFilters.chat_id))).scalar() 83 | finally: 84 | SESSION.close() 85 | 86 | 87 | def __load_chat_blacklists(): 88 | global CHAT_BLACKLISTS 89 | try: 90 | chats = SESSION.query(BlackListFilters.chat_id).distinct().all() 91 | for (chat_id, ) in chats: # remove tuple by ( ,) 92 | CHAT_BLACKLISTS[chat_id] = [] 93 | 94 | all_filters = SESSION.query(BlackListFilters).all() 95 | for x in all_filters: 96 | CHAT_BLACKLISTS[x.chat_id] += [x.trigger] 97 | 98 | CHAT_BLACKLISTS = {x: set(y) for x, y in CHAT_BLACKLISTS.items()} 99 | 100 | finally: 101 | SESSION.close() 102 | 103 | 104 | def migrate_chat(old_chat_id, new_chat_id): 105 | with BLACKLIST_FILTER_INSERTION_LOCK: 106 | chat_filters = SESSION.query(BlackListFilters).filter( 107 | BlackListFilters.chat_id == str(old_chat_id)).all() 108 | for filt in chat_filters: 109 | filt.chat_id = str(new_chat_id) 110 | SESSION.commit() 111 | 112 | 113 | __load_chat_blacklists() 114 | -------------------------------------------------------------------------------- /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_multiple_users(message: Message, args: List[str]): 26 | prev_message = message.reply_to_message 27 | split_text = message.text.split(None, 1) 28 | if len(split_text) < 2: 29 | return list(id_from_reply(message)) # only option possible 30 | retList = [] 31 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 32 | for ent in entities: 33 | retList.append(ent.user.id) 34 | for arg in args: 35 | if arg[0] == '@': 36 | user = arg 37 | user_id = get_user_id(user) 38 | retList.append(user_id) 39 | return retList 40 | 41 | 42 | def extract_user_and_text(message: Message, 43 | args: List[str]) -> (Optional[int], Optional[str]): 44 | prev_message = message.reply_to_message 45 | split_text = message.text.split(None, 1) 46 | 47 | if len(split_text) < 2: 48 | return id_from_reply(message) # only option possible 49 | 50 | text_to_parse = split_text[1] 51 | 52 | text = "" 53 | 54 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 55 | if len(entities) > 0: 56 | ent = entities[0] 57 | else: 58 | ent = None 59 | 60 | # if entity offset matches (command end/text start) then all good 61 | if entities and ent and ent.offset == len( 62 | message.text) - len(text_to_parse): 63 | ent = entities[0] 64 | user_id = ent.user.id 65 | text = message.text[ent.offset + ent.length:] 66 | 67 | elif len(args) >= 1 and args[0][0] == '@': 68 | user = args[0] 69 | user_id = get_user_id(user) 70 | if not user_id: 71 | message.reply_text( 72 | "I don't have that user in my db. You'll be able to interact with them if " 73 | "you reply to that person's message instead, or forward one of that user's messages." 74 | ) 75 | return None, None 76 | 77 | else: 78 | user_id = user_id 79 | res = message.text.split(None, 2) 80 | if len(res) >= 3: 81 | text = res[2] 82 | 83 | elif len(args) >= 1 and args[0].isdigit(): 84 | user_id = int(args[0]) 85 | res = message.text.split(None, 2) 86 | if len(res) >= 3: 87 | text = res[2] 88 | 89 | elif prev_message: 90 | user_id, text = id_from_reply(message) 91 | 92 | else: 93 | return None, None 94 | 95 | try: 96 | message.bot.get_chat(user_id) 97 | except BadRequest as excp: 98 | if excp.message in ("User_id_invalid", "Chat not found"): 99 | message.reply_text( 100 | "I don't seem to have interacted with this user before - please forward a message from " 101 | "them to give me control!") 102 | else: 103 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 104 | 105 | return None, None 106 | 107 | return user_id, text 108 | 109 | 110 | def extract_text(message) -> str: 111 | return message.text or message.caption or (message.sticker.emoji 112 | if message.sticker else None) 113 | -------------------------------------------------------------------------------- /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, 43 | module_dict: Dict, 44 | prefix, 45 | chat=None) -> List: 46 | if not chat: 47 | modules = sorted([ 48 | EqInlineKeyboardButton(x.__mod_name__, 49 | callback_data="{}_module({})".format( 50 | prefix, x.__mod_name__.lower())) 51 | for x in module_dict.values() 52 | ]) 53 | else: 54 | modules = sorted([ 55 | EqInlineKeyboardButton(x.__mod_name__, 56 | callback_data="{}_module({},{})".format( 57 | prefix, chat, x.__mod_name__.lower())) 58 | for x in module_dict.values() 59 | ]) 60 | 61 | pairs = list(zip(modules[::2], modules[1::2])) 62 | 63 | if len(modules) % 2 == 1: 64 | pairs.append((modules[-1], )) 65 | 66 | max_num_pages = ceil(len(pairs) / 7) 67 | modulo_page = page_n % max_num_pages 68 | 69 | # can only have a certain amount of buttons side by side 70 | if len(pairs) > 7: 71 | pairs = pairs[modulo_page * 7:7 * (modulo_page + 1)] + [ 72 | (EqInlineKeyboardButton( 73 | "<", callback_data="{}_prev({})".format(prefix, modulo_page)), 74 | EqInlineKeyboardButton( 75 | ">", callback_data="{}_next({})".format(prefix, modulo_page))) 76 | ] 77 | 78 | return pairs 79 | 80 | 81 | def send_to_list(bot: Bot, 82 | send_to: list, 83 | message: str, 84 | markdown=False, 85 | html=False) -> None: 86 | if html and markdown: 87 | raise Exception("Can only send with either markdown or HTML!") 88 | for user_id in set(send_to): 89 | try: 90 | if markdown: 91 | bot.send_message(user_id, 92 | message, 93 | parse_mode=ParseMode.MARKDOWN) 94 | elif html: 95 | bot.send_message(user_id, message, parse_mode=ParseMode.HTML) 96 | else: 97 | bot.send_message(user_id, message) 98 | except TelegramError: 99 | pass # ignore users who fail 100 | 101 | 102 | def build_keyboard(buttons): 103 | keyb = [] 104 | for btn in buttons: 105 | if btn.same_line and keyb: 106 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 107 | else: 108 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 109 | 110 | return keyb 111 | 112 | 113 | def revert_buttons(buttons): 114 | res = "" 115 | for btn in buttons: 116 | if btn.same_line: 117 | res += "\n[{}](buttonurl://{}:same)".format(btn.name, btn.url) 118 | else: 119 | res += "\n[{}](buttonurl://{})".format(btn.name, btn.url) 120 | 121 | return res 122 | 123 | 124 | def is_module_loaded(name): 125 | return (not LOAD or name in LOAD) and name not in NO_LOAD 126 | -------------------------------------------------------------------------------- /tg_bot/modules/antiarabic.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | from telegram import Message, Chat, Update, Bot, User, ParseMode 4 | from telegram.ext import CommandHandler, MessageHandler, run_async, Filters 5 | from telegram.utils.helpers import mention_html 6 | from tg_bot import dispatcher, CallbackContext, LOGGER, SUDO_USERS 7 | from tg_bot.modules.helper_funcs.chat_status import user_not_admin, user_admin, can_delete, is_user_admin, bot_admin 8 | from tg_bot.modules.log_channel import loggable 9 | from tg_bot.modules.helper_funcs.extraction import extract_text 10 | from tg_bot.modules.sql import antiarabic_sql as sql 11 | 12 | ANTIARABIC_GROUPS = 12 13 | 14 | 15 | @user_admin 16 | def antiarabic_setting(update: Update, context: CallbackContext): 17 | bot, args = context.bot, context.args 18 | chat = update.effective_chat # type: Optional[Chat] 19 | msg = update.effective_message # type: Optional[Message] 20 | user = update.effective_user 21 | member = chat.get_member(int(user.id)) 22 | 23 | if chat.type != chat.PRIVATE: 24 | if len(args) >= 1: 25 | if args[0] in ("yes", "on"): 26 | sql.set_chat_setting(chat.id, True) 27 | msg.reply_text( 28 | "Turned on AntiArabic! Messages sent by any non-admin which contains arabic text " 29 | "will be deleted.") 30 | 31 | elif args[0] in ("no", "off"): 32 | sql.set_chat_setting(chat.id, False) 33 | msg.reply_text( 34 | "Turned off AntiArabic! Messages containing arabic text won't be deleted." 35 | ) 36 | else: 37 | msg.reply_text("This chat's current setting is: `{}`".format( 38 | sql.chat_antiarabic(chat.id)), 39 | parse_mode=ParseMode.MARKDOWN) 40 | 41 | 42 | @user_not_admin 43 | def antiarabic(update: Update, context: CallbackContext): 44 | bot = context.bot 45 | chat = update.effective_chat # type: Optional[Chat] 46 | if can_delete(chat, bot.id): 47 | msg = update.effective_message # type: Optional[Message] 48 | to_match = extract_text(msg) 49 | user = update.effective_user # type: Optional[User] 50 | 51 | if not sql.chat_antiarabic(chat.id): 52 | return "" 53 | 54 | if not user.id or int(user.id) == 777000 or int(user.id) == 1087968824: 55 | return "" 56 | 57 | if not to_match: 58 | return 59 | 60 | if chat.type != chat.PRIVATE: 61 | for c in to_match: 62 | if ('\u0600' <= c <= '\u06FF' or '\u0750' <= c <= '\u077F' 63 | or '\u08A0' <= c <= '\u08FF' or '\uFB50' <= c <= '\uFDFF' 64 | or '\uFE70' <= c <= '\uFEFF' 65 | or '\U00010E60' <= c <= '\U00010E7F' 66 | or '\U0001EE00' <= c <= '\U0001EEFF'): 67 | update.effective_message.delete() 68 | return "" 69 | 70 | 71 | def __migrate__(old_chat_id, new_chat_id): 72 | sql.migrate_chat(old_chat_id, new_chat_id) 73 | 74 | 75 | def __chat_settings__(chat_id, user_id): 76 | return "This chat is setup to delete messages containing Arabic: `{}`".format( 77 | sql.chat_antiarabic(chat_id)) 78 | 79 | 80 | __mod_name__ = "AntiArabicScript" 81 | 82 | __help__ = """ 83 | AntiArabicScript module is used to delete messages containing characters from one of the following automatically: 84 | 85 | • Arabic 86 | • Arabic Supplement 87 | • Arabic Extended-A 88 | • Arabic Presentation Forms-A 89 | • Arabic Presentation Forms-B 90 | • Rumi Numeral Symbols 91 | • Arabic Mathematical Alphabetic Symbols 92 | 93 | *NOTE:* AntiArabicScript module doesn't affect messages sent by admins. 94 | 95 | *Admin only:* 96 | - /antiarabic : turn antiarabic module on/off ( off by default ) 97 | - /antiarabic: get status of AntiArabicScript module in chat 98 | """ 99 | 100 | SETTING_HANDLER = CommandHandler("antiarabic", 101 | antiarabic_setting, 102 | run_async=True) 103 | ANTI_ARABIC = MessageHandler( 104 | (Filters.text | Filters.command | Filters.sticker | Filters.photo) 105 | & Filters.chat_type.groups, 106 | antiarabic, 107 | run_async=True) 108 | 109 | dispatcher.add_handler(SETTING_HANDLER) 110 | dispatcher.add_handler(ANTI_ARABIC, group=ANTIARABIC_GROUPS) 111 | -------------------------------------------------------------------------------- /tg_bot/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | import telegram.ext as tg 6 | 7 | # enable logging 8 | logging.basicConfig( 9 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 10 | level=logging.INFO) 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | # if version < 3.6, stop bot. 15 | if sys.version_info[0] < 3 or sys.version_info[1] < 6: 16 | LOGGER.error( 17 | "You MUST have a python version of at least 3.6! Multiple features depend on this. Bot quitting." 18 | ) 19 | quit(1) 20 | 21 | ENV = bool(os.environ.get('ENV', False)) 22 | 23 | if ENV: 24 | TOKEN = os.environ.get('TOKEN', None) 25 | try: 26 | OWNER_ID = int(os.environ.get('OWNER_ID', None)) 27 | except ValueError: 28 | raise Exception("Your OWNER_ID env variable is not a valid integer.") 29 | 30 | MESSAGE_DUMP = os.environ.get('MESSAGE_DUMP', None) 31 | OWNER_USERNAME = os.environ.get("OWNER_USERNAME", None) 32 | 33 | try: 34 | SUDO_USERS = set( 35 | int(x) for x in os.environ.get("SUDO_USERS", "").split()) 36 | except ValueError: 37 | raise Exception( 38 | "Your sudo users list does not contain valid integers.") 39 | 40 | try: 41 | SUPPORT_USERS = set( 42 | int(x) for x in os.environ.get("SUPPORT_USERS", "").split()) 43 | except ValueError: 44 | raise Exception( 45 | "Your support users list does not contain valid integers.") 46 | 47 | try: 48 | WHITELIST_USERS = set( 49 | int(x) for x in os.environ.get("WHITELIST_USERS", "").split()) 50 | except ValueError: 51 | raise Exception( 52 | "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', 67 | 'CAADAgADOwADPPEcAXkko5EB3YGYAg') 68 | ALLOW_EXCL = os.environ.get('ALLOW_EXCL', False) 69 | API_WEATHER = os.environ.get('API_OPENWEATHER', None) 70 | STRICT_GMUTE = bool(os.environ.get('STRICT_GMUTE', False)) 71 | 72 | else: 73 | from tg_bot.config import Development as Config 74 | TOKEN = Config.API_KEY 75 | try: 76 | OWNER_ID = int(Config.OWNER_ID) 77 | except ValueError: 78 | raise Exception("Your OWNER_ID variable is not a valid integer.") 79 | 80 | MESSAGE_DUMP = Config.MESSAGE_DUMP 81 | OWNER_USERNAME = Config.OWNER_USERNAME 82 | 83 | try: 84 | SUDO_USERS = set(int(x) for x in Config.SUDO_USERS or []) 85 | except ValueError: 86 | raise Exception( 87 | "Your sudo users list does not contain valid integers.") 88 | 89 | try: 90 | SUPPORT_USERS = set(int(x) for x in Config.SUPPORT_USERS or []) 91 | except ValueError: 92 | raise Exception( 93 | "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( 99 | "Your whitelisted users list does not contain valid integers.") 100 | 101 | WEBHOOK = Config.WEBHOOK 102 | URL = Config.URL 103 | PORT = Config.PORT 104 | CERT_PATH = Config.CERT_PATH 105 | 106 | DB_URI = Config.SQLALCHEMY_DATABASE_URI 107 | DONATION_LINK = Config.DONATION_LINK 108 | LOAD = Config.LOAD 109 | NO_LOAD = Config.NO_LOAD 110 | DEL_CMDS = Config.DEL_CMDS 111 | STRICT_GBAN = Config.STRICT_GBAN 112 | WORKERS = Config.WORKERS 113 | BAN_STICKER = Config.BAN_STICKER 114 | ALLOW_EXCL = Config.ALLOW_EXCL 115 | API_WEATHER = Config.API_OPENWEATHER 116 | STRICT_GMUTE = Config.STRICT_GMUTE 117 | 118 | SUDO_USERS.add(OWNER_ID) 119 | 120 | updater = tg.Updater(TOKEN, workers=WORKERS) 121 | 122 | dispatcher = updater.dispatcher 123 | 124 | CallbackContext = tg.CallbackContext 125 | 126 | SUDO_USERS = list(SUDO_USERS) 127 | WHITELIST_USERS = list(WHITELIST_USERS) 128 | SUPPORT_USERS = list(SUPPORT_USERS) 129 | 130 | # Load at end to ensure all prev variables have been set 131 | from tg_bot.modules.helper_funcs.handlers import CustomCommandHandler, CustomRegexHandler 132 | 133 | # make sure the regex handler can take extra kwargs 134 | tg.RegexHandler = CustomRegexHandler 135 | 136 | if ALLOW_EXCL: 137 | tg.CommandHandler = CustomCommandHandler 138 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/global_bans_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, BigInteger, 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(BigInteger, 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 { 24 | "user_id": self.user_id, 25 | "name": self.name, 26 | "reason": self.reason 27 | } 28 | 29 | 30 | class GbanSettings(BASE): 31 | __tablename__ = "gban_settings" 32 | chat_id = Column(String(14), primary_key=True) 33 | setting = Column(Boolean, default=True, nullable=False) 34 | 35 | def __init__(self, chat_id, enabled): 36 | self.chat_id = str(chat_id) 37 | self.setting = enabled 38 | 39 | def __repr__(self): 40 | return "".format(self.chat_id, self.setting) 41 | 42 | 43 | GloballyBannedUsers.__table__.create(checkfirst=True) 44 | GbanSettings.__table__.create(checkfirst=True) 45 | 46 | GBANNED_USERS_LOCK = threading.RLock() 47 | GBAN_SETTING_LOCK = threading.RLock() 48 | GBANNED_LIST = set() 49 | GBANSTAT_LIST = set() 50 | 51 | 52 | def gban_user(user_id, name, reason=None): 53 | with GBANNED_USERS_LOCK: 54 | user = SESSION.query(GloballyBannedUsers).get(user_id) 55 | if not user: 56 | user = GloballyBannedUsers(user_id, name, reason) 57 | else: 58 | user.name = name 59 | user.reason = reason 60 | 61 | SESSION.merge(user) 62 | SESSION.commit() 63 | __load_gbanned_userid_list() 64 | 65 | 66 | def update_gban_reason(user_id, name, reason=None): 67 | with GBANNED_USERS_LOCK: 68 | user = SESSION.query(GloballyBannedUsers).get(user_id) 69 | if not user: 70 | return None 71 | old_reason = user.reason 72 | user.name = name 73 | user.reason = reason 74 | 75 | SESSION.merge(user) 76 | SESSION.commit() 77 | return old_reason 78 | 79 | 80 | def ungban_user(user_id): 81 | with GBANNED_USERS_LOCK: 82 | user = SESSION.query(GloballyBannedUsers).get(user_id) 83 | if user: 84 | SESSION.delete(user) 85 | 86 | SESSION.commit() 87 | __load_gbanned_userid_list() 88 | 89 | 90 | def is_user_gbanned(user_id): 91 | return user_id in GBANNED_LIST 92 | 93 | 94 | def get_gbanned_user(user_id): 95 | try: 96 | return SESSION.query(GloballyBannedUsers).get(user_id) 97 | finally: 98 | SESSION.close() 99 | 100 | 101 | def get_gban_list(): 102 | try: 103 | return [x.to_dict() for x in SESSION.query(GloballyBannedUsers).all()] 104 | finally: 105 | SESSION.close() 106 | 107 | 108 | def enable_gbans(chat_id): 109 | with GBAN_SETTING_LOCK: 110 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 111 | if not chat: 112 | chat = GbanSettings(chat_id, True) 113 | 114 | chat.setting = True 115 | SESSION.add(chat) 116 | SESSION.commit() 117 | if str(chat_id) in GBANSTAT_LIST: 118 | GBANSTAT_LIST.remove(str(chat_id)) 119 | 120 | 121 | def disable_gbans(chat_id): 122 | with GBAN_SETTING_LOCK: 123 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 124 | if not chat: 125 | chat = GbanSettings(chat_id, False) 126 | 127 | chat.setting = False 128 | SESSION.add(chat) 129 | SESSION.commit() 130 | GBANSTAT_LIST.add(str(chat_id)) 131 | 132 | 133 | def does_chat_gban(chat_id): 134 | return str(chat_id) not in GBANSTAT_LIST 135 | 136 | 137 | def num_gbanned_users(): 138 | return len(GBANNED_LIST) 139 | 140 | 141 | def __load_gbanned_userid_list(): 142 | global GBANNED_LIST 143 | try: 144 | GBANNED_LIST = { 145 | x.user_id 146 | for x in SESSION.query(GloballyBannedUsers).all() 147 | } 148 | finally: 149 | SESSION.close() 150 | 151 | 152 | def __load_gban_stat_list(): 153 | global GBANSTAT_LIST 154 | try: 155 | GBANSTAT_LIST = { 156 | x.chat_id 157 | for x in SESSION.query(GbanSettings).all() if not x.setting 158 | } 159 | finally: 160 | SESSION.close() 161 | 162 | 163 | def migrate_chat(old_chat_id, new_chat_id): 164 | with GBAN_SETTING_LOCK: 165 | chat = SESSION.query(GbanSettings).get(str(old_chat_id)) 166 | if chat: 167 | chat.chat_id = new_chat_id 168 | SESSION.add(chat) 169 | 170 | SESSION.commit() 171 | 172 | 173 | # Create in memory userid to avoid disk access 174 | __load_gbanned_userid_list() 175 | __load_gban_stat_list() 176 | -------------------------------------------------------------------------------- /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, CallbackContext, 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 | def broadcast(update: Update, context: CallbackContext): 51 | bot = context.bot 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", 63 | str(chat.chat_id), str(chat.chat_name)) 64 | 65 | update.effective_message.reply_text( 66 | "Broadcast complete. {} groups failed to receive the message, probably " 67 | "due to being kicked.".format(failed)) 68 | 69 | 70 | def log_user(update: Update, context: CallbackContext): 71 | bot = context.bot 72 | chat = update.effective_chat # type: Optional[Chat] 73 | msg = update.effective_message # type: Optional[Message] 74 | 75 | sql.update_user(msg.from_user.id, msg.from_user.username, 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, chat.id, 81 | chat.title) 82 | 83 | if msg.forward_from: 84 | sql.update_user(msg.forward_from.id, msg.forward_from.username) 85 | 86 | 87 | def chats(update: Update, context: CallbackContext): 88 | bot = context.bot 89 | all_chats = sql.get_all_chats() or [] 90 | chatfile = 'List of chats.\n' 91 | for chat in all_chats: 92 | chatfile += "{} - ({})\n".format(chat.chat_name, chat.chat_id) 93 | 94 | with BytesIO(str.encode(chatfile)) as output: 95 | output.name = "chatlist.txt" 96 | update.effective_message.reply_document( 97 | document=output, 98 | filename="chatlist.txt", 99 | caption="Here is the list of chats in my database.") 100 | 101 | 102 | def __user_info__(user_id): 103 | if user_id == dispatcher.bot.id: 104 | return """I've seen them in... Wow. Are they stalking me? They're in all the same places I am... oh. It's me.""" 105 | num_chats = sql.get_user_num_chats(user_id) 106 | return """I've seen them in {} chats in total.""".format( 107 | num_chats) 108 | 109 | 110 | def __stats__(): 111 | return "{} users, across {} chats.".format(sql.num_users(), 112 | sql.num_chats()) 113 | 114 | 115 | def __gdpr__(user_id): 116 | sql.del_user(user_id) 117 | 118 | 119 | def __migrate__(old_chat_id, new_chat_id): 120 | sql.migrate_chat(old_chat_id, new_chat_id) 121 | 122 | 123 | __help__ = "" # no help string 124 | 125 | __mod_name__ = "Users" 126 | 127 | BROADCAST_HANDLER = CommandHandler("broadcast", 128 | broadcast, 129 | filters=Filters.user(OWNER_ID), 130 | run_async=True) 131 | USER_HANDLER = MessageHandler(Filters.all & Filters.chat_type.groups, 132 | log_user, 133 | run_async=True) 134 | CHATLIST_HANDLER = CommandHandler("chatlist", 135 | chats, 136 | filters=CustomFilters.sudo_filter, 137 | run_async=True) 138 | 139 | dispatcher.add_handler(USER_HANDLER, USERS_GROUP) 140 | dispatcher.add_handler(BROADCAST_HANDLER) 141 | dispatcher.add_handler(CHATLIST_HANDLER) 142 | -------------------------------------------------------------------------------- /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, CallbackContext, LOGGER, OWNER_ID, SUDO_USERS, SUPPORT_USERS 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[ 16 | 1] in DELIMITERS and sed_string.count(sed_string[1]) >= 2: 17 | delim = sed_string[1] 18 | start = counter = 2 19 | while counter < len(sed_string): 20 | if sed_string[counter] == "\\": 21 | counter += 1 22 | 23 | elif sed_string[counter] == delim: 24 | replace = sed_string[start:counter] 25 | counter += 1 26 | start = counter 27 | break 28 | 29 | counter += 1 30 | 31 | else: 32 | return None 33 | 34 | while counter < len(sed_string): 35 | if sed_string[counter] == "\\" and counter + 1 < len( 36 | sed_string) and sed_string[counter + 1] == delim: 37 | sed_string = sed_string[:counter] + sed_string[counter + 1:] 38 | 39 | elif sed_string[counter] == delim: 40 | replace_with = sed_string[start:counter] 41 | counter += 1 42 | break 43 | 44 | counter += 1 45 | else: 46 | return replace, sed_string[start:], "" 47 | 48 | flags = "" 49 | if counter < len(sed_string): 50 | flags = sed_string[counter:] 51 | return replace, replace_with, flags.lower() 52 | 53 | 54 | def sed(update: Update, context: CallbackContext): 55 | user_id = update.effective_user.id 56 | if user_id is not OWNER_ID and user_id not in SUDO_USERS and user_id not in SUPPORT_USERS: 57 | return 58 | sed_result = separate_sed(update.effective_message.text) 59 | if sed_result and update.effective_message.reply_to_message: 60 | if update.effective_message.reply_to_message.text: 61 | to_fix = update.effective_message.reply_to_message.text 62 | elif update.effective_message.reply_to_message.caption: 63 | to_fix = update.effective_message.reply_to_message.caption 64 | else: 65 | return 66 | 67 | repl, repl_with, flags = sed_result 68 | 69 | if not repl: 70 | update.effective_message.reply_to_message.reply_text( 71 | "You're trying to replace... " 72 | "nothing with something?") 73 | return 74 | 75 | try: 76 | check = re.match(repl, to_fix, flags=re.IGNORECASE) 77 | 78 | if check and check.group(0).lower() == to_fix.lower(): 79 | update.effective_message.reply_to_message.reply_text( 80 | "There has been an unspecified error".format( 81 | update.effective_user.first_name)) 82 | return 83 | 84 | if 'i' in flags and 'g' in flags: 85 | text = re.sub(repl, repl_with, to_fix, flags=re.I).strip() 86 | elif 'i' in flags: 87 | text = re.sub(repl, repl_with, to_fix, count=1, 88 | flags=re.I).strip() 89 | elif 'g' in flags: 90 | text = re.sub(repl, repl_with, to_fix).strip() 91 | else: 92 | text = re.sub(repl, repl_with, to_fix, count=1).strip() 93 | except sre_constants.error: 94 | LOGGER.warning(update.effective_message.text) 95 | LOGGER.exception("SRE constant error") 96 | update.effective_message.reply_text( 97 | "Do you even sed? Apparently not.") 98 | return 99 | 100 | # empty string errors -_- 101 | if len(text) >= telegram.MAX_MESSAGE_LENGTH: 102 | update.effective_message.reply_text( 103 | "The result of the sed command was too long for \ 104 | telegram!") 105 | elif text: 106 | update.effective_message.reply_to_message.reply_text(text) 107 | 108 | 109 | #__help__ = """ 110 | # - s//(/): Reply to a message with this to perform a sed operation on that message, replacing all \ 111 | #occurrences of 'text1' with 'text2'. Flags are optional, and currently include 'i' for ignore case, 'g' for global, \ 112 | #or nothing. Delimiters include `/`, `_`, `|`, and `:`. Text grouping is supported. The resulting message cannot be \ 113 | #larger than {}. 114 | # 115 | #*Reminder:* Sed uses some special characters to make matching easier, such as these: `+*.?\\` 116 | #If you want to use these characters, make sure you escape them! 117 | #eg: \\?. 118 | #""".format(telegram.MAX_MESSAGE_LENGTH) 119 | 120 | #__mod_name__ = "Sed/Regex" 121 | 122 | SED_HANDLER = DisableAbleRegexHandler(r's([{}]).*?\1.*'.format( 123 | "".join(DELIMITERS)), 124 | sed, 125 | friendly="sed", 126 | run_async=True) 127 | 128 | dispatcher.add_handler(SED_HANDLER) 129 | -------------------------------------------------------------------------------- /tg_bot/modules/msg_deleting.py: -------------------------------------------------------------------------------- 1 | import html, time 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, CallbackContext, 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 | from tg_bot.modules.helper_funcs.perms import check_perms 14 | 15 | 16 | @user_admin 17 | @loggable 18 | def purge(update: Update, context: CallbackContext) -> str: 19 | if not check_perms(update, 0): 20 | return 21 | bot, args = context.bot, context.args 22 | msg = update.effective_message # type: Optional[Message] 23 | chat = update.effective_chat # type: Optional[Chat] 24 | user = update.effective_user # type: Optional[User] 25 | 26 | if msg.reply_to_message: 27 | if can_delete(chat, bot.id): 28 | message_id = msg.reply_to_message.message_id 29 | delete_to = msg.message_id - 1 30 | if args and args[0].isdigit(): 31 | new_del = message_id + int(args[0]) 32 | # No point deleting messages which haven't been written yet. 33 | if new_del < delete_to: 34 | delete_to = new_del 35 | 36 | for m_id in range(delete_to, message_id - 1, 37 | -1): # Reverse iteration over message ids 38 | try: 39 | bot.deleteMessage(chat.id, m_id) 40 | except BadRequest as err: 41 | if err.message == "Message can't be deleted": 42 | bot.send_message( 43 | chat.id, 44 | "Cannot delete all messages. The messages may be too old, I might " 45 | "not have delete rights, or this might not be a supergroup." 46 | ) 47 | 48 | elif err.message != "Message to delete not found": 49 | LOGGER.exception("Error while purging chat messages.") 50 | 51 | try: 52 | msg.delete() 53 | except BadRequest as err: 54 | if err.message == "Message can't be deleted": 55 | bot.send_message( 56 | chat.id, 57 | "Cannot delete all messages. The messages may be too old, I might " 58 | "not have delete rights, or this might not be a supergroup." 59 | ) 60 | 61 | elif err.message != "Message to delete not found": 62 | LOGGER.exception("Error while purging chat messages.") 63 | 64 | del_msg = bot.send_message(chat.id, "Purge complete.") 65 | time.sleep(5) 66 | del_msg.delete() 67 | return "{}:" \ 68 | "\n#PURGE" \ 69 | "\nAdmin: {}" \ 70 | "\nPurged {} messages.".format(html.escape(chat.title), 71 | mention_html(user.id, user.first_name), 72 | delete_to - message_id) 73 | 74 | else: 75 | msg.reply_text( 76 | "Reply to a message to select where to start purging from.") 77 | 78 | return "" 79 | 80 | 81 | @user_admin 82 | @loggable 83 | def del_message(update: Update, context: CallbackContext) -> str: 84 | if not check_perms(update, 0): 85 | return 86 | bot = context.bot 87 | if update.effective_message.reply_to_message: 88 | user = update.effective_user # type: Optional[User] 89 | chat = update.effective_chat # type: Optional[Chat] 90 | 91 | if can_delete(chat, bot.id): 92 | update.effective_message.reply_to_message.delete() 93 | update.effective_message.delete() 94 | return "{}:" \ 95 | "\n#DEL" \ 96 | "\nAdmin: {}" \ 97 | "\nMessage deleted.".format(html.escape(chat.title), 98 | mention_html(user.id, user.first_name)) 99 | else: 100 | update.effective_message.reply_text("Whadya want to delete?") 101 | 102 | return "" 103 | 104 | 105 | __help__ = """ 106 | Deleting messages made easy with this command. Bot purges \ 107 | messages all together or individually. 108 | 109 | *Admin only:* 110 | - /del: deletes the message you replied to 111 | - /purge: deletes all messages between this and the replied to message. 112 | - /purge : deletes the replied message, and X messages following it. 113 | """ 114 | 115 | __mod_name__ = "Purges" 116 | 117 | DELETE_HANDLER = CommandHandler("del", 118 | del_message, 119 | filters=Filters.chat_type.groups, 120 | run_async=True) 121 | PURGE_HANDLER = CommandHandler("purge", 122 | purge, 123 | filters=Filters.chat_type.groups, 124 | run_async=True) 125 | 126 | dispatcher.add_handler(DELETE_HANDLER) 127 | dispatcher.add_handler(PURGE_HANDLER) 128 | -------------------------------------------------------------------------------- /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, CallbackContext 11 | from tg_bot.modules.disable import DisableAbleCommandHandler 12 | from tg_bot.modules.helper_funcs.chat_status import user_admin 13 | from tg_bot.modules.helper_funcs.string_handling import markdown_parser 14 | 15 | 16 | def get_rules(update: Update, context: CallbackContext): 17 | bot = context.bot 18 | chat_id = update.effective_chat.id 19 | send_rules(update, chat_id) 20 | 21 | 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( 30 | user.id, 31 | "The rules shortcut for this chat hasn't been set properly! Ask admins to " 32 | "fix this.") 33 | return 34 | else: 35 | raise 36 | 37 | rules = sql.get_rules(chat_id) 38 | text = "The rules for *{}* are:\n\n{}".format(escape_markdown(chat.title), 39 | rules) 40 | 41 | if from_pm and rules: 42 | bot.send_message(user.id, text, parse_mode=ParseMode.MARKDOWN) 43 | elif from_pm: 44 | bot.send_message( 45 | user.id, 46 | "The group admins haven't set any rules for this chat yet. " 47 | "This probably doesn't mean it's lawless though...!") 48 | elif rules: 49 | update.effective_message.reply_text( 50 | "Contact me in PM to get this group's rules.", 51 | reply_markup=InlineKeyboardMarkup([[ 52 | InlineKeyboardButton(text="Rules", 53 | url="t.me/{}?start={}".format( 54 | bot.username, chat_id)) 55 | ]])) 56 | else: 57 | update.effective_message.reply_text( 58 | "The group admins haven't set any rules for this chat yet. " 59 | "This probably doesn't mean it's lawless though...!") 60 | 61 | 62 | @user_admin 63 | def set_rules(update: Update, context: CallbackContext): 64 | bot = context.bot 65 | chat_id = update.effective_chat.id 66 | msg = update.effective_message # type: Optional[Message] 67 | raw_text = msg.text 68 | args = raw_text.split(None, 69 | 1) # use python's maxsplit to separate cmd and args 70 | if len(args) == 2: 71 | txt = args[1] 72 | offset = len(txt) - len( 73 | raw_text) # set correct offset relative to command 74 | markdown_rules = markdown_parser(txt, 75 | entities=msg.parse_entities(), 76 | offset=offset) 77 | 78 | sql.set_rules(chat_id, markdown_rules) 79 | update.effective_message.reply_text( 80 | "Successfully set rules for this group.") 81 | 82 | 83 | @user_admin 84 | def clear_rules(update: Update, context: CallbackContext): 85 | bot = context.bot 86 | chat_id = update.effective_chat.id 87 | sql.set_rules(chat_id, "") 88 | update.effective_message.reply_text("Successfully cleared rules!") 89 | 90 | 91 | def __stats__(): 92 | return "{} chats have rules set.".format(sql.num_chats()) 93 | 94 | 95 | def __import_data__(chat_id, data): 96 | # set chat rules 97 | rules = data.get('info', {}).get('rules', "") 98 | sql.set_rules(chat_id, rules) 99 | 100 | 101 | def __migrate__(old_chat_id, new_chat_id): 102 | sql.migrate_chat(old_chat_id, new_chat_id) 103 | 104 | 105 | def __chat_settings__(chat_id, user_id): 106 | return "This chat has had it's rules set: `{}`".format( 107 | bool(sql.get_rules(chat_id))) 108 | 109 | 110 | __help__ = """ 111 | Every chat works with different rules; this module will help make those rules clearer! 112 | 113 | - /rules: get the rules for this chat. 114 | 115 | *Admin only:* 116 | - /setrules : set the rules for this chat. 117 | - /clearrules: clear the rules for this chat. 118 | 119 | *Important* 120 | By default rules are sent to PM of the user that sends the command. 121 | If you want the rules to be sent in the group (more spam but as you wish), you can use `/disable rules` command for your group and set them with custom filters ( `/filter /rules random rules`) 122 | """ 123 | 124 | __mod_name__ = "Rules" 125 | 126 | GET_RULES_HANDLER = DisableAbleCommandHandler("rules", 127 | get_rules, 128 | filters=Filters.chat_type.groups, 129 | run_async=True) 130 | SET_RULES_HANDLER = CommandHandler("setrules", 131 | set_rules, 132 | filters=Filters.chat_type.groups, 133 | run_async=True) 134 | RESET_RULES_HANDLER = CommandHandler("clearrules", 135 | clear_rules, 136 | filters=Filters.chat_type.groups, 137 | run_async=True) 138 | 139 | dispatcher.add_handler(GET_RULES_HANDLER) 140 | dispatcher.add_handler(SET_RULES_HANDLER) 141 | dispatcher.add_handler(RESET_RULES_HANDLER) 142 | -------------------------------------------------------------------------------- /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, CallbackContext, 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_GROUPS = 5 15 | 16 | 17 | @user_admin 18 | def report_setting(update: Update, context: CallbackContext): 19 | bot, args = context.bot, context.args 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( 28 | "Turned on reporting! You'll be notified whenever anyone reports something." 29 | ) 30 | 31 | elif args[0] in ("no", "off"): 32 | sql.set_user_setting(chat.id, False) 33 | msg.reply_text( 34 | "Turned off reporting! You wont get any reports.") 35 | else: 36 | msg.reply_text("Your current report preference is: `{}`".format( 37 | sql.user_should_report(chat.id)), 38 | parse_mode=ParseMode.MARKDOWN) 39 | 40 | else: 41 | if len(args) >= 1: 42 | if args[0] in ("yes", "on"): 43 | sql.set_chat_setting(chat.id, True) 44 | msg.reply_text( 45 | "Turned on reporting! Admins who have turned on reports will be notified when /report " 46 | "or @admin are called.") 47 | 48 | elif args[0] in ("no", "off"): 49 | sql.set_chat_setting(chat.id, False) 50 | msg.reply_text( 51 | "Turned off reporting! No admins will be notified on /report or @admin." 52 | ) 53 | else: 54 | msg.reply_text("This chat's current setting is: `{}`".format( 55 | sql.chat_should_report(chat.id)), 56 | parse_mode=ParseMode.MARKDOWN) 57 | 58 | 59 | @user_not_admin 60 | @loggable 61 | def report(update: Update, context: CallbackContext) -> str: 62 | bot = context.bot 63 | message = update.effective_message # type: Optional[Message] 64 | chat = update.effective_chat # type: Optional[Chat] 65 | user = update.effective_user # type: Optional[User] 66 | ping_list = "" 67 | 68 | if chat and message.reply_to_message and sql.chat_should_report(chat.id): 69 | reported_user = message.reply_to_message.from_user # type: Optional[User] 70 | if reported_user.id == bot.id: 71 | message.reply_text("Haha nope, not gonna report myself.") 72 | return "" 73 | chat_name = chat.title or chat.first or chat.username 74 | admin_list = chat.get_administrators() 75 | 76 | for admin in admin_list: 77 | if admin.user.is_bot: # can't message bots 78 | continue 79 | 80 | ping_list += f"​[​](tg://user?id={admin.user.id})" 81 | 82 | message.reply_text( 83 | f"Successfully reported [{reported_user.first_name}](tg://user?id={reported_user.id}) to admins! " 84 | + ping_list, 85 | parse_mode=ParseMode.MARKDOWN) 86 | 87 | return "" 88 | 89 | 90 | def __migrate__(old_chat_id, new_chat_id): 91 | sql.migrate_chat(old_chat_id, new_chat_id) 92 | 93 | 94 | def __chat_settings__(chat_id, user_id): 95 | return "This chat is setup to send user reports to admins, via /report and @admin: `{}`".format( 96 | sql.chat_should_report(chat_id)) 97 | 98 | 99 | def __user_settings__(user_id): 100 | return "You receive reports from chats you're admin in: `{}`.\nToggle this with /reports in PM.".format( 101 | sql.user_should_report(user_id)) 102 | 103 | 104 | __mod_name__ = "Reporting" 105 | 106 | __help__ = """ 107 | We're all busy people who don't have time to monitor our groups 24/7. But how do you \ 108 | react if someone in your group is spamming? 109 | 110 | Presenting reports; if someone in your group thinks someone needs reporting, they now have \ 111 | an easy way to call all admins. 112 | 113 | - /report : reply to a message to report it to admins. 114 | - @admin: reply to a message to report it to admins. 115 | NOTE: neither of these will get triggered if used by admins 116 | 117 | *Admin only:* 118 | - /reports : change report setting, or view current status. 119 | - If done in pm, toggles your status. 120 | - If in chat, toggles that chat's status. 121 | 122 | To report a user, simply reply to user's message with @admin or /report. \ 123 | This message tags all the chat admins; same as if they had been @'ed. 124 | You MUST reply to a message to report a user; you can't just use @admin to tag admins for no reason! 125 | 126 | Note that the report commands do not work when admins use them; or when used to report an admin. Bot assumes that \ 127 | admins don't need to report, or be reported! 128 | """ 129 | 130 | REPORT_HANDLER = CommandHandler("report", 131 | report, 132 | filters=Filters.chat_type.groups, 133 | run_async=True) 134 | SETTING_HANDLER = CommandHandler("reports", report_setting, run_async=True) 135 | ADMIN_REPORT_HANDLER = RegexHandler("(?i)@admin(s)?", report, run_async=True) 136 | 137 | dispatcher.add_handler(REPORT_HANDLER, group=REPORT_GROUPS) 138 | dispatcher.add_handler(ADMIN_REPORT_HANDLER, group=REPORT_GROUPS) 139 | dispatcher.add_handler(SETTING_HANDLER) 140 | -------------------------------------------------------------------------------- /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 CallbackContext, DEL_CMDS, SUDO_USERS, WHITELIST_USERS 7 | 8 | 9 | def can_delete(chat: Chat, bot_id: int) -> bool: 10 | return chat.get_member(bot_id).can_delete_messages 11 | 12 | 13 | def is_user_ban_protected(chat: Chat, 14 | user_id: int, 15 | member: ChatMember = None) -> bool: 16 | if chat.type == 'private' \ 17 | or user_id in SUDO_USERS \ 18 | or user_id in WHITELIST_USERS \ 19 | or chat.all_members_are_administrators: 20 | return True 21 | 22 | if not member: 23 | member = chat.get_member(user_id) 24 | return member.status in ('administrator', 'creator') 25 | 26 | 27 | def is_user_admin(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 28 | if chat.type == 'private' \ 29 | or user_id in SUDO_USERS \ 30 | or chat.all_members_are_administrators: 31 | return True 32 | 33 | if not member: 34 | member = chat.get_member(user_id) 35 | return member.status in ('administrator', 'creator') 36 | 37 | 38 | def is_bot_admin(chat: Chat, 39 | bot_id: int, 40 | bot_member: ChatMember = None) -> bool: 41 | if chat.type == 'private' \ 42 | or chat.all_members_are_administrators: 43 | return True 44 | 45 | if not bot_member: 46 | bot_member = chat.get_member(bot_id) 47 | return bot_member.status in ('administrator', 'creator') 48 | 49 | 50 | def is_user_in_chat(chat: Chat, user_id: int) -> bool: 51 | member = chat.get_member(user_id) 52 | return member.status not in ('left', 'kicked') 53 | 54 | 55 | def bot_can_delete(func): 56 | @wraps(func) 57 | def delete_rights(update: Update, context: CallbackContext, *args, 58 | **kwargs): 59 | bot = context.bot 60 | if can_delete(update.effective_chat, bot.id): 61 | return func(update, context, *args, **kwargs) 62 | else: 63 | update.effective_message.reply_text( 64 | "I can't delete messages here! " 65 | "Make sure I'm admin and can delete other user's messages.") 66 | 67 | return delete_rights 68 | 69 | 70 | def can_pin(func): 71 | @wraps(func) 72 | def pin_rights(update: Update, context: CallbackContext, *args, **kwargs): 73 | bot = context.bot 74 | if update.effective_chat.get_member(bot.id).can_pin_messages: 75 | return func(update, context, *args, **kwargs) 76 | else: 77 | update.effective_message.reply_text( 78 | "I can't pin messages here! " 79 | "Make sure I'm admin and can pin messages.") 80 | 81 | return pin_rights 82 | 83 | 84 | def can_promote(func): 85 | @wraps(func) 86 | def promote_rights(update: Update, context: CallbackContext, *args, 87 | **kwargs): 88 | bot = context.bot 89 | if update.effective_chat.get_member(bot.id).can_promote_members: 90 | return func(update, context, *args, **kwargs) 91 | else: 92 | update.effective_message.reply_text( 93 | "I can't promote/demote people here! " 94 | "Make sure I'm admin and can appoint new admins.") 95 | 96 | return promote_rights 97 | 98 | 99 | def can_restrict(func): 100 | @wraps(func) 101 | def promote_rights(update: Update, context: CallbackContext, *args, 102 | **kwargs): 103 | bot = context.bot 104 | if update.effective_chat.get_member(bot.id).can_restrict_members: 105 | return func(update, context, *args, **kwargs) 106 | else: 107 | update.effective_message.reply_text( 108 | "I can't restrict people here! " 109 | "Make sure I'm admin and can appoint new admins.") 110 | 111 | return promote_rights 112 | 113 | 114 | def bot_admin(func): 115 | @wraps(func) 116 | def is_admin(update: Update, context: CallbackContext, *args, **kwargs): 117 | bot = context.bot 118 | if is_bot_admin(update.effective_chat, bot.id): 119 | return func(update, context, *args, **kwargs) 120 | else: 121 | update.effective_message.reply_text("I'm not admin!") 122 | 123 | return is_admin 124 | 125 | 126 | def user_admin(func): 127 | @wraps(func) 128 | def is_admin(update: Update, context: CallbackContext, *args, **kwargs): 129 | bot = context.bot 130 | user = update.effective_user # type: Optional[User] 131 | if user and is_user_admin(update.effective_chat, user.id): 132 | return func(update, context, *args, **kwargs) 133 | 134 | elif not user: 135 | pass 136 | 137 | else: 138 | update.effective_message.delete() 139 | 140 | return is_admin 141 | 142 | 143 | def user_admin_no_reply(func): 144 | @wraps(func) 145 | def is_admin(update: Update, context: CallbackContext, *args, **kwargs): 146 | bot = context.bot 147 | user = update.effective_user # type: Optional[User] 148 | if user and is_user_admin(update.effective_chat, user.id): 149 | return func(update, context, *args, **kwargs) 150 | 151 | elif not user: 152 | pass 153 | 154 | elif DEL_CMDS and " " not in update.effective_message.text: 155 | update.effective_message.delete() 156 | 157 | return is_admin 158 | 159 | 160 | def user_not_admin(func): 161 | @wraps(func) 162 | def is_not_admin(update: Update, context: CallbackContext, *args, 163 | **kwargs): 164 | bot = context.bot 165 | user = update.effective_user # type: Optional[User] 166 | if user and not is_user_admin(update.effective_chat, user.id): 167 | return func(update, context, *args, **kwargs) 168 | 169 | return is_not_admin 170 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/users_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, BigInteger, 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(BigInteger, 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(BigInteger, 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(BigInteger, 45 | ForeignKey("users.user_id", 46 | onupdate="CASCADE", 47 | ondelete="CASCADE"), 48 | nullable=False) 49 | __table_args__ = (UniqueConstraint('chat', 'user', 50 | name='_chat_members_uc'), ) 51 | 52 | def __init__(self, chat, user): 53 | self.chat = chat 54 | self.user = user 55 | 56 | def __repr__(self): 57 | return "".format( 58 | self.user.username, self.user.user_id, self.chat.chat_name, 59 | self.chat.chat_id) 60 | 61 | 62 | Users.__table__.create(checkfirst=True) 63 | Chats.__table__.create(checkfirst=True) 64 | ChatMembers.__table__.create(checkfirst=True) 65 | 66 | INSERTION_LOCK = threading.RLock() 67 | 68 | 69 | def ensure_bot_in_db(): 70 | with INSERTION_LOCK: 71 | bot = Users(dispatcher.bot.id, dispatcher.bot.username) 72 | SESSION.merge(bot) 73 | SESSION.commit() 74 | 75 | 76 | def update_user(user_id, username, chat_id=None, chat_name=None): 77 | with INSERTION_LOCK: 78 | user = SESSION.query(Users).get(user_id) 79 | if not user: 80 | user = Users(user_id, username) 81 | SESSION.add(user) 82 | SESSION.flush() 83 | else: 84 | user.username = username 85 | 86 | if not chat_id or not chat_name: 87 | SESSION.commit() 88 | return 89 | 90 | chat = SESSION.query(Chats).get(str(chat_id)) 91 | if not chat: 92 | chat = Chats(str(chat_id), chat_name) 93 | SESSION.add(chat) 94 | SESSION.flush() 95 | 96 | else: 97 | chat.chat_name = chat_name 98 | 99 | member = SESSION.query(ChatMembers).filter( 100 | ChatMembers.chat == chat.chat_id, 101 | ChatMembers.user == user.user_id).first() 102 | if not member: 103 | chat_member = ChatMembers(chat.chat_id, user.user_id) 104 | SESSION.add(chat_member) 105 | 106 | SESSION.commit() 107 | 108 | 109 | def get_userid_by_name(username): 110 | try: 111 | return SESSION.query(Users).filter( 112 | func.lower(Users.username) == username.lower()).all() 113 | finally: 114 | SESSION.close() 115 | 116 | 117 | def get_name_by_userid(user_id): 118 | try: 119 | return SESSION.query(Users).get(Users.user_id == int(user_id)).first() 120 | finally: 121 | SESSION.close() 122 | 123 | 124 | def get_chat_members(chat_id): 125 | try: 126 | return SESSION.query(ChatMembers).filter( 127 | ChatMembers.chat == str(chat_id)).all() 128 | finally: 129 | SESSION.close() 130 | 131 | 132 | def get_all_chats(): 133 | try: 134 | return SESSION.query(Chats).all() 135 | finally: 136 | SESSION.close() 137 | 138 | 139 | def get_user_num_chats(user_id): 140 | try: 141 | return SESSION.query(ChatMembers).filter( 142 | ChatMembers.user == int(user_id)).count() 143 | finally: 144 | SESSION.close() 145 | 146 | 147 | def num_chats(): 148 | try: 149 | return SESSION.query(Chats).count() 150 | finally: 151 | SESSION.close() 152 | 153 | 154 | def num_users(): 155 | try: 156 | return SESSION.query(Users).count() 157 | finally: 158 | SESSION.close() 159 | 160 | 161 | def get_chat_name(chat_id): 162 | try: 163 | return SESSION.query(Chats).get(str(chat_id)).chat_name 164 | finally: 165 | SESSION.close() 166 | 167 | 168 | def migrate_chat(old_chat_id, new_chat_id): 169 | with INSERTION_LOCK: 170 | chat = SESSION.query(Chats).get(str(old_chat_id)) 171 | if chat: 172 | chat.chat_id = str(new_chat_id) 173 | SESSION.add(chat) 174 | 175 | SESSION.flush() 176 | 177 | chat_members = SESSION.query(ChatMembers).filter( 178 | ChatMembers.chat == str(old_chat_id)).all() 179 | for member in chat_members: 180 | member.chat = str(new_chat_id) 181 | SESSION.add(member) 182 | 183 | SESSION.commit() 184 | 185 | 186 | ensure_bot_in_db() 187 | 188 | 189 | def del_user(user_id): 190 | with INSERTION_LOCK: 191 | curr = SESSION.query(Users).get(user_id) 192 | if curr: 193 | SESSION.delete(curr) 194 | SESSION.commit() 195 | return True 196 | 197 | ChatMembers.query.filter(ChatMembers.user == user_id).delete() 198 | SESSION.commit() 199 | SESSION.close() 200 | return False 201 | -------------------------------------------------------------------------------- /tg_bot/modules/webtools.py: -------------------------------------------------------------------------------- 1 | import html 2 | import json 3 | from datetime import datetime 4 | from typing import Optional, List 5 | import requests 6 | import subprocess 7 | import os 8 | import speedtest 9 | import time 10 | from telegram import Message, Chat, Update, Bot, MessageEntity 11 | from telegram import ParseMode 12 | from telegram.ext import CommandHandler, run_async, Filters 13 | from telegram.utils.helpers import escape_markdown, mention_html 14 | from tg_bot.modules.helper_funcs.extraction import extract_text 15 | 16 | from tg_bot import dispatcher, CallbackContext, OWNER_ID, SUDO_USERS, SUPPORT_USERS, WHITELIST_USERS 17 | from tg_bot.modules.helper_funcs.filters import CustomFilters 18 | 19 | 20 | #Kanged from PaperPlane Extended userbot 21 | def speed_convert(size): 22 | """ 23 | Hi human, you can't read bytes? 24 | """ 25 | power = 2**10 26 | zero = 0 27 | units = {0: '', 1: 'Kb/s', 2: 'Mb/s', 3: 'Gb/s', 4: 'Tb/s'} 28 | while size > power: 29 | size /= power 30 | zero += 1 31 | return f"{round(size, 2)} {units[zero]}" 32 | 33 | 34 | def get_bot_ip(update: Update, context: CallbackContext): 35 | bot = context.bot 36 | """ Sends the bot's IP address, so as to be able to ssh in if necessary. 37 | OWNER ONLY. 38 | """ 39 | res = requests.get("http://ipinfo.io/ip") 40 | update.message.reply_text(res.text) 41 | 42 | 43 | def rtt(update: Update, context: CallbackContext): 44 | bot = context.bot 45 | out = "" 46 | under = False 47 | if os.name == 'nt': 48 | output = subprocess.check_output("ping -n 1 1.0.0.1 | findstr time*", 49 | shell=True).decode() 50 | outS = output.splitlines() 51 | out = outS[0] 52 | else: 53 | out = subprocess.check_output("ping -c 1 1.0.0.1 | grep time=", 54 | shell=True).decode() 55 | splitOut = out.split(' ') 56 | stringtocut = "" 57 | for line in splitOut: 58 | if (line.startswith('time=') or line.startswith('time<')): 59 | stringtocut = line 60 | break 61 | newstra = stringtocut.split('=') 62 | if len(newstra) == 1: 63 | under = True 64 | newstra = stringtocut.split('<') 65 | newstr = "" 66 | if os.name == 'nt': 67 | newstr = newstra[1].split('ms') 68 | else: 69 | newstr = newstra[1].split( 70 | ' ') #redundant split, but to try and not break windows ping 71 | ping_time = float(newstr[0]) 72 | if os.name == 'nt' and under: 73 | update.effective_message.reply_text( 74 | " Round-trip time is <{}ms".format(ping_time)) 75 | else: 76 | update.effective_message.reply_text( 77 | " Round-trip time: {}ms".format(ping_time)) 78 | 79 | 80 | def ping(update: Update, context: CallbackContext): 81 | bot = context.bot 82 | message = update.effective_message 83 | parsing = extract_text(message).split(' ') 84 | if (len(parsing) < 2): 85 | message.reply_text("Give me an address to ping!") 86 | return 87 | elif (len(parsing) > 2): 88 | message.reply_text("Too many arguments!") 89 | return 90 | dns = (parsing)[1] 91 | out = "" 92 | under = False 93 | if os.name == 'nt': 94 | try: 95 | output = subprocess.check_output("ping -n 1 " + dns + 96 | " | findstr time*", 97 | shell=True).decode() 98 | except: 99 | message.reply_text("There was a problem parsing the IP/Hostname") 100 | return 101 | outS = output.splitlines() 102 | out = outS[0] 103 | else: 104 | try: 105 | out = subprocess.check_output("ping -c 1 " + dns + " | grep time=", 106 | shell=True).decode() 107 | except: 108 | message.reply_text("There was a problem parsing the IP/Hostname") 109 | return 110 | splitOut = out.split(' ') 111 | stringtocut = "" 112 | for line in splitOut: 113 | if (line.startswith('time=') or line.startswith('time<')): 114 | stringtocut = line 115 | break 116 | newstra = stringtocut.split('=') 117 | if len(newstra) == 1: 118 | under = True 119 | newstra = stringtocut.split('<') 120 | newstr = "" 121 | if os.name == 'nt': 122 | newstr = newstra[1].split('ms') 123 | else: 124 | newstr = newstra[1].split( 125 | ' ') #redundant split, but to try and not break windows ping 126 | ping_time = float(newstr[0]) 127 | if os.name == 'nt' and under: 128 | update.effective_message.reply_text(" Ping speed of " + dns + 129 | " is <{}ms".format(ping_time)) 130 | else: 131 | update.effective_message.reply_text(" Ping speed of " + dns + 132 | ": {}ms".format(ping_time)) 133 | 134 | 135 | def speedtst(update: Update, context: CallbackContext): 136 | bot = context.bot 137 | test = speedtest.Speedtest(secure=True) 138 | test.get_best_server() 139 | test.download() 140 | test.upload() 141 | test.results.share() 142 | result = test.results.dict() 143 | update.effective_message.reply_text( 144 | "Download " 145 | f"{speed_convert(result['download'])} \n" 146 | "Upload " 147 | f"{speed_convert(result['upload'])} \n" 148 | "Ping " 149 | f"{result['ping']} \n" 150 | "ISP " 151 | f"{result['client']['isp']}") 152 | 153 | 154 | IP_HANDLER = CommandHandler("ip", 155 | get_bot_ip, 156 | filters=Filters.chat(OWNER_ID), 157 | run_async=True) 158 | RTT_HANDLER = CommandHandler("ping", 159 | rtt, 160 | filters=CustomFilters.sudo_filter, 161 | run_async=True) 162 | PING_HANDLER = CommandHandler("cping", 163 | ping, 164 | filters=CustomFilters.sudo_filter, 165 | run_async=True) 166 | SPEED_HANDLER = CommandHandler("speedtest", 167 | speedtst, 168 | filters=CustomFilters.sudo_filter, 169 | run_async=True) 170 | 171 | dispatcher.add_handler(IP_HANDLER) 172 | dispatcher.add_handler(RTT_HANDLER) 173 | dispatcher.add_handler(SPEED_HANDLER) 174 | dispatcher.add_handler(PING_HANDLER) 175 | -------------------------------------------------------------------------------- /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 | class ClearNotes(BASE): 49 | __tablename__ = "clear_notes" 50 | chat_id = Column(String(14), primary_key=True) 51 | timer = Column(Integer, default=0) 52 | 53 | def __init__(self, chat_id, timer): 54 | self.chat_id = str(chat_id) 55 | self.timer = int(timer) 56 | 57 | Notes.__table__.create(checkfirst=True) 58 | Buttons.__table__.create(checkfirst=True) 59 | ClearNotes.__table__.create(checkfirst=True) 60 | 61 | NOTES_INSERTION_LOCK = threading.RLock() 62 | BUTTONS_INSERTION_LOCK = threading.RLock() 63 | CLEARNOTES_INSERTION_LOCK = threading.RLock() 64 | 65 | def add_note_to_db(chat_id, 66 | note_name, 67 | note_data, 68 | msgtype, 69 | buttons=None, 70 | file=None): 71 | if not buttons: 72 | buttons = [] 73 | 74 | with NOTES_INSERTION_LOCK: 75 | prev = SESSION.query(Notes).get((str(chat_id), note_name)) 76 | if prev: 77 | with BUTTONS_INSERTION_LOCK: 78 | prev_buttons = SESSION.query(Buttons).filter( 79 | Buttons.chat_id == str(chat_id), 80 | Buttons.note_name == note_name).all() 81 | for btn in prev_buttons: 82 | SESSION.delete(btn) 83 | SESSION.delete(prev) 84 | note = Notes(str(chat_id), 85 | note_name, 86 | note_data or "", 87 | msgtype=msgtype.value, 88 | file=file) 89 | SESSION.add(note) 90 | SESSION.commit() 91 | 92 | for b_name, url, same_line in buttons: 93 | add_note_button_to_db(chat_id, note_name, b_name, url, same_line) 94 | 95 | 96 | def get_note(chat_id, note_name): 97 | try: 98 | return SESSION.query(Notes).get((str(chat_id), note_name)) 99 | finally: 100 | SESSION.close() 101 | 102 | 103 | def rm_note(chat_id, note_name): 104 | with NOTES_INSERTION_LOCK: 105 | note = SESSION.query(Notes).get((str(chat_id), note_name)) 106 | if note: 107 | with BUTTONS_INSERTION_LOCK: 108 | buttons = SESSION.query(Buttons).filter( 109 | Buttons.chat_id == str(chat_id), 110 | Buttons.note_name == note_name).all() 111 | for btn in buttons: 112 | SESSION.delete(btn) 113 | 114 | SESSION.delete(note) 115 | SESSION.commit() 116 | return True 117 | 118 | else: 119 | SESSION.close() 120 | return False 121 | 122 | 123 | def get_all_chat_notes(chat_id): 124 | try: 125 | return SESSION.query(Notes).filter( 126 | Notes.chat_id == str(chat_id)).order_by(Notes.name.asc()).all() 127 | finally: 128 | SESSION.close() 129 | 130 | 131 | def add_note_button_to_db(chat_id, note_name, b_name, url, same_line): 132 | with BUTTONS_INSERTION_LOCK: 133 | button = Buttons(chat_id, note_name, b_name, url, same_line) 134 | SESSION.add(button) 135 | SESSION.commit() 136 | 137 | 138 | def get_buttons(chat_id, note_name): 139 | try: 140 | return SESSION.query(Buttons).filter( 141 | Buttons.chat_id == str(chat_id), 142 | Buttons.note_name == note_name).order_by(Buttons.id).all() 143 | finally: 144 | SESSION.close() 145 | 146 | 147 | def num_notes(): 148 | try: 149 | return SESSION.query(Notes).count() 150 | finally: 151 | SESSION.close() 152 | 153 | 154 | def num_chats(): 155 | try: 156 | return SESSION.query(func.count(distinct(Notes.chat_id))).scalar() 157 | finally: 158 | SESSION.close() 159 | 160 | 161 | def migrate_chat(old_chat_id, new_chat_id): 162 | with NOTES_INSERTION_LOCK: 163 | chat_notes = SESSION.query(Notes).filter( 164 | Notes.chat_id == str(old_chat_id)).all() 165 | for note in chat_notes: 166 | note.chat_id = str(new_chat_id) 167 | 168 | with BUTTONS_INSERTION_LOCK: 169 | chat_buttons = SESSION.query(Buttons).filter( 170 | Buttons.chat_id == str(old_chat_id)).all() 171 | for btn in chat_buttons: 172 | btn.chat_id = str(new_chat_id) 173 | 174 | SESSION.commit() 175 | 176 | def get_clearnotes(chat_id): 177 | try: 178 | clear = SESSION.query(ClearNotes).get(str(chat_id)) 179 | if clear: 180 | return clear.timer 181 | return 0 182 | finally: 183 | SESSION.close() 184 | 185 | def set_clearnotes(chat_id, clearnotes=False, timer=120): 186 | with CLEARNOTES_INSERTION_LOCK: 187 | exists = SESSION.query(ClearNotes).get(str(chat_id)) 188 | if exists: 189 | SESSION.delete(exists) 190 | clearnotes = ClearNotes(str(chat_id), int(timer)) 191 | SESSION.add(clearnotes) 192 | SESSION.commit() -------------------------------------------------------------------------------- /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 | 19 | 20 | def get_note_type(msg: Message): 21 | data_type = None 22 | content = None 23 | text = "" 24 | raw_text = msg.text or msg.caption 25 | args = raw_text.split(None, 26 | 2) # use python's maxsplit to separate cmd and args 27 | note_name = args[1] 28 | 29 | buttons = [] 30 | try: 31 | ttext = args[2] 32 | except: 33 | ttext = "" 34 | # determine what the contents of the filter are - text, image, sticker, etc 35 | 36 | if msg.reply_to_message: 37 | entities = msg.reply_to_message.parse_entities() 38 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 39 | if len(args) >= 2 and msg.reply_to_message.text: # not caption, text 40 | text, buttons = button_markdown_parser(msgtext, entities=entities) 41 | if buttons: 42 | data_type = Types.BUTTON_TEXT 43 | else: 44 | data_type = Types.TEXT 45 | 46 | elif msg.reply_to_message.sticker: 47 | content = msg.reply_to_message.sticker.file_id 48 | data_type = Types.STICKER 49 | 50 | elif msg.reply_to_message.document: 51 | content = msg.reply_to_message.document.file_id 52 | text, buttons = button_markdown_parser(msgtext, entities=entities) 53 | text = ttext or text 54 | data_type = Types.DOCUMENT 55 | 56 | elif msg.reply_to_message.photo: 57 | content = msg.reply_to_message.photo[ 58 | -1].file_id # last elem = best quality 59 | text, buttons = button_markdown_parser(msgtext, entities=entities) 60 | text = ttext or text 61 | data_type = Types.PHOTO 62 | 63 | elif msg.reply_to_message.audio: 64 | content = msg.reply_to_message.audio.file_id 65 | text, buttons = button_markdown_parser(msgtext, entities=entities) 66 | text = ttext or text 67 | data_type = Types.AUDIO 68 | 69 | elif msg.reply_to_message.voice: 70 | content = msg.reply_to_message.voice.file_id 71 | text, buttons = button_markdown_parser(msgtext, entities=entities) 72 | text = ttext or text 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 | text = ttext or text 79 | data_type = Types.VIDEO 80 | elif len(args) >= 3: 81 | offset = len(args[2]) - len( 82 | raw_text) # set correct offset relative to command + notename 83 | text, buttons = button_markdown_parser(args[2], 84 | entities=msg.parse_entities() 85 | or msg.parse_caption_entities(), 86 | offset=offset) 87 | if buttons: 88 | data_type = Types.BUTTON_TEXT 89 | else: 90 | data_type = Types.TEXT 91 | 92 | 93 | return note_name, text, data_type, content, buttons 94 | 95 | 96 | # note: add own args? 97 | # note: add own args? 98 | def get_welcome_type(msg: Message): 99 | data_type = None 100 | content = None 101 | text = "" 102 | raw_text = msg.text or msg.caption 103 | args = raw_text.split(None, 104 | 1) # use python's maxsplit to separate cmd and args 105 | 106 | buttons = [] 107 | # determine what the contents of the filter are - text, image, sticker, etc 108 | if len(args) >= 2: 109 | offset = len(args[1]) - len( 110 | raw_text) # set correct offset relative to command + notename 111 | text, buttons = button_markdown_parser( 112 | args[1], 113 | entities=msg.parse_entities() or msg.parse_caption_entities(), 114 | offset=offset, 115 | ) 116 | if buttons: 117 | data_type = Types.BUTTON_TEXT 118 | else: 119 | data_type = Types.TEXT 120 | 121 | elif msg.reply_to_message: 122 | entities = msg.reply_to_message.parse_entities() 123 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 124 | if len(args) >= 1 and msg.reply_to_message.text: # not caption, text 125 | text, buttons = button_markdown_parser(msgtext, entities=entities) 126 | if buttons: 127 | data_type = Types.BUTTON_TEXT 128 | else: 129 | data_type = Types.TEXT 130 | 131 | elif msg.reply_to_message.sticker: 132 | content = msg.reply_to_message.sticker.file_id 133 | data_type = Types.STICKER 134 | 135 | elif msg.reply_to_message.document: 136 | content = msg.reply_to_message.document.file_id 137 | text, buttons = button_markdown_parser(msgtext, entities=entities) 138 | data_type = Types.DOCUMENT 139 | 140 | elif msg.reply_to_message.photo: 141 | # last elem = best quality 142 | content = msg.reply_to_message.photo[-1].file_id 143 | text, buttons = button_markdown_parser(msgtext, entities=entities) 144 | data_type = Types.PHOTO 145 | 146 | elif msg.reply_to_message.audio: 147 | content = msg.reply_to_message.audio.file_id 148 | text, buttons = button_markdown_parser(msgtext, entities=entities) 149 | data_type = Types.AUDIO 150 | 151 | elif msg.reply_to_message.voice: 152 | content = msg.reply_to_message.voice.file_id 153 | text, buttons = button_markdown_parser(msgtext, entities=entities) 154 | data_type = Types.VOICE 155 | 156 | elif msg.reply_to_message.video: 157 | content = msg.reply_to_message.video.file_id 158 | text, buttons = button_markdown_parser(msgtext, entities=entities) 159 | data_type = Types.VIDEO 160 | 161 | elif msg.reply_to_message.video_note: 162 | content = msg.reply_to_message.video_note.file_id 163 | text, buttons = button_markdown_parser(msgtext, entities=entities) 164 | data_type = Types.VIDEO_NOTE 165 | 166 | return text, data_type, content, buttons 167 | -------------------------------------------------------------------------------- /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, CallbackContext, SUDO_USERS 11 | from tg_bot.modules.disable import DisableAbleCommandHandler 12 | from tg_bot.modules.helper_funcs.extraction import extract_user 13 | 14 | 15 | def about_me(update: Update, context: CallbackContext): 16 | bot, args = context.bot, context.args 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( 29 | user.first_name, escape_markdown(info)), 30 | parse_mode=ParseMode.MARKDOWN) 31 | elif message.reply_to_message: 32 | username = message.reply_to_message.from_user.first_name 33 | update.effective_message.reply_text( 34 | username + " hasn't set an info message about themselves yet!") 35 | else: 36 | update.effective_message.reply_text( 37 | "You haven't set an info message about yourself yet!") 38 | 39 | 40 | def set_about_me(update: Update, context: CallbackContext): 41 | bot = context.bot 42 | message = update.effective_message # type: Optional[Message] 43 | user_id = message.from_user.id 44 | text = message.text 45 | info = text.split( 46 | None, 1 47 | ) # use python's maxsplit to only remove the cmd, hence keeping newlines. 48 | if len(info) == 2: 49 | if len(info[1]) < MAX_MESSAGE_LENGTH // 4: 50 | sql.set_user_me_info(user_id, info[1]) 51 | message.reply_text("Updated your info!") 52 | else: 53 | message.reply_text( 54 | "Your info needs to be under {} characters! You have {}.". 55 | format(MAX_MESSAGE_LENGTH // 4, len(info[1]))) 56 | 57 | 58 | def about_bio(update: Update, context: CallbackContext): 59 | bot, args = context.bot, context.args 60 | message = update.effective_message # type: Optional[Message] 61 | 62 | user_id = extract_user(message, args) 63 | if user_id: 64 | user = bot.get_chat(user_id) 65 | else: 66 | user = message.from_user 67 | 68 | info = sql.get_user_bio(user.id) 69 | 70 | if info: 71 | update.effective_message.reply_text("*{}*:\n{}".format( 72 | user.first_name, escape_markdown(info)), 73 | parse_mode=ParseMode.MARKDOWN) 74 | elif message.reply_to_message: 75 | username = user.first_name 76 | update.effective_message.reply_text( 77 | "{} hasn't had a message set about themselves yet!".format( 78 | username)) 79 | else: 80 | update.effective_message.reply_text( 81 | "You haven't had a bio set about yourself yet!") 82 | 83 | 84 | def set_about_bio(update: Update, context: CallbackContext): 85 | bot = context.bot 86 | message = update.effective_message # type: Optional[Message] 87 | sender = update.effective_user # type: Optional[User] 88 | if message.reply_to_message: 89 | repl_message = message.reply_to_message 90 | user_id = repl_message.from_user.id 91 | if user_id == message.from_user.id: 92 | message.reply_text( 93 | "Ha, you can't set your own bio! You're at the mercy of others here..." 94 | ) 95 | return 96 | elif user_id == bot.id and sender.id not in SUDO_USERS: 97 | message.reply_text( 98 | "Erm... yeah, I only trust sudo users to set my bio.") 99 | return 100 | 101 | text = message.text 102 | bio = text.split( 103 | None, 1 104 | ) # use python's maxsplit to only remove the cmd, hence keeping newlines. 105 | if len(bio) == 2: 106 | if len(bio[1]) < MAX_MESSAGE_LENGTH // 4: 107 | sql.set_user_bio(user_id, bio[1]) 108 | message.reply_text("Updated {}'s bio!".format( 109 | repl_message.from_user.first_name)) 110 | else: 111 | message.reply_text( 112 | "A bio needs to be under {} characters! You tried to set {}." 113 | .format(MAX_MESSAGE_LENGTH // 4, len(bio[1]))) 114 | else: 115 | message.reply_text("Reply to someone's message to set their bio!") 116 | 117 | 118 | def __user_info__(user_id): 119 | bio = html.escape(sql.get_user_bio(user_id) or "") 120 | me = html.escape(sql.get_user_me_info(user_id) or "") 121 | if bio and me: 122 | return "About user:\n{me}\nWhat others say:\n{bio}".format( 123 | me=me, bio=bio) 124 | elif bio: 125 | return "What others say:\n{bio}\n".format(me=me, bio=bio) 126 | elif me: 127 | return "About user:\n{me}" "".format(me=me, bio=bio) 128 | else: 129 | return "" 130 | 131 | 132 | def __gdpr__(user_id): 133 | sql.clear_user_info(user_id) 134 | sql.clear_user_bio(user_id) 135 | 136 | 137 | __help__ = """ 138 | Writing something about yourself is cool, whether to make people know about yourself or \ 139 | promoting your profile. 140 | 141 | All bios are displayed on /info command. 142 | 143 | - /setbio : while replying, will save another user's bio 144 | - /bio: will get your or another user's bio. This cannot be set by yourself. 145 | - /setme : will set your info 146 | - /me: will get your or another user's info 147 | 148 | An example of setting a bio for yourself: 149 | `/setme I work for Telegram`; Bio is set to yourself. 150 | 151 | An example of writing someone else' bio: 152 | Reply to user's message: `/setbio He is such cool person`. 153 | 154 | *Notice:* Do not use /setbio against yourself! 155 | """ 156 | 157 | __mod_name__ = "Bios and Abouts" 158 | 159 | SET_BIO_HANDLER = DisableAbleCommandHandler("setbio", 160 | set_about_bio, 161 | run_async=True) 162 | GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio, run_async=True) 163 | 164 | SET_ABOUT_HANDLER = DisableAbleCommandHandler("setme", 165 | set_about_me, 166 | run_async=True) 167 | GET_ABOUT_HANDLER = DisableAbleCommandHandler("me", about_me, run_async=True) 168 | 169 | dispatcher.add_handler(SET_BIO_HANDLER) 170 | dispatcher.add_handler(GET_BIO_HANDLER) 171 | dispatcher.add_handler(SET_ABOUT_HANDLER) 172 | dispatcher.add_handler(GET_ABOUT_HANDLER) 173 | -------------------------------------------------------------------------------- /tg_bot/modules/global_kicks.py: -------------------------------------------------------------------------------- 1 | import html 2 | from telegram import Message, Update, Bot, User, Chat, ParseMode, ChatPermissions 3 | from typing import List, Optional 4 | from telegram.error import BadRequest, TelegramError 5 | from telegram.ext import run_async, CommandHandler, MessageHandler, Filters 6 | from telegram.utils.helpers import mention_html 7 | from tg_bot import dispatcher, CallbackContext, OWNER_ID, SUDO_USERS, SUPPORT_USERS 8 | from tg_bot.modules.helper_funcs.chat_status import user_admin, is_user_admin 9 | from tg_bot.modules.helper_funcs.extraction import extract_user, extract_user_and_text 10 | from tg_bot.modules.helper_funcs.filters import CustomFilters 11 | from tg_bot.modules.helper_funcs.misc import send_to_list 12 | from tg_bot.modules.sql.users_sql import get_all_chats 13 | import tg_bot.modules.sql.global_kicks_sql as sql 14 | 15 | GKICK_ERRORS = { 16 | "Bots can't add new chat members", "Channel_private", "Chat not found", 17 | "Can't demote chat creator", "Chat_admin_required", 18 | "Group chat was deactivated", 19 | "Method is available for supergroup and channel chats only", 20 | "Method is available only for supergroups", 21 | "Need to be inviter of a user to kick it from a basic group", 22 | "Not enough rights to restrict/unrestrict chat member", "Not in the chat", 23 | "Only the creator of a basic group can kick group administrators", 24 | "Peer_id_invalid", "User is an administrator of the chat", 25 | "User_not_participant", "Reply message not found", "User not found" 26 | } 27 | 28 | 29 | def gkick(update: Update, context: CallbackContext): 30 | bot, args = context.bot, context.args 31 | message = update.effective_message 32 | user_id = extract_user(message, args) 33 | try: 34 | user_chat = bot.get_chat(user_id) 35 | except BadRequest as excp: 36 | if excp.message in GKICK_ERRORS: 37 | pass 38 | else: 39 | message.reply_text( 40 | "User cannot be Globally kicked because: {}".format( 41 | excp.message)) 42 | return 43 | except TelegramError: 44 | pass 45 | 46 | if not user_id or int(user_id) == 777000 or int(user_id) == 1087968824: 47 | message.reply_text("You don't seem to be referring to a user.") 48 | return 49 | if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: 50 | message.reply_text( 51 | "OHHH! Someone's trying to gkick a sudo/support user! *Grabs popcorn*" 52 | ) 53 | return 54 | if int(user_id) == OWNER_ID: 55 | message.reply_text( 56 | "Wow! Someone's so noob that he want to gkick my owner! *Grabs Potato Chips*" 57 | ) 58 | return 59 | 60 | if user_id == bot.id: 61 | message.reply_text("Welp, I'm not gonna to gkick myself!") 62 | return 63 | 64 | chats = get_all_chats() 65 | banner = update.effective_user # type: Optional[User] 66 | send_to_list(bot, SUDO_USERS + SUPPORT_USERS, 67 | "Global Kick" \ 68 | "\n#GKICK" \ 69 | "\nStatus: Enforcing" \ 70 | "\nSudo Admin: {}" \ 71 | "\nUser: {}" \ 72 | "\nID: {}".format(mention_html(banner.id, banner.first_name), 73 | mention_html(user_chat.id, user_chat.first_name), 74 | user_chat.id), 75 | html=True) 76 | message.reply_text("Globally kicking user @{}".format(user_chat.username)) 77 | sql.gkick_user(user_id, user_chat.username, 1) 78 | for chat in chats: 79 | try: 80 | member = bot.get_chat_member(chat.chat_id, user_id) 81 | if member.can_send_messages is False: 82 | bot.unban_chat_member( 83 | chat.chat_id, user_id) # Unban_member = kick (and not ban) 84 | bot.restrict_chat_member( 85 | chat.chat_id, 86 | user_id, 87 | permissions=ChatPermissions(can_send_messages=False)) 88 | else: 89 | bot.unban_chat_member(chat.chat_id, user_id) 90 | except BadRequest as excp: 91 | if excp.message in GKICK_ERRORS: 92 | pass 93 | else: 94 | message.reply_text( 95 | "User cannot be Globally kicked because: {}".format( 96 | excp.message)) 97 | return 98 | except TelegramError: 99 | pass 100 | 101 | 102 | def __user_info__(user_id): 103 | times = sql.get_times(user_id) 104 | 105 | if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: 106 | text = "Globally kicked: No (Immortal)" 107 | else: 108 | text = "Globally kicked: {}" 109 | if times != 0: 110 | text = text.format("Yes (Times: {})".format(times)) 111 | else: 112 | text = text.format("No") 113 | return text 114 | 115 | 116 | def gkickset(update: Update, context: CallbackContext): 117 | bot, args = context.bot, context.args 118 | message = update.effective_message 119 | user_id, value = extract_user_and_text(message, args) 120 | try: 121 | user_chat = bot.get_chat(user_id) 122 | except BadRequest as excp: 123 | if excp.message in GKICK_ERRORS: 124 | pass 125 | else: 126 | message.reply_text("GENERIC ERROR: {}".format(excp.message)) 127 | except TelegramError: 128 | pass 129 | if not user_id: 130 | message.reply_text("You do not seems to be referring to a user") 131 | return 132 | if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: 133 | message.reply_text("SUDOER: Irrelevant") 134 | return 135 | if int(user_id) == OWNER_ID: 136 | message.reply_text("OWNER: Irrelevant") 137 | return 138 | if user_id == bot.id: 139 | message.reply_text("It's me, nigga") 140 | return 141 | 142 | sql.gkick_setvalue(user_id, user_chat.username, int(value)) 143 | return 144 | 145 | 146 | def gkickreset(update: Update, context: CallbackContext): 147 | bot, args = context.bot, context.args 148 | message = update.effective_message 149 | user_id, value = extract_user_and_text(message, args) 150 | try: 151 | user_chat = bot.get_chat(user_id) 152 | except BadRequest as excp: 153 | if excp.message in GKICK_ERRORS: 154 | pass 155 | else: 156 | message.reply_text("GENERIC ERROR: {}".format(excp.message)) 157 | except TelegramError: 158 | pass 159 | if not user_id: 160 | message.reply_text("You do not seems to be referring to a user") 161 | return 162 | if int(user_id) in SUDO_USERS or int(user_id) in SUPPORT_USERS: 163 | message.reply_text("SUDOER: Irrelevant") 164 | return 165 | if int(user_id) == OWNER_ID: 166 | message.reply_text("OWNER: Irrelevant") 167 | return 168 | if user_id == bot.id: 169 | message.reply_text("It's me, nigga") 170 | return 171 | 172 | sql.gkick_reset(user_id) 173 | return 174 | 175 | 176 | GKICK_HANDLER = CommandHandler("gkick", 177 | gkick, 178 | run_async=True, 179 | filters=CustomFilters.sudo_filter 180 | | CustomFilters.support_filter) 181 | SET_HANDLER = CommandHandler("gkickset", 182 | gkickset, 183 | run_async=True, 184 | filters=Filters.user(OWNER_ID)) 185 | RESET_HANDLER = CommandHandler("gkickreset", 186 | gkickreset, 187 | run_async=True, 188 | filters=Filters.user(OWNER_ID)) 189 | 190 | dispatcher.add_handler(GKICK_HANDLER) 191 | dispatcher.add_handler(SET_HANDLER) 192 | dispatcher.add_handler(RESET_HANDLER) 193 | -------------------------------------------------------------------------------- /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, Filters 12 | from telegram.utils.helpers import escape_markdown 13 | 14 | from tg_bot import dispatcher, CallbackContext, 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 | def loggable(func): 19 | @wraps(func) 20 | def log_action(update: Update, context: CallbackContext, *args, 21 | **kwargs): 22 | result = func(update, context, *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( 37 | "%s was set as loggable, but had no return statement.", 38 | func) 39 | 40 | return result 41 | 42 | return log_action 43 | 44 | def send_log(bot: Bot, log_chat_id: str, orig_chat_id: str, result: str): 45 | try: 46 | bot.send_message(log_chat_id, result, parse_mode=ParseMode.HTML) 47 | except BadRequest as excp: 48 | if excp.message == "Chat not found": 49 | bot.send_message( 50 | orig_chat_id, 51 | "This log channel has been deleted - unsetting.") 52 | sql.stop_chat_logging(orig_chat_id) 53 | else: 54 | LOGGER.warning(excp.message) 55 | LOGGER.warning(result) 56 | LOGGER.exception("Could not parse") 57 | 58 | bot.send_message( 59 | log_chat_id, result + 60 | "\n\nFormatting has been disabled due to an unexpected error." 61 | ) 62 | 63 | @user_admin 64 | def logging(update: Update, context: CallbackContext): 65 | bot = context.bot 66 | message = update.effective_message # type: Optional[Message] 67 | chat = update.effective_chat # type: Optional[Chat] 68 | 69 | log_channel = sql.get_chat_log_channel(chat.id) 70 | if log_channel: 71 | log_channel_info = bot.get_chat(log_channel) 72 | message.reply_text( 73 | "This group has all it's logs sent to: {} (`{}`)".format( 74 | escape_markdown(log_channel_info.title), log_channel), 75 | parse_mode=ParseMode.MARKDOWN) 76 | 77 | else: 78 | message.reply_text("No log channel has been set for this group!") 79 | 80 | @user_admin 81 | def setlog(update: Update, context: CallbackContext): 82 | bot = context.bot 83 | message = update.effective_message # type: Optional[Message] 84 | chat = update.effective_chat # type: Optional[Chat] 85 | if chat.type == chat.CHANNEL: 86 | message.reply_text( 87 | "Now, forward the /setlog to the group you want to tie this channel to!" 88 | ) 89 | 90 | elif message.forward_from_chat: 91 | sql.set_chat_log_channel(chat.id, message.forward_from_chat.id) 92 | try: 93 | message.delete() 94 | except BadRequest as excp: 95 | if excp.message == "Message to delete not found": 96 | pass 97 | else: 98 | LOGGER.exception( 99 | "Error deleting message in log channel. Should work anyway though." 100 | ) 101 | 102 | try: 103 | bot.send_message( 104 | message.forward_from_chat.id, 105 | "This channel has been set as the log channel for {}.". 106 | format(chat.title or chat.first_name)) 107 | except Unauthorized as excp: 108 | if excp.message == "Forbidden: bot is not a member of the channel chat": 109 | bot.send_message(chat.id, "Successfully set log channel!") 110 | else: 111 | LOGGER.exception("ERROR in setting the log channel.") 112 | 113 | bot.send_message(chat.id, "Successfully set log channel!") 114 | 115 | else: 116 | message.reply_text("The steps to set a log channel are:\n" 117 | " - add bot to the desired channel\n" 118 | " - send /setlog to the channel\n" 119 | " - forward the /setlog to the group\n") 120 | 121 | @user_admin 122 | def unsetlog(update: Update, context: CallbackContext): 123 | bot = context.bot 124 | message = update.effective_message # type: Optional[Message] 125 | chat = update.effective_chat # type: Optional[Chat] 126 | 127 | log_channel = sql.stop_chat_logging(chat.id) 128 | if log_channel: 129 | bot.send_message( 130 | log_channel, 131 | "Channel has been unlinked from {}".format(chat.title)) 132 | message.reply_text("Log channel has been un-set.") 133 | 134 | else: 135 | message.reply_text("No log channel has been set yet!") 136 | 137 | def __stats__(): 138 | return "{} log channels set.".format(sql.num_logchannels()) 139 | 140 | def __migrate__(old_chat_id, new_chat_id): 141 | sql.migrate_chat(old_chat_id, new_chat_id) 142 | 143 | def __chat_settings__(chat_id, user_id): 144 | log_channel = sql.get_chat_log_channel(chat_id) 145 | if log_channel: 146 | log_channel_info = dispatcher.bot.get_chat(log_channel) 147 | return "This group has all it's logs sent to: {} (`{}`)".format( 148 | escape_markdown(log_channel_info.title), log_channel) 149 | return "No log channel is set for this group!" 150 | 151 | __help__ = """ 152 | Recent actions are nice, but they don't help you log every action taken by the bot. This is why you need log channels! 153 | 154 | Log channels can help you keep track of exactly what the other admins are doing. \ 155 | Bans, Mutes, warns, notes - everything can be moderated. 156 | 157 | *Admin only:* 158 | - /logchannel: get log channel info 159 | - /setlog: set the log channel. 160 | - /unsetlog: unset the log channel. 161 | 162 | Setting the log channel is done by: 163 | - Add the bot to your channel, as an admin. This is done via the "add administrators" tab. 164 | - Send /setlog to your channel. 165 | - Forward the /setlog command to the group you wish to be logged. 166 | - Congratulations! All is set! 167 | """ 168 | 169 | __mod_name__ = "Log Channels" 170 | 171 | LOG_HANDLER = CommandHandler("logchannel", 172 | logging, 173 | run_async=True, 174 | filters=Filters.chat_type.groups) 175 | SET_LOG_HANDLER = CommandHandler("setlog", 176 | setlog, 177 | run_async=True, 178 | filters=Filters.chat_type.groups) 179 | UNSET_LOG_HANDLER = CommandHandler("unsetlog", 180 | unsetlog, 181 | run_async=True, 182 | filters=Filters.chat_type.groups) 183 | 184 | dispatcher.add_handler(LOG_HANDLER) 185 | dispatcher.add_handler(SET_LOG_HANDLER) 186 | dispatcher.add_handler(UNSET_LOG_HANDLER) 187 | 188 | else: 189 | # run anyway if module not loaded 190 | def loggable(func): 191 | return func 192 | -------------------------------------------------------------------------------- /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, MessageHandler, Filters 6 | from telegram.utils.helpers import escape_markdown 7 | 8 | from tg_bot import dispatcher, CallbackContext 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 | message = update.effective_message 42 | if super().check_update(update): 43 | # Should be safe since check_update passed. 44 | command = update.effective_message.text_html.split( 45 | None, 1)[0][1:].split('@')[0] 46 | 47 | # disabled, admincmd, user admin 48 | if sql.is_command_disabled(chat.id, command): 49 | if command in ADMIN_CMDS and is_user_admin(chat, user.id): 50 | pass 51 | else: 52 | return None 53 | 54 | args = message.text.split()[1:] 55 | filter_result = self.filters(update) 56 | if filter_result: 57 | return args, filter_result 58 | else: 59 | return False 60 | 61 | return None 62 | 63 | class DisableAbleRegexHandler(MessageHandler): 64 | def __init__(self, pattern, callback, friendly="", **kwargs): 65 | super().__init__(Filters.regex(pattern), callback, **kwargs) 66 | DISABLE_OTHER.append(friendly or pattern) 67 | self.friendly = friendly or pattern 68 | 69 | def check_update(self, update): 70 | chat = update.effective_chat 71 | return super().check_update( 72 | update) and not sql.is_command_disabled( 73 | chat.id, self.friendly) 74 | 75 | @user_admin 76 | def disable(update: Update, context: CallbackContext): 77 | bot, args = context.bot, context.args 78 | chat = update.effective_chat # type: Optional[Chat] 79 | if len(args) >= 1: 80 | disable_cmd = args[0] 81 | if disable_cmd.startswith(CMD_STARTERS): 82 | disable_cmd = disable_cmd[1:] 83 | 84 | if disable_cmd in set(DISABLE_CMDS + DISABLE_OTHER): 85 | sql.disable_command(chat.id, disable_cmd) 86 | update.effective_message.reply_text( 87 | "Disabled the use of `{}`".format(disable_cmd), 88 | parse_mode=ParseMode.MARKDOWN) 89 | else: 90 | update.effective_message.reply_text( 91 | "That command can't be disabled") 92 | 93 | else: 94 | update.effective_message.reply_text("What should I disable?") 95 | 96 | @user_admin 97 | def enable(update: Update, context: CallbackContext): 98 | bot, args = context.bot, context.args 99 | chat = update.effective_chat # type: Optional[Chat] 100 | if len(args) >= 1: 101 | enable_cmd = args[0] 102 | if enable_cmd.startswith(CMD_STARTERS): 103 | enable_cmd = enable_cmd[1:] 104 | 105 | if sql.enable_command(chat.id, enable_cmd): 106 | update.effective_message.reply_text( 107 | "Enabled the use of `{}`".format(enable_cmd), 108 | parse_mode=ParseMode.MARKDOWN) 109 | else: 110 | update.effective_message.reply_text("Is that even disabled?") 111 | 112 | else: 113 | update.effective_message.reply_text("What should I enable?") 114 | 115 | @user_admin 116 | def list_cmds(update: Update, context: CallbackContext): 117 | bot = context.bot 118 | if DISABLE_CMDS + DISABLE_OTHER: 119 | result = "" 120 | for cmd in set(DISABLE_CMDS + DISABLE_OTHER): 121 | result += " - `{}`\n".format(escape_markdown(cmd)) 122 | update.effective_message.reply_text( 123 | "The following commands are toggleable:\n{}".format(result), 124 | parse_mode=ParseMode.MARKDOWN) 125 | else: 126 | update.effective_message.reply_text("No commands can be disabled.") 127 | 128 | # do not async 129 | def build_curr_disabled(chat_id: Union[str, int]) -> str: 130 | disabled = sql.get_all_disabled(chat_id) 131 | if not disabled: 132 | return "No commands are disabled!" 133 | 134 | result = "" 135 | for cmd in disabled: 136 | result += " - `{}`\n".format(escape_markdown(cmd)) 137 | return "The following commands are currently restricted:\n{}".format( 138 | result) 139 | 140 | def commands(update: Update, context: CallbackContext): 141 | bot = context.bot 142 | chat = update.effective_chat 143 | update.effective_message.reply_text(build_curr_disabled(chat.id), 144 | parse_mode=ParseMode.MARKDOWN) 145 | 146 | def __stats__(): 147 | return "{} disabled items, across {} chats.".format( 148 | sql.num_disabled(), sql.num_chats()) 149 | 150 | def __migrate__(old_chat_id, new_chat_id): 151 | sql.migrate_chat(old_chat_id, new_chat_id) 152 | 153 | def __chat_settings__(chat_id, user_id): 154 | return build_curr_disabled(chat_id) 155 | 156 | __mod_name__ = "Command disabling" 157 | 158 | __help__ = """ 159 | Not everyone wants every feature that the bot offers. Some commands are best \ 160 | left unused; to avoid spam and abuse. 161 | 162 | This allows you to disable some commonly used commands, so noone can use them. \ 163 | It'll also allow you to autodelete them, stopping people from bluetexting. 164 | 165 | - /cmds: check the current status of disabled commands 166 | 167 | *Admin only:* 168 | - /enable : enable that command 169 | - /disable : disable that command 170 | - /listcmds: list all possible toggleable commands 171 | """ 172 | 173 | DISABLE_HANDLER = CommandHandler("disable", 174 | disable, 175 | filters=Filters.chat_type.groups, 176 | run_async=True) 177 | ENABLE_HANDLER = CommandHandler("enable", 178 | enable, 179 | filters=Filters.chat_type.groups, 180 | run_async=True) 181 | COMMANDS_HANDLER = CommandHandler(["cmds", "disabled"], 182 | commands, 183 | filters=Filters.chat_type.groups, 184 | run_async=True) 185 | TOGGLE_HANDLER = CommandHandler("listcmds", 186 | list_cmds, 187 | filters=Filters.chat_type.groups, 188 | run_async=True) 189 | 190 | dispatcher.add_handler(DISABLE_HANDLER) 191 | dispatcher.add_handler(ENABLE_HANDLER) 192 | dispatcher.add_handler(COMMANDS_HANDLER) 193 | dispatcher.add_handler(TOGGLE_HANDLER) 194 | 195 | else: 196 | DisableAbleCommandHandler = CommandHandler 197 | DisableAbleRegexHandler = RegexHandler 198 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/cust_filters_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, Boolean, Integer, distinct, func 4 | 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 | 20 | has_buttons = Column(Boolean, nullable=False, default=False) 21 | # NOTE: Here for legacy purposes, to ensure older filters don't mess up. 22 | has_markdown = Column(Boolean, nullable=False, default=False) 23 | 24 | def __init__(self, 25 | chat_id, 26 | keyword, 27 | reply, 28 | is_sticker=False, 29 | is_document=False, 30 | is_image=False, 31 | is_audio=False, 32 | is_voice=False, 33 | is_video=False, 34 | has_buttons=False): 35 | self.chat_id = str(chat_id) # ensure string 36 | self.keyword = keyword 37 | self.reply = reply 38 | self.is_sticker = is_sticker 39 | self.is_document = is_document 40 | self.is_image = is_image 41 | self.is_audio = is_audio 42 | self.is_voice = is_voice 43 | self.is_video = is_video 44 | self.has_buttons = has_buttons 45 | self.has_markdown = True 46 | 47 | def __repr__(self): 48 | return "" % self.chat_id 49 | 50 | def __eq__(self, other): 51 | return bool( 52 | isinstance(other, CustomFilters) and self.chat_id == other.chat_id 53 | and self.keyword == other.keyword) 54 | 55 | 56 | class Buttons(BASE): 57 | __tablename__ = "cust_filter_urls" 58 | id = Column(Integer, primary_key=True, autoincrement=True) 59 | chat_id = Column(String(14), primary_key=True) 60 | keyword = Column(UnicodeText, primary_key=True) 61 | name = Column(UnicodeText, nullable=False) 62 | url = Column(UnicodeText, nullable=False) 63 | same_line = Column(Boolean, default=False) 64 | 65 | def __init__(self, chat_id, keyword, name, url, same_line=False): 66 | self.chat_id = str(chat_id) 67 | self.keyword = keyword 68 | self.name = name 69 | self.url = url 70 | self.same_line = same_line 71 | 72 | 73 | CustomFilters.__table__.create(checkfirst=True) 74 | Buttons.__table__.create(checkfirst=True) 75 | 76 | CUST_FILT_LOCK = threading.RLock() 77 | BUTTON_LOCK = threading.RLock() 78 | CHAT_FILTERS = {} 79 | 80 | 81 | def get_all_filters(): 82 | try: 83 | return SESSION.query(CustomFilters).all() 84 | finally: 85 | SESSION.close() 86 | 87 | 88 | def add_filter(chat_id, 89 | keyword, 90 | reply, 91 | is_sticker=False, 92 | is_document=False, 93 | is_image=False, 94 | is_audio=False, 95 | is_voice=False, 96 | is_video=False, 97 | buttons=None): 98 | global CHAT_FILTERS 99 | 100 | if buttons is None: 101 | buttons = [] 102 | 103 | with CUST_FILT_LOCK: 104 | prev = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 105 | if prev: 106 | with BUTTON_LOCK: 107 | prev_buttons = SESSION.query(Buttons).filter( 108 | Buttons.chat_id == str(chat_id), 109 | Buttons.keyword == keyword).all() 110 | for btn in prev_buttons: 111 | SESSION.delete(btn) 112 | SESSION.delete(prev) 113 | 114 | filt = CustomFilters(str(chat_id), keyword, reply, is_sticker, 115 | is_document, is_image, is_audio, is_voice, 116 | is_video, bool(buttons)) 117 | 118 | if keyword not in CHAT_FILTERS.get(str(chat_id), []): 119 | CHAT_FILTERS[str(chat_id)] = sorted( 120 | CHAT_FILTERS.get(str(chat_id), []) + [keyword], 121 | key=lambda x: (-len(x), x)) 122 | 123 | SESSION.add(filt) 124 | SESSION.commit() 125 | 126 | for b_name, url, same_line in buttons: 127 | add_note_button_to_db(chat_id, keyword, b_name, url, same_line) 128 | 129 | 130 | def remove_filter(chat_id, keyword): 131 | global CHAT_FILTERS 132 | with CUST_FILT_LOCK: 133 | filt = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 134 | if filt: 135 | if keyword in CHAT_FILTERS.get(str(chat_id), []): # Sanity check 136 | CHAT_FILTERS.get(str(chat_id), []).remove(keyword) 137 | 138 | with BUTTON_LOCK: 139 | prev_buttons = SESSION.query(Buttons).filter( 140 | Buttons.chat_id == str(chat_id), 141 | Buttons.keyword == keyword).all() 142 | for btn in prev_buttons: 143 | SESSION.delete(btn) 144 | 145 | SESSION.delete(filt) 146 | SESSION.commit() 147 | return True 148 | 149 | SESSION.close() 150 | return False 151 | 152 | 153 | def get_chat_triggers(chat_id): 154 | return CHAT_FILTERS.get(str(chat_id), set()) 155 | 156 | 157 | def get_chat_filters(chat_id): 158 | try: 159 | return SESSION.query(CustomFilters).filter( 160 | CustomFilters.chat_id == str(chat_id)).order_by( 161 | func.length(CustomFilters.keyword).desc()).order_by( 162 | CustomFilters.keyword.asc()).all() 163 | finally: 164 | SESSION.close() 165 | 166 | 167 | def get_filter(chat_id, keyword): 168 | try: 169 | return SESSION.query(CustomFilters).get((str(chat_id), keyword)) 170 | finally: 171 | SESSION.close() 172 | 173 | 174 | def add_note_button_to_db(chat_id, keyword, b_name, url, same_line): 175 | with BUTTON_LOCK: 176 | button = Buttons(chat_id, keyword, b_name, url, same_line) 177 | SESSION.add(button) 178 | SESSION.commit() 179 | 180 | 181 | def get_buttons(chat_id, keyword): 182 | try: 183 | return SESSION.query(Buttons).filter( 184 | Buttons.chat_id == str(chat_id), 185 | Buttons.keyword == keyword).order_by(Buttons.id).all() 186 | finally: 187 | SESSION.close() 188 | 189 | 190 | def num_filters(): 191 | try: 192 | return SESSION.query(CustomFilters).count() 193 | finally: 194 | SESSION.close() 195 | 196 | 197 | def num_chats(): 198 | try: 199 | return SESSION.query(func.count(distinct( 200 | CustomFilters.chat_id))).scalar() 201 | finally: 202 | SESSION.close() 203 | 204 | 205 | def __load_chat_filters(): 206 | global CHAT_FILTERS 207 | try: 208 | chats = SESSION.query(CustomFilters.chat_id).distinct().all() 209 | for (chat_id, ) in chats: # remove tuple by ( ,) 210 | CHAT_FILTERS[chat_id] = [] 211 | 212 | all_filters = SESSION.query(CustomFilters).all() 213 | for x in all_filters: 214 | CHAT_FILTERS[x.chat_id] += [x.keyword] 215 | 216 | CHAT_FILTERS = { 217 | x: sorted(set(y), key=lambda i: (-len(i), i)) 218 | for x, y in CHAT_FILTERS.items() 219 | } 220 | 221 | finally: 222 | SESSION.close() 223 | 224 | 225 | def migrate_chat(old_chat_id, new_chat_id): 226 | with CUST_FILT_LOCK: 227 | chat_filters = SESSION.query(CustomFilters).filter( 228 | CustomFilters.chat_id == str(old_chat_id)).all() 229 | for filt in chat_filters: 230 | filt.chat_id = str(new_chat_id) 231 | SESSION.commit() 232 | CHAT_FILTERS[str(new_chat_id)] = CHAT_FILTERS[str(old_chat_id)] 233 | del CHAT_FILTERS[str(old_chat_id)] 234 | 235 | with BUTTON_LOCK: 236 | chat_buttons = SESSION.query(Buttons).filter( 237 | Buttons.chat_id == str(old_chat_id)).all() 238 | for btn in chat_buttons: 239 | btn.chat_id = str(new_chat_id) 240 | SESSION.commit() 241 | 242 | 243 | __load_chat_filters() 244 | -------------------------------------------------------------------------------- /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 | emoji = Column(Boolean, default=False) 28 | bigemoji = Column(Boolean, default=False) 29 | anonchannel = Column(Boolean, default=False) 30 | 31 | def __init__(self, chat_id): 32 | self.chat_id = str(chat_id) # ensure string 33 | self.audio = False 34 | self.voice = False 35 | self.contact = False 36 | self.video = False 37 | self.videonote = False 38 | self.document = False 39 | self.photo = False 40 | self.sticker = False 41 | self.gif = False 42 | self.url = False 43 | self.bots = False 44 | self.forward = False 45 | self.game = False 46 | self.location = False 47 | self.emoji = False 48 | self.bigemoji = False 49 | self.anonchannel = False 50 | 51 | def __repr__(self): 52 | return "" % self.chat_id 53 | 54 | 55 | class Restrictions(BASE): 56 | __tablename__ = "restrictions" 57 | chat_id = Column(String(14), primary_key=True) 58 | # Booleans are for "is this restricted", _NOT_ "is this allowed" 59 | messages = Column(Boolean, default=False) 60 | media = Column(Boolean, default=False) 61 | other = Column(Boolean, default=False) 62 | preview = Column(Boolean, default=False) 63 | 64 | def __init__(self, chat_id): 65 | self.chat_id = str(chat_id) # ensure string 66 | self.messages = False 67 | self.media = False 68 | self.other = False 69 | self.preview = False 70 | 71 | def __repr__(self): 72 | return "" % self.chat_id 73 | 74 | 75 | Permissions.__table__.create(checkfirst=True) 76 | Restrictions.__table__.create(checkfirst=True) 77 | 78 | PERM_LOCK = threading.RLock() 79 | RESTR_LOCK = threading.RLock() 80 | 81 | 82 | def init_permissions(chat_id, reset=False): 83 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 84 | if reset: 85 | SESSION.delete(curr_perm) 86 | SESSION.flush() 87 | perm = Permissions(str(chat_id)) 88 | SESSION.add(perm) 89 | SESSION.commit() 90 | return perm 91 | 92 | 93 | def init_restrictions(chat_id, reset=False): 94 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 95 | if reset: 96 | SESSION.delete(curr_restr) 97 | SESSION.flush() 98 | restr = Restrictions(str(chat_id)) 99 | SESSION.add(restr) 100 | SESSION.commit() 101 | return restr 102 | 103 | 104 | def update_lock(chat_id, lock_type, locked): 105 | with PERM_LOCK: 106 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 107 | if not curr_perm: 108 | curr_perm = init_permissions(chat_id) 109 | 110 | if lock_type == "audio": 111 | curr_perm.audio = locked 112 | elif lock_type == "voice": 113 | curr_perm.voice = locked 114 | elif lock_type == "contact": 115 | curr_perm.contact = locked 116 | elif lock_type == "video": 117 | curr_perm.video = locked 118 | elif lock_type == "videonote": 119 | curr_perm.videonote = locked 120 | elif lock_type == "document": 121 | curr_perm.document = locked 122 | elif lock_type == "photo": 123 | curr_perm.photo = locked 124 | elif lock_type == "sticker": 125 | curr_perm.sticker = locked 126 | elif lock_type == "gif": 127 | curr_perm.gif = locked 128 | elif lock_type == 'url': 129 | curr_perm.url = locked 130 | elif lock_type == 'bots': 131 | curr_perm.bots = locked 132 | elif lock_type == 'forward': 133 | curr_perm.forward = locked 134 | elif lock_type == 'game': 135 | curr_perm.game = locked 136 | elif lock_type == 'location': 137 | curr_perm.location = locked 138 | elif lock_type == 'emoji': 139 | curr_perm.emoji = locked 140 | elif lock_type == 'bigemoji': 141 | curr_perm.bigemoji = locked 142 | elif lock_type == 'anonchannel': 143 | curr_perm.anonchannel = locked 144 | 145 | SESSION.add(curr_perm) 146 | SESSION.commit() 147 | 148 | 149 | def update_restriction(chat_id, restr_type, locked): 150 | with RESTR_LOCK: 151 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 152 | if not curr_restr: 153 | curr_restr = init_restrictions(chat_id) 154 | 155 | if restr_type == "messages": 156 | curr_restr.messages = locked 157 | elif restr_type == "media": 158 | curr_restr.media = locked 159 | elif restr_type == "other": 160 | curr_restr.other = locked 161 | elif restr_type == "previews": 162 | curr_restr.preview = locked 163 | elif restr_type == "all": 164 | curr_restr.messages = locked 165 | curr_restr.media = locked 166 | curr_restr.other = locked 167 | curr_restr.preview = locked 168 | SESSION.add(curr_restr) 169 | SESSION.commit() 170 | 171 | 172 | def is_locked(chat_id, lock_type): 173 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 174 | SESSION.close() 175 | 176 | if not curr_perm: 177 | return False 178 | 179 | elif lock_type == "sticker": 180 | return curr_perm.sticker 181 | elif lock_type == "photo": 182 | return curr_perm.photo 183 | elif lock_type == "audio": 184 | return curr_perm.audio 185 | elif lock_type == "voice": 186 | return curr_perm.voice 187 | elif lock_type == "contact": 188 | return curr_perm.contact 189 | elif lock_type == "video": 190 | return curr_perm.video 191 | elif lock_type == "videonote": 192 | return curr_perm.videonote 193 | elif lock_type == "document": 194 | return curr_perm.document 195 | elif lock_type == "gif": 196 | return curr_perm.gif 197 | elif lock_type == "url": 198 | return curr_perm.url 199 | elif lock_type == "bots": 200 | return curr_perm.bots 201 | elif lock_type == "forward": 202 | return curr_perm.forward 203 | elif lock_type == "game": 204 | return curr_perm.game 205 | elif lock_type == "location": 206 | return curr_perm.location 207 | elif lock_type == "emoji": 208 | return curr_perm.emoji 209 | elif lock_type == "bigemoji": 210 | return curr_perm.bigemoji 211 | elif lock_type == "anonchannel": 212 | return curr_perm.anonchannel 213 | 214 | 215 | def is_restr_locked(chat_id, lock_type): 216 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 217 | SESSION.close() 218 | 219 | if not curr_restr: 220 | return False 221 | 222 | if lock_type == "messages": 223 | return curr_restr.messages 224 | elif lock_type == "media": 225 | return curr_restr.media 226 | elif lock_type == "other": 227 | return curr_restr.other 228 | elif lock_type == "previews": 229 | return curr_restr.preview 230 | elif lock_type == "all": 231 | return curr_restr.messages and curr_restr.media and curr_restr.other and curr_restr.preview 232 | 233 | 234 | def get_locks(chat_id): 235 | try: 236 | return SESSION.query(Permissions).get(str(chat_id)) 237 | finally: 238 | SESSION.close() 239 | 240 | 241 | def get_restr(chat_id): 242 | try: 243 | return SESSION.query(Restrictions).get(str(chat_id)) 244 | finally: 245 | SESSION.close() 246 | 247 | 248 | def migrate_chat(old_chat_id, new_chat_id): 249 | with PERM_LOCK: 250 | perms = SESSION.query(Permissions).get(str(old_chat_id)) 251 | if perms: 252 | perms.chat_id = str(new_chat_id) 253 | SESSION.commit() 254 | 255 | with RESTR_LOCK: 256 | rest = SESSION.query(Restrictions).get(str(old_chat_id)) 257 | if rest: 258 | rest.chat_id = str(new_chat_id) 259 | SESSION.commit() 260 | -------------------------------------------------------------------------------- /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 into becoming a basis for modular 5 | bots aiming to provide simple plugin expansion via a simple drag and drop. 6 | 7 | For questions regarding creating your own bot, please head to [this chat](https://t.me/bot_workshop) where you'll find a 8 | group of volunteers to help. We'll also help when a database schema changes, and some table column needs to be 9 | modified/added (this info can also be found in the commit messages) 10 | 11 | 12 | Join the [news channel](https://t.me/bot_workshop_channel) if you just want to stay in the loop about new features or 13 | announcements. 14 | 15 | Our bots and we can also be found moderating the [bot support group](https://t.me/bot_workshop) aimed at providing help 16 | setting up our bots in your chats (*not* for bot clones). 17 | Feel free to join to report bugs, and stay in the loop on the status of the bot development. 18 | 19 | Note to maintainers that all schema changes will be found in the commit messages, and its their responsibility to read any new commits. 20 | 21 | ## Starting the bot. 22 | 23 | Once you've setup your database and your configuration (see below) is complete, simply run: 24 | 25 | `python3 -m tg_bot` 26 | 27 | 28 | ## Setting up the bot (Read this before trying to use!): 29 | Please make sure to use python3.6, as I cannot guarantee everything will work as expected on older python versions! 30 | This is because markdown parsing is done by iterating through a dict, which are ordered by default in 3.6. 31 | 32 | ### Configuration 33 | 34 | There are two possible ways of configuring your bot: a config.py file, or ENV variables. 35 | 36 | The prefered version is to use a `config.py` file, as it makes it easier to see all your settings grouped together. 37 | This file should be placed in your `tg_bot` folder, alongside the `__main__.py` file . 38 | 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 39 | your other settings. 40 | 41 | It is recommended to import sample_config and extend the Config class, as this will ensure your config contains all 42 | defaults set in the sample_config, hence making it easier to upgrade. 43 | 44 | An example `config.py` file could be: 45 | ``` 46 | from tg_bot.sample_config import Config 47 | 48 | 49 | class Development(Config): 50 | OWNER_ID = 254318997 # my telegram ID 51 | OWNER_USERNAME = "SonOfLars" # my telegram username 52 | API_KEY = "your bot api key" # my api key, as provided by the botfather 53 | SQLALCHEMY_DATABASE_URI = 'postgresql://username:password@localhost:5432/database' # sample db credentials 54 | MESSAGE_DUMP = '-1234567890' # some group chat that your bot is a member of 55 | USE_MESSAGE_DUMP = True 56 | SUDO_USERS = [18673980, 83489514] # List of id's for users which have sudo access to the bot. 57 | LOAD = [] 58 | NO_LOAD = ['translation'] 59 | ``` 60 | 61 | If you can't have a config.py file (EG on heroku), it is also possible to use environment variables. 62 | The following env variables are supported: 63 | - `ENV`: Setting this to ANYTHING will enable env variables 64 | 65 | - `TOKEN`: Your bot token, as a string. 66 | - `OWNER_ID`: An integer of consisting of your owner ID 67 | - `OWNER_USERNAME`: Your username 68 | 69 | - `DATABASE_URL`: Your database URL 70 | - `MESSAGE_DUMP`: optional: a chat where your replied saved messages are stored, to stop people deleting their old 71 | - `LOAD`: Space separated list of modules you would like to load 72 | - `NO_LOAD`: Space separated list of modules you would like NOT to load 73 | - `WEBHOOK`: Setting this to ANYTHING will enable webhooks when in env mode 74 | messages 75 | - `URL`: The URL your webhook should connect to (only needed for webhook mode) 76 | 77 | - `SUDO_USERS`: A space separated list of user_ids which should be considered sudo users 78 | - `SUPPORT_USERS`: A space separated list of user_ids which should be considered support users (can gban/ungban, 79 | nothing else) 80 | - `WHITELIST_USERS`: A space separated list of user_ids which should be considered whitelisted - they can't be banned. 81 | - `DONATION_LINK`: Optional: link where you would like to receive donations. 82 | - `CERT_PATH`: Path to your webhook certificate 83 | - `PORT`: Port to use for your webhooks 84 | - `DEL_CMDS`: Whether to delete commands from users which don't have rights to use that command 85 | - `STRICT_GBAN`: Enforce gbans across new groups as well as old groups. When a gbanned user talks, he will be banned. 86 | - `WORKERS`: Number of threads to use. 8 is the recommended (and default) amount, but your experience may vary. 87 | __Note__ that going crazy with more threads wont necessarily speed up your bot, given the large amount of sql data 88 | accesses, and the way python asynchronous calls work. 89 | - `BAN_STICKER`: Which sticker to use when banning people. 90 | - `ALLOW_EXCL`: Whether to allow using exclamation marks ! for commands as well as /. 91 | 92 | ### Python dependencies (Without Docker) 93 | 94 | Install the necessary python dependencies by moving to the project directory and running: 95 | 96 | `pip3 install -r requirements.txt`. 97 | 98 | This will install all necessary python packages. 99 | 100 | ### Database (Without Docker) 101 | 102 | If you wish to use a database-dependent module (eg: locks, notes, userinfo, users, filters, welcomes), 103 | you'll need to have a database installed on your system. I use postgres, so I recommend using it for optimal compatibility. 104 | 105 | In the case of postgres, this is how you would set up a the database on a debian/ubuntu system. Other distributions may vary. 106 | 107 | - install postgresql: 108 | 109 | `sudo apt-get update && sudo apt-get install postgresql` 110 | 111 | - change to the postgres user: 112 | 113 | `sudo su - postgres` 114 | 115 | - create a new database user (change YOUR_USER appropriately): 116 | 117 | `createuser -P -s -e YOUR_USER` 118 | 119 | This will be followed by you needing to input your password. 120 | 121 | - create a new database table: 122 | 123 | `createdb -O YOUR_USER YOUR_DB_NAME` 124 | 125 | Change YOUR_USER and YOUR_DB_NAME appropriately. 126 | 127 | - finally: 128 | 129 | `psql YOUR_DB_NAME -h YOUR_HOST YOUR_USER` 130 | 131 | This will allow you to connect to your database via your terminal. 132 | By default, YOUR_HOST should be 0.0.0.0:5432. 133 | 134 | You should now be able to build your database URI. This will be: 135 | 136 | `sqldbtype://username:pw@hostname:port/db_name` 137 | 138 | Replace sqldbtype with whichever db youre using (eg postgres, mysql, sqllite, etc) 139 | repeat for your username, password, hostname (localhost?), port (5432?), and db name. 140 | 141 | ### Docker 142 | 143 | Alternatively, you can also use docker to start the bot. It comes with everything needed 144 | to run the bot so the setup is simpler. 145 | 146 | You should change database URI in `config.py` to 147 | ``` 148 | postgresql://postgres:changethis@tgbot_db:5432/tgbot 149 | ``` 150 | 151 | It is highly recommended to change the password to something else in `docker-compose.yml` and 152 | update it accordingly in `config.py`. 153 | 154 | After configuration is done, build the docker image: 155 | ``` 156 | docker compose build 157 | ``` 158 | 159 | Then, run the bot with 160 | ``` 161 | docker compose up 162 | ``` 163 | 164 | Incase you've changed any files, you'll need to rebuild the image. In some cases where rebuild 165 | may not work, try removing the image first, then build it again. 166 | 167 | ``` 168 | docker compose rm && docker compose build 169 | ``` 170 | 171 | If you want to keep the bot running in the background, simply pass the `--detach` flag. 172 | ``` 173 | docker compose up --detach 174 | ``` 175 | 176 | ## Modules 177 | ### Setting load order. 178 | 179 | The module load order can be changed via the `LOAD` and `NO_LOAD` configuration settings. 180 | These should both represent lists. 181 | 182 | If `LOAD` is an empty list, all modules in `modules/` will be selected for loading by default. 183 | 184 | If `NO_LOAD` is not present, or is an empty list, all modules selected for loading will be loaded. 185 | 186 | If a module is in both `LOAD` and `NO_LOAD`, the module will not be loaded - `NO_LOAD` takes priority. 187 | 188 | ### Creating your own modules. 189 | 190 | Creating a module has been simplified as much as possible - but do not hesitate to suggest further simplification. 191 | 192 | All that is needed is that your .py file be in the modules folder. 193 | 194 | To add commands, make sure to import the dispatcher via 195 | 196 | `from tg_bot import dispatcher`. 197 | 198 | You can then add commands using the usual 199 | 200 | `dispatcher.add_handler()`. 201 | 202 | Assigning the `__help__` variable to a string describing this modules' available 203 | commands will allow the bot to load it and add the documentation for 204 | your module to the `/help` command. Setting the `__mod_name__` variable will also allow you to use a nicer, user 205 | friendly name for a module. 206 | 207 | The `__migrate__()` function is used for migrating chats - when a chat is upgraded to a supergroup, the ID changes, so 208 | it is necessary to migrate it in the db. 209 | 210 | The `__stats__()` function is for retrieving module statistics, eg number of users, number of chats. This is accessed 211 | through the `/stats` command, which is only available to the bot owner. 212 | -------------------------------------------------------------------------------- /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, CallbackContext, 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 | from tg_bot.modules.helper_funcs.perms import check_perms 16 | 17 | BLACKLIST_GROUP = 11 18 | 19 | BASE_BLACKLIST_STRING = "The following blacklist filters are currently active in {}:\n" 20 | 21 | 22 | def blacklist(update: Update, context: CallbackContext): 23 | bot, args = context.bot, context.args 24 | msg = update.effective_message # type: Optional[Message] 25 | chat = update.effective_chat # type: Optional[Chat] 26 | chat_name = chat.title or chat.first or chat.username 27 | all_blacklisted = sql.get_chat_blacklist(chat.id) 28 | 29 | filter_list = BASE_BLACKLIST_STRING 30 | 31 | if len(args) > 0 and args[0].lower() == 'copy': 32 | for trigger in all_blacklisted: 33 | filter_list += "{}\n".format(html.escape(trigger)) 34 | else: 35 | for trigger in all_blacklisted: 36 | filter_list += " • {}\n".format(html.escape(trigger)) 37 | 38 | split_text = split_message(filter_list) 39 | for text in split_text: 40 | if text == BASE_BLACKLIST_STRING: 41 | msg.reply_text("There are no blacklisted messages here!") 42 | return 43 | msg.reply_text(text.format(chat_name), parse_mode=ParseMode.HTML) 44 | 45 | 46 | @user_admin 47 | def add_blacklist(update: Update, context: CallbackContext): 48 | if not check_perms(update, 1): 49 | return 50 | bot = context.bot 51 | msg = update.effective_message # type: Optional[Message] 52 | chat = update.effective_chat # type: Optional[Chat] 53 | words = msg.text.split(None, 1) 54 | 55 | if len(words) > 1: 56 | text = words[1] 57 | if "**" in text: 58 | msg.reply_text( 59 | "Can't set blacklist, please don't use consecutive multiple \"*\"." 60 | ) 61 | return 62 | to_blacklist = list( 63 | set(trigger.strip() for trigger in text.split("\n") 64 | if trigger.strip())) 65 | for trigger in to_blacklist: 66 | sql.add_to_blacklist(chat.id, trigger.lower()) 67 | 68 | if len(to_blacklist) == 1: 69 | msg.reply_text("Added {} to the blacklist!".format( 70 | html.escape(to_blacklist[0])), 71 | parse_mode=ParseMode.HTML) 72 | 73 | else: 74 | msg.reply_text( 75 | "Added {} triggers to the blacklist.".format( 76 | len(to_blacklist)), 77 | parse_mode=ParseMode.HTML) 78 | 79 | else: 80 | msg.reply_text( 81 | "Tell me which words you would like to add to the blacklist.") 82 | 83 | 84 | @user_admin 85 | def unblacklist(update: Update, context: CallbackContext): 86 | if not check_perms(update, 0): 87 | return 88 | bot = context.bot 89 | msg = update.effective_message # type: Optional[Message] 90 | chat = update.effective_chat # type: Optional[Chat] 91 | words = msg.text.split(None, 1) 92 | 93 | if len(words) > 1: 94 | text = words[1] 95 | to_unblacklist = list( 96 | set(trigger.strip() for trigger in text.split("\n") 97 | if trigger.strip())) 98 | successful = 0 99 | for trigger in to_unblacklist: 100 | success = sql.rm_from_blacklist(chat.id, trigger.lower()) 101 | if success: 102 | successful += 1 103 | 104 | if len(to_unblacklist) == 1: 105 | if successful: 106 | msg.reply_text( 107 | "Removed {} from the blacklist!".format( 108 | html.escape(to_unblacklist[0])), 109 | parse_mode=ParseMode.HTML) 110 | else: 111 | msg.reply_text("This isn't a blacklisted trigger...!") 112 | 113 | elif successful == len(to_unblacklist): 114 | msg.reply_text( 115 | "Removed {} triggers from the blacklist.".format( 116 | successful), 117 | parse_mode=ParseMode.HTML) 118 | 119 | elif not successful: 120 | msg.reply_text( 121 | "None of these triggers exist, so they weren't removed.". 122 | format(successful, 123 | len(to_unblacklist) - successful), 124 | parse_mode=ParseMode.HTML) 125 | 126 | else: 127 | msg.reply_text( 128 | "Removed {} triggers from the blacklist. {} did not exist, " 129 | "so were not removed.".format(successful, 130 | len(to_unblacklist) - 131 | successful), 132 | parse_mode=ParseMode.HTML) 133 | else: 134 | msg.reply_text( 135 | "Tell me which words you would like to remove from the blacklist.") 136 | 137 | 138 | @user_not_admin 139 | def del_blacklist(update: Update, context: CallbackContext): 140 | bot = context.bot 141 | chat = update.effective_chat # type: Optional[Chat] 142 | message = update.effective_message # type: Optional[Message] 143 | to_match = extract_text(message) 144 | user = update.effective_user # type: Optional[User] 145 | 146 | if not user.id or int(user.id) == 777000 or int(user.id) == 1087968824: 147 | return "" 148 | 149 | if not to_match: 150 | return 151 | 152 | chat_filters = sql.get_chat_blacklist(chat.id) 153 | for trigger in chat_filters: 154 | pattern = r"( |^|[^\w])" + re.escape(trigger).replace( 155 | "\*", "(.*)").replace("\\(.*)", "*") + r"( |$|[^\w])" 156 | if re.search(pattern, to_match, flags=re.IGNORECASE): 157 | try: 158 | message.delete() 159 | except BadRequest as excp: 160 | if excp.message == "Message to delete not found": 161 | pass 162 | else: 163 | LOGGER.exception("Error while deleting blacklist message.") 164 | break 165 | 166 | 167 | def __migrate__(old_chat_id, new_chat_id): 168 | sql.migrate_chat(old_chat_id, new_chat_id) 169 | 170 | 171 | def __chat_settings__(chat_id, user_id): 172 | blacklisted = sql.num_blacklist_chat_filters(chat_id) 173 | return "There are {} blacklisted words.".format(blacklisted) 174 | 175 | 176 | def __stats__(): 177 | return "{} blacklist triggers, across {} chats.".format( 178 | sql.num_blacklist_filters(), sql.num_blacklist_filter_chats()) 179 | 180 | 181 | __mod_name__ = "Word Blacklists" 182 | 183 | __help__ = """ 184 | Blacklists are used to stop certain triggers from being said in a group. Any time the trigger is mentioned, \ 185 | the message will immediately be deleted. A good combo is sometimes to pair this up with warn filters! 186 | 187 | Please check /regexhelp for how to setup proper triggers. 188 | 189 | *NOTE:* blacklists do not affect group admins. 190 | 191 | - /blacklist: View the current blacklisted words. 192 | 193 | *Admin only:* 194 | - /addblacklist : Add a trigger to the blacklist. Each line is considered one trigger, so using different \ 195 | lines will allow you to add multiple triggers. 196 | - /unblacklist : Remove triggers from the blacklist. Same newline logic applies here, so you can remove \ 197 | multiple triggers at once. 198 | - /rmblacklist : Same as above. 199 | 200 | Tip: To copy list of saved blacklist simply use `/blacklist copy`, the bot will send non-bulleted list of blacklist. 201 | """ 202 | 203 | BLACKLIST_HANDLER = DisableAbleCommandHandler("blacklist", 204 | blacklist, 205 | filters=Filters.chat_type.groups, 206 | pass_args=True, 207 | admin_ok=True, 208 | run_async=True) 209 | ADD_BLACKLIST_HANDLER = CommandHandler("addblacklist", 210 | add_blacklist, 211 | filters=Filters.chat_type.groups, 212 | run_async=True) 213 | UNBLACKLIST_HANDLER = CommandHandler(["unblacklist", "rmblacklist"], 214 | unblacklist, 215 | filters=Filters.chat_type.groups, 216 | run_async=True) 217 | BLACKLIST_DEL_HANDLER = MessageHandler( 218 | (Filters.text | Filters.command | Filters.sticker | Filters.photo) 219 | & Filters.chat_type.groups, 220 | del_blacklist, 221 | run_async=True) 222 | 223 | dispatcher.add_handler(BLACKLIST_HANDLER) 224 | dispatcher.add_handler(ADD_BLACKLIST_HANDLER) 225 | dispatcher.add_handler(UNBLACKLIST_HANDLER) 226 | dispatcher.add_handler(BLACKLIST_DEL_HANDLER, group=BLACKLIST_GROUP) 227 | -------------------------------------------------------------------------------- /tg_bot/modules/sql/warns_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import BigInteger, Integer, Column, String, UnicodeText, func, distinct, Boolean 4 | from sqlalchemy.dialects import postgresql 5 | 6 | from tg_bot.modules.sql import SESSION, BASE 7 | 8 | 9 | class Warns(BASE): 10 | __tablename__ = "warns" 11 | 12 | user_id = Column(BigInteger, primary_key=True) 13 | chat_id = Column(String(14), primary_key=True) 14 | num_warns = Column(Integer, default=0) 15 | reasons = Column(postgresql.ARRAY(UnicodeText)) 16 | 17 | def __init__(self, user_id, chat_id): 18 | self.user_id = user_id 19 | self.chat_id = str(chat_id) 20 | self.num_warns = 0 21 | self.reasons = [] 22 | 23 | def __repr__(self): 24 | return "<{} warns for {} in {} for reasons {}>".format( 25 | self.num_warns, self.user_id, self.chat_id, self.reasons) 26 | 27 | 28 | class WarnFilters(BASE): 29 | __tablename__ = "warn_filters" 30 | chat_id = Column(String(14), primary_key=True) 31 | keyword = Column(UnicodeText, primary_key=True, nullable=False) 32 | reply = Column(UnicodeText, nullable=False) 33 | 34 | def __init__(self, chat_id, keyword, reply): 35 | self.chat_id = str(chat_id) # ensure string 36 | self.keyword = keyword 37 | self.reply = reply 38 | 39 | def __repr__(self): 40 | return "" % self.chat_id 41 | 42 | def __eq__(self, other): 43 | return bool( 44 | isinstance(other, WarnFilters) and self.chat_id == other.chat_id 45 | and self.keyword == other.keyword) 46 | 47 | 48 | class WarnSettings(BASE): 49 | __tablename__ = "warn_settings" 50 | chat_id = Column(String(14), primary_key=True) 51 | warn_limit = Column(Integer, default=3) 52 | soft_warn = Column(Boolean, default=False) 53 | 54 | def __init__(self, chat_id, warn_limit=3, soft_warn=False): 55 | self.chat_id = str(chat_id) 56 | self.warn_limit = warn_limit 57 | self.soft_warn = soft_warn 58 | 59 | def __repr__(self): 60 | return "<{} has {} possible warns.>".format(self.chat_id, 61 | self.warn_limit) 62 | 63 | 64 | Warns.__table__.create(checkfirst=True) 65 | WarnFilters.__table__.create(checkfirst=True) 66 | WarnSettings.__table__.create(checkfirst=True) 67 | 68 | WARN_INSERTION_LOCK = threading.RLock() 69 | WARN_FILTER_INSERTION_LOCK = threading.RLock() 70 | WARN_SETTINGS_LOCK = threading.RLock() 71 | 72 | WARN_FILTERS = {} 73 | 74 | 75 | def warn_user(user_id, chat_id, reason=None): 76 | with WARN_INSERTION_LOCK: 77 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 78 | if not warned_user: 79 | warned_user = Warns(user_id, str(chat_id)) 80 | 81 | warned_user.num_warns += 1 82 | if reason: 83 | warned_user.reasons = warned_user.reasons + [ 84 | reason 85 | ] # TODO:: double check this wizardry 86 | 87 | reasons = warned_user.reasons 88 | num = warned_user.num_warns 89 | 90 | SESSION.add(warned_user) 91 | SESSION.commit() 92 | 93 | return num, reasons 94 | 95 | 96 | def remove_warn(user_id, chat_id): 97 | with WARN_INSERTION_LOCK: 98 | removed = False 99 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 100 | 101 | if warned_user and warned_user.num_warns > 0: 102 | warned_user.num_warns -= 1 103 | 104 | SESSION.add(warned_user) 105 | SESSION.commit() 106 | removed = True 107 | 108 | SESSION.close() 109 | return removed 110 | 111 | 112 | def reset_warns(user_id, chat_id): 113 | with WARN_INSERTION_LOCK: 114 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 115 | if warned_user: 116 | warned_user.num_warns = 0 117 | warned_user.reasons = [] 118 | 119 | SESSION.add(warned_user) 120 | SESSION.commit() 121 | SESSION.close() 122 | 123 | 124 | def get_warns(user_id, chat_id): 125 | try: 126 | user = SESSION.query(Warns).get((user_id, str(chat_id))) 127 | if not user: 128 | return None 129 | reasons = user.reasons 130 | num = user.num_warns 131 | return num, reasons 132 | finally: 133 | SESSION.close() 134 | 135 | 136 | def add_warn_filter(chat_id, keyword, reply): 137 | with WARN_FILTER_INSERTION_LOCK: 138 | warn_filt = WarnFilters(str(chat_id), keyword, reply) 139 | 140 | if keyword not in WARN_FILTERS.get(str(chat_id), []): 141 | WARN_FILTERS[str(chat_id)] = sorted( 142 | WARN_FILTERS.get(str(chat_id), []) + [keyword], 143 | key=lambda x: (-len(x), x)) 144 | 145 | SESSION.merge(warn_filt) # merge to avoid duplicate key issues 146 | SESSION.commit() 147 | 148 | 149 | def remove_warn_filter(chat_id, keyword): 150 | with WARN_FILTER_INSERTION_LOCK: 151 | warn_filt = SESSION.query(WarnFilters).get((str(chat_id), keyword)) 152 | if warn_filt: 153 | if keyword in WARN_FILTERS.get(str(chat_id), []): # sanity check 154 | WARN_FILTERS.get(str(chat_id), []).remove(keyword) 155 | 156 | SESSION.delete(warn_filt) 157 | SESSION.commit() 158 | return True 159 | SESSION.close() 160 | return False 161 | 162 | 163 | def get_chat_warn_triggers(chat_id): 164 | return WARN_FILTERS.get(str(chat_id), set()) 165 | 166 | 167 | def get_chat_warn_filters(chat_id): 168 | try: 169 | return SESSION.query(WarnFilters).filter( 170 | WarnFilters.chat_id == str(chat_id)).all() 171 | finally: 172 | SESSION.close() 173 | 174 | 175 | def get_warn_filter(chat_id, keyword): 176 | try: 177 | return SESSION.query(WarnFilters).get((str(chat_id), keyword)) 178 | finally: 179 | SESSION.close() 180 | 181 | 182 | def set_warn_limit(chat_id, warn_limit): 183 | with WARN_SETTINGS_LOCK: 184 | curr_setting = SESSION.query(WarnSettings).get(str(chat_id)) 185 | if not curr_setting: 186 | curr_setting = WarnSettings(chat_id, warn_limit=warn_limit) 187 | 188 | curr_setting.warn_limit = warn_limit 189 | 190 | SESSION.add(curr_setting) 191 | SESSION.commit() 192 | 193 | 194 | def set_warn_strength(chat_id, soft_warn): 195 | with WARN_SETTINGS_LOCK: 196 | curr_setting = SESSION.query(WarnSettings).get(str(chat_id)) 197 | if not curr_setting: 198 | curr_setting = WarnSettings(chat_id, soft_warn=soft_warn) 199 | 200 | curr_setting.soft_warn = soft_warn 201 | 202 | SESSION.add(curr_setting) 203 | SESSION.commit() 204 | 205 | 206 | def get_warn_setting(chat_id): 207 | try: 208 | setting = SESSION.query(WarnSettings).get(str(chat_id)) 209 | if setting: 210 | return setting.warn_limit, setting.soft_warn 211 | else: 212 | return 3, False 213 | 214 | finally: 215 | SESSION.close() 216 | 217 | 218 | def num_warns(): 219 | try: 220 | return SESSION.query(func.sum(Warns.num_warns)).scalar() or 0 221 | finally: 222 | SESSION.close() 223 | 224 | 225 | def num_warn_chats(): 226 | try: 227 | return SESSION.query(func.count(distinct(Warns.chat_id))).scalar() 228 | finally: 229 | SESSION.close() 230 | 231 | 232 | def num_warn_filters(): 233 | try: 234 | return SESSION.query(WarnFilters).count() 235 | finally: 236 | SESSION.close() 237 | 238 | 239 | def num_warn_chat_filters(chat_id): 240 | try: 241 | return SESSION.query(WarnFilters.chat_id).filter( 242 | WarnFilters.chat_id == str(chat_id)).count() 243 | finally: 244 | SESSION.close() 245 | 246 | 247 | def num_warn_filter_chats(): 248 | try: 249 | return SESSION.query(func.count(distinct( 250 | WarnFilters.chat_id))).scalar() 251 | finally: 252 | SESSION.close() 253 | 254 | 255 | def __load_chat_warn_filters(): 256 | global WARN_FILTERS 257 | try: 258 | chats = SESSION.query(WarnFilters.chat_id).distinct().all() 259 | for (chat_id, ) in chats: # remove tuple by ( ,) 260 | WARN_FILTERS[chat_id] = [] 261 | 262 | all_filters = SESSION.query(WarnFilters).all() 263 | for x in all_filters: 264 | WARN_FILTERS[x.chat_id] += [x.keyword] 265 | 266 | WARN_FILTERS = { 267 | x: sorted(set(y), key=lambda i: (-len(i), i)) 268 | for x, y in WARN_FILTERS.items() 269 | } 270 | 271 | finally: 272 | SESSION.close() 273 | 274 | 275 | def migrate_chat(old_chat_id, new_chat_id): 276 | with WARN_INSERTION_LOCK: 277 | chat_notes = SESSION.query(Warns).filter( 278 | Warns.chat_id == str(old_chat_id)).all() 279 | for note in chat_notes: 280 | note.chat_id = str(new_chat_id) 281 | SESSION.commit() 282 | 283 | with WARN_FILTER_INSERTION_LOCK: 284 | chat_filters = SESSION.query(WarnFilters).filter( 285 | WarnFilters.chat_id == str(old_chat_id)).all() 286 | for filt in chat_filters: 287 | filt.chat_id = str(new_chat_id) 288 | SESSION.commit() 289 | WARN_FILTERS[str(new_chat_id)] = WARN_FILTERS[str(old_chat_id)] 290 | del WARN_FILTERS[str(old_chat_id)] 291 | 292 | with WARN_SETTINGS_LOCK: 293 | chat_settings = SESSION.query(WarnSettings).filter( 294 | WarnSettings.chat_id == str(old_chat_id)).all() 295 | for setting in chat_settings: 296 | setting.chat_id = str(new_chat_id) 297 | SESSION.commit() 298 | 299 | 300 | __load_chat_warn_filters() 301 | --------------------------------------------------------------------------------