├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── config.env ├── docker-compose.yml ├── heroku.yml ├── requirements.txt ├── sample_config.py └── spr ├── __init__.py ├── __main__.py ├── core ├── __init__.py └── keyboard.py ├── modules ├── __init__.py ├── __main__.py ├── blacklist.py ├── devs.py ├── info.py ├── manage.py ├── vote.py └── watcher.py └── utils ├── __init__.py ├── db.py ├── functions.py └── misc.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | *.session* 7 | *.sqlite3 8 | *.db 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | .idea 135 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.5-buster 2 | WORKDIR /app 3 | RUN chmod 777 /app 4 | RUN pip3 install -U pip 5 | COPY requirements.txt . 6 | RUN pip3 install --no-cache-dir -U -r requirements.txt 7 | COPY . . 8 | CMD ["python3", "-m", "spr"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Akshay Rajput 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✨ SpamProtectionRobot ✨ 2 | ### Anti Spam/NSFW Telegram Bot Written In Python With Pyrogram. 3 | 4 | 5 | [![Python](http://forthebadge.com/images/badges/made-with-python.svg)](https://python.org)  6 | [![ForTheBadge built-with-love](http://ForTheBadge.com/images/badges/built-with-love.svg)](https://GitHub.com/TheHamkerCat/) 7 | 8 | 9 | 10 | 11 | 12 | ## Requirements 13 | 14 | - Python >= 3.7 15 | 16 | 17 | ## Install Locally Or On A VPS 18 | 19 | ```sh 20 | $ git clone https://github.com/thehamkercat/SpamProtectionRobot 21 | 22 | $ cd SpamProtectionRobot 23 | 24 | $ pip3 install -U -r requirements.txt 25 | 26 | $ cp sample_config.py config.py 27 | ``` 28 | Edit **config.py** with your own values 29 | 30 | # Run Directly 31 | ```sh 32 | $ python3 -m spr 33 | ``` 34 | 35 | # Heroku 36 | 37 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/thehamkercat/SpamProtectionRobot/) 38 | 39 | # Docker 40 | 41 | ```sh 42 | $ git clone https://github.com/TheHamkerCat/SpamProtectionRobot 43 | 44 | $ cd SpamProtectionRobot 45 | ``` 46 | 47 | Edit **config.env** With Own Values. 48 | 49 | ```sh 50 | $ docker compose up --build 51 | ``` 52 | 53 | ## Note 54 | 55 | 1. NOT RELATED TO INTELLIVOID IN ANY WAY. 56 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SpamProtectionRobot", 3 | "description": "Anti Spam/NSFW Telegram Bot Written In Python With Pyrogram.", 4 | "repository": "https://github.com/thehamkercat/SpamProtectionRobot", 5 | "logo": "https://upload.wikimedia.org/wikipedia/commons/c/c3/Python-logo-notext.svg", 6 | "keywords": [ 7 | "python3", 8 | "telegram", 9 | "bot", 10 | "anti spam bot", 11 | "telegram-bot", 12 | "pyrogram", 13 | "anti nsfw bot", 14 | "SpamProtectionRobot", 15 | "Spam Protection Bot" 16 | ], 17 | "stack": "container", 18 | "env": { 19 | "BOT_TOKEN": { 20 | "description": "Obtain a Telegram bot oken by contacting @BotFather", 21 | "required": true 22 | }, 23 | "SUDOERS": { 24 | "description": "Sudo user's ids, Ex:- 123456654 654654311 1546523456", 25 | "required": true 26 | }, 27 | "NSFW_LOG_CHANNEL": { 28 | "description": "ID of the channel where NSFW logs should be forwarded. Ex: '-123456'", 29 | "required": true 30 | }, 31 | "SPAM_LOG_CHANNEL": { 32 | "description": "ID of the channel where SPAM logs should be forwarded. Ex: '-123456'", 33 | "required": true 34 | }, 35 | "ARQ_API_KEY": { 36 | "description": "Get this from @ARQRobot.", 37 | "required": true 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config.env: -------------------------------------------------------------------------------- 1 | # Only for docker and heroku 2 | 3 | BOT_TOKEN=12345:abcaskhdkqlwjgbdklajwbdliw 4 | SUDOERS=1243703097 1351353543 5 | NSFW_LOG_CHANNEL=-1001470187101 6 | SPAM_LOG_CHANNEL=-1001554591017 7 | ARQ_API_KEY=A54D655S4D654654D # Get it from @ARQRobot 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | bot: 4 | container_name: spr 5 | stop_grace_period: 1m 6 | restart: always 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | env_file: 11 | - config.env 12 | 13 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/KurimuzonAkuma/pyrogram.git 2 | TgCrypto 3 | python-arq 4 | pykeyboard 5 | python-dotenv 6 | -------------------------------------------------------------------------------- /sample_config.py: -------------------------------------------------------------------------------- 1 | from os import environ as env 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv("config.env") 6 | 7 | """ 8 | READ EVERYTHING CAREFULLY!!! 9 | """ 10 | 11 | 12 | DEPLOYING_ON_HEROKU = ( 13 | True # Make this False if you're not deploying On heroku/Docker 14 | ) 15 | 16 | 17 | if not DEPLOYING_ON_HEROKU: 18 | BOT_TOKEN = "123456:qwertyuiopasdfghjklzxcvbnm" 19 | SUDOERS = [1243703097] 20 | NSFW_LOG_CHANNEL = -1001470187101 21 | SPAM_LOG_CHANNEL = -1001554591017 22 | ARQ_API_KEY = "" # Get it from @ARQRobot 23 | else: 24 | BOT_TOKEN = env.get("BOT_TOKEN") 25 | SUDOERS = [int(x) for x in env.get("SUDO_USERS_ID", "").split()] 26 | NSFW_LOG_CHANNEL = int(env.get("NSFW_LOG_CHANNEL")) 27 | SPAM_LOG_CHANNEL = int(env.get("SPAM_LOG_CHANNEL")) 28 | ARQ_API_KEY = env.get("ARQ_API_KEY") 29 | -------------------------------------------------------------------------------- /spr/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import exists 2 | from sqlite3 import connect 3 | 4 | from aiohttp import ClientSession 5 | from pyrogram import Client 6 | from Python_ARQ import ARQ 7 | 8 | SESSION_NAME = "spr" 9 | DB_NAME = "db.sqlite3" 10 | API_ID = 6 11 | API_HASH = "eb06d4abfb49dc3eeb1aeb98ae0f581e" 12 | ARQ_API_URL = "https://arq.hamker.dev" 13 | 14 | if exists("config.py"): 15 | from config import * 16 | else: 17 | from sample_config import * 18 | 19 | session = ClientSession() 20 | 21 | arq = ARQ(ARQ_API_URL, ARQ_API_KEY, session) 22 | 23 | conn = connect(DB_NAME) 24 | 25 | spr = Client( 26 | SESSION_NAME, 27 | bot_token=BOT_TOKEN, 28 | api_id=API_ID, 29 | api_hash=API_HASH, 30 | ) 31 | with spr: 32 | bot = spr.get_me() 33 | BOT_ID = bot.id 34 | BOT_USERNAME = bot.username 35 | -------------------------------------------------------------------------------- /spr/__main__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | from importlib import import_module as import_ 4 | 5 | from pyrogram import filters, idle 6 | from pyrogram.types import (CallbackQuery, InlineKeyboardButton, 7 | InlineKeyboardMarkup, Message) 8 | from pyrogram.enums import ChatType 9 | 10 | from spr import BOT_USERNAME, conn, session, spr 11 | from spr.core import ikb 12 | from spr.modules import MODULES 13 | from spr.utils.misc import once_a_day, once_a_minute, paginate_modules 14 | 15 | HELPABLE = {} 16 | 17 | 18 | async def main(): 19 | await spr.start() 20 | # Load all the modules. 21 | for module in MODULES: 22 | imported_module = import_(module) 23 | if ( 24 | hasattr(imported_module, "__MODULE__") 25 | and imported_module.__MODULE__ 26 | ): 27 | imported_module.__MODULE__ = imported_module.__MODULE__ 28 | if ( 29 | hasattr(imported_module, "__HELP__") 30 | and imported_module.__HELP__ 31 | ): 32 | HELPABLE[ 33 | imported_module.__MODULE__.lower() 34 | ] = imported_module 35 | print("STARTED !") 36 | loop = asyncio.get_running_loop() 37 | loop.create_task(once_a_day()) 38 | loop.create_task(once_a_minute()) 39 | await idle() 40 | conn.commit() 41 | conn.close() 42 | await session.close() 43 | await spr.stop() 44 | 45 | 46 | @spr.on_message(filters.command(["help", "start"]), group=2) 47 | async def help_command(_, message: Message): 48 | if message.chat.type != ChatType.PRIVATE: 49 | kb = ikb({"Help": f"https://t.me/{BOT_USERNAME}?start=help"}) 50 | return await message.reply("Pm Me For Help", reply_markup=kb) 51 | kb = ikb( 52 | { 53 | "Help": "bot_commands", 54 | "Repo": "https://github.com/TheHamkerCat/SpamProtectionRobot", 55 | "Add Me To Your Group": f"https://t.me/{BOT_USERNAME}?startgroup=new", 56 | "Support Chat (for now)": "https://t.me/WBBSupport", 57 | } 58 | ) 59 | mention = message.from_user.mention 60 | await message.reply_photo( 61 | "https://hamker.me/logo_3.png", 62 | caption=f"Hi {mention}, I'm SpamProtectionRobot," 63 | + " Choose An Option From Below.", 64 | reply_markup=kb, 65 | ) 66 | 67 | 68 | @spr.on_callback_query(filters.regex("bot_commands")) 69 | async def commands_callbacc(_, cq: CallbackQuery): 70 | text, keyboard = await help_parser(cq.from_user.mention) 71 | await asyncio.gather( 72 | cq.answer(), 73 | cq.message.delete(), 74 | spr.send_message( 75 | cq.message.chat.id, 76 | text=text, 77 | reply_markup=keyboard, 78 | ), 79 | ) 80 | 81 | 82 | async def help_parser(name, keyboard=None): 83 | if not keyboard: 84 | keyboard = InlineKeyboardMarkup( 85 | paginate_modules(0, HELPABLE, "help") 86 | ) 87 | return ( 88 | f"Hello {name}, I'm SpamProtectionRobot, I can protect " 89 | + "your group from Spam and NSFW media using " 90 | + "machine learning. Choose an option from below.", 91 | keyboard, 92 | ) 93 | 94 | 95 | @spr.on_callback_query(filters.regex(r"help_(.*?)")) 96 | async def help_button(client, query: CallbackQuery): 97 | mod_match = re.match(r"help_module\((.+?)\)", query.data) 98 | prev_match = re.match(r"help_prev\((.+?)\)", query.data) 99 | next_match = re.match(r"help_next\((.+?)\)", query.data) 100 | back_match = re.match(r"help_back", query.data) 101 | create_match = re.match(r"help_create", query.data) 102 | u = query.from_user.mention 103 | top_text = ( 104 | f"Hello {u}, I'm SpamProtectionRobot, I can protect " 105 | + "your group from Spam and NSFW media using " 106 | + "machine learning. Choose an option from below." 107 | ) 108 | if mod_match: 109 | module = mod_match.group(1) 110 | text = ( 111 | "{} **{}**:\n".format( 112 | "Here is the help for", HELPABLE[module].__MODULE__ 113 | ) 114 | + HELPABLE[module].__HELP__ 115 | ) 116 | 117 | await query.message.edit( 118 | text=text, 119 | reply_markup=InlineKeyboardMarkup( 120 | [ 121 | [ 122 | InlineKeyboardButton( 123 | "back", callback_data="help_back" 124 | ) 125 | ] 126 | ] 127 | ), 128 | disable_web_page_preview=True, 129 | ) 130 | 131 | elif prev_match: 132 | curr_page = int(prev_match.group(1)) 133 | await query.message.edit( 134 | text=top_text, 135 | reply_markup=InlineKeyboardMarkup( 136 | paginate_modules(curr_page - 1, HELPABLE, "help") 137 | ), 138 | disable_web_page_preview=True, 139 | ) 140 | 141 | elif next_match: 142 | next_page = int(next_match.group(1)) 143 | await query.message.edit( 144 | text=top_text, 145 | reply_markup=InlineKeyboardMarkup( 146 | paginate_modules(next_page + 1, HELPABLE, "help") 147 | ), 148 | disable_web_page_preview=True, 149 | ) 150 | 151 | elif back_match: 152 | await query.message.edit( 153 | text=top_text, 154 | reply_markup=InlineKeyboardMarkup( 155 | paginate_modules(0, HELPABLE, "help") 156 | ), 157 | disable_web_page_preview=True, 158 | ) 159 | 160 | elif create_match: 161 | text, keyboard = await help_parser(query) 162 | await query.message.edit( 163 | text=text, 164 | reply_markup=keyboard, 165 | disable_web_page_preview=True, 166 | ) 167 | 168 | return await client.answer_callback_query(query.id) 169 | 170 | 171 | @spr.on_message(filters.command("runs"), group=3) 172 | async def runs_func(_, message: Message): 173 | await message.reply("What am i? Rose?") 174 | 175 | 176 | if __name__ == "__main__": 177 | loop = asyncio.get_event_loop() 178 | loop.run_until_complete(main()) 179 | -------------------------------------------------------------------------------- /spr/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .keyboard import ikb 2 | -------------------------------------------------------------------------------- /spr/core/keyboard.py: -------------------------------------------------------------------------------- 1 | from re import findall 2 | 3 | from pykeyboard import InlineKeyboard 4 | from pyrogram.types import InlineKeyboardButton as Ikb 5 | 6 | 7 | def is_url(text: str) -> bool: 8 | """Function to extract urls from a string""" 9 | regex = r"""(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-] 10 | [.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|( 11 | \([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\ 12 | ()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".strip() 13 | return bool([x[0] for x in findall(regex, text)]) 14 | 15 | 16 | def keyboard(buttons_list, row_width: int = 2): 17 | """Buttons builder, pass buttons in a list and it will 18 | return pyrogram.types.IKB object 19 | Ex: keyboard([["click here", "https://google.com"]]) 20 | if theres, a url, it will make url button, else callback button 21 | """ 22 | buttons = InlineKeyboard(row_width=row_width) 23 | data = [ 24 | ( 25 | Ikb(text=i[0], callback_data=i[1]) 26 | if not is_url(i[1]) 27 | else Ikb(text=i[0], url=i[1]) 28 | ) 29 | for i in buttons_list 30 | ] 31 | buttons.add(*data) 32 | return buttons 33 | 34 | 35 | def ikb(data: dict, row_width: int = 2): 36 | """Converts a dict to pyrogram buttons using item's key and value 37 | Ex: dict_to_keyboard({"click here": "this is callback data"})""" 38 | return keyboard(data.items(), row_width=2) 39 | -------------------------------------------------------------------------------- /spr/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from importlib import import_module 3 | 4 | mod_paths = glob("spr/modules/*.py") 5 | MODULES = [ 6 | "spr.modules." + f.split("/")[-1][:-3] 7 | for f in mod_paths 8 | if not f.endswith("__.py") # to exclude __init__.py 9 | ] 10 | import_module("spr.modules.__main__") 11 | -------------------------------------------------------------------------------- /spr/modules/__main__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHamkerCat/SpamProtectionRobot/cde850aa922471eb8897d40ff0f1014c5ef349fa/spr/modules/__main__.py -------------------------------------------------------------------------------- /spr/modules/blacklist.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | from pyrogram.types import Message 3 | 4 | from spr import SPAM_LOG_CHANNEL, SUDOERS, spr 5 | from spr.modules.info import get_info 6 | from spr.utils.db import (add_chat, add_user, blacklist_chat, 7 | blacklist_user, chat_exists, 8 | is_chat_blacklisted, is_user_blacklisted, 9 | user_exists, whitelist_chat, whitelist_user) 10 | 11 | 12 | @spr.on_message( 13 | filters.command("blacklist") & filters.user(SUDOERS), group=3 14 | ) 15 | async def blacklist_func(_, message: Message): 16 | err = "Enter a user/chat's id and give a reason." 17 | if len(message.command) < 3: 18 | return await message.reply_text(err) 19 | id = message.text.split(None, 2)[1] 20 | reason = message.text.split(None, 2)[2].strip() 21 | if not reason or not id: 22 | return await message.reply_text(err) 23 | try: 24 | id = int(id) 25 | except ValueError: 26 | return await message.reply_text(err) 27 | 28 | if id == 0: 29 | return await message.reply_text(err) 30 | 31 | if id < 0: 32 | try: 33 | chat = await spr.get_chat(id) 34 | except Exception as e: 35 | return await message.reply_text(str(e)) 36 | 37 | if not chat_exists(id): 38 | add_chat(id) 39 | if is_chat_blacklisted(id): 40 | return await message.reply_text( 41 | "This chat is already blacklisted." 42 | ) 43 | blacklist_chat(id, reason) 44 | await message.reply_text(f"Blacklisted chat {chat.title}") 45 | msg = f"**BLACKLIST EVENT**\n{await get_info(id)}" 46 | return await spr.send_message(SPAM_LOG_CHANNEL, text=msg) 47 | 48 | if id in SUDOERS: 49 | return await message.reply_text( 50 | "This user is in SUDOERS and cannot be blacklisted." 51 | ) 52 | try: 53 | user = await spr.get_users(id) 54 | except Exception as e: 55 | return await message.reply_text(str(e)) 56 | 57 | if not user_exists(id): 58 | add_user(id) 59 | if is_user_blacklisted(id): 60 | return await message.reply_text( 61 | "This user is already blacklisted." 62 | ) 63 | blacklist_user(id, reason) 64 | await message.reply_text(f"Blacklisted user {user.mention}") 65 | msg = f"**BLACKLIST EVENT**\n{await get_info(id)}" 66 | await spr.send_message(SPAM_LOG_CHANNEL, text=msg) 67 | 68 | 69 | @spr.on_message( 70 | filters.command("whitelist") & filters.user(SUDOERS), group=3 71 | ) 72 | async def whitelist_func(_, message: Message): 73 | err = "Enter a user/chat's id." 74 | if len(message.command) != 2: 75 | return await message.reply_text(err) 76 | id = message.text.split(None, 1)[1] 77 | try: 78 | id = int(id) 79 | except ValueError: 80 | return await message.reply_text(err) 81 | if id == 0: 82 | return await message.reply_text(err) 83 | if id < 0: 84 | try: 85 | chat = await spr.get_chat(id) 86 | except Exception as e: 87 | return await message.reply_text(str(e)) 88 | 89 | if not chat_exists(id): 90 | add_chat(id) 91 | if not is_chat_blacklisted(id): 92 | return await message.reply_text( 93 | "This chat is already whitelisted." 94 | ) 95 | whitelist_chat(id) 96 | return await message.reply_text(f"Whitelisted {chat.title}") 97 | 98 | try: 99 | user = await spr.get_users(id) 100 | except Exception as e: 101 | return await message.reply_text(str(e)) 102 | 103 | if not user_exists(id): 104 | add_user(id) 105 | if not is_user_blacklisted(id): 106 | return await message.reply_text( 107 | "This user is already whitelisted." 108 | ) 109 | whitelist_user(id) 110 | return await message.reply_text(f"Whitelisted {user.mention}") 111 | -------------------------------------------------------------------------------- /spr/modules/devs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | import sys 5 | import traceback 6 | from inspect import getfullargspec 7 | from io import StringIO 8 | from time import time 9 | 10 | from pyrogram import filters 11 | from pyrogram.types import (InlineKeyboardButton, 12 | InlineKeyboardMarkup, Message) 13 | 14 | from spr import SUDOERS, arq, spr 15 | from spr.utils.db import conn 16 | 17 | __MODULE__ = "Devs" 18 | __HELP__ = """ 19 | **THIS MODULE IS ONLY FOR DEVS** 20 | 21 | /eval - Execute python code. 22 | /sh - Execute shell code. 23 | 24 | /blacklist [CHAT_ID|USER_ID] - Blacklist a chat/user. 25 | /whitelist [CHAT_ID|USER_ID] - Whitelist a chat/user. 26 | """ 27 | 28 | conn = conn 29 | p = print 30 | arq = arq 31 | 32 | 33 | async def aexec(code, client, message): 34 | exec( 35 | "async def __aexec(client, message): " 36 | + "".join(f"\n {a}" for a in code.split("\n")) 37 | ) 38 | return await locals()["__aexec"](client, message) 39 | 40 | 41 | async def edit_or_reply(msg: Message, **kwargs): 42 | func = msg.edit_text if msg.from_user.is_self else msg.reply 43 | spec = getfullargspec(func.__wrapped__).args 44 | await func(**{k: v for k, v in kwargs.items() if k in spec}) 45 | 46 | 47 | @spr.on_message( 48 | filters.user(SUDOERS) 49 | & ~filters.forwarded 50 | & ~filters.via_bot 51 | & filters.command("eval"), 52 | group=50, 53 | ) 54 | async def executor(client, message): 55 | try: 56 | cmd = message.text.split(" ", maxsplit=1)[1] 57 | except IndexError: 58 | return await message.delete() 59 | t1 = time() 60 | old_stderr = sys.stderr 61 | old_stdout = sys.stdout 62 | redirected_output = sys.stdout = StringIO() 63 | redirected_error = sys.stderr = StringIO() 64 | stdout, stderr, exc = None, None, None 65 | try: 66 | await aexec(cmd, client, message) 67 | except Exception: 68 | exc = traceback.format_exc() 69 | stdout = redirected_output.getvalue() 70 | stderr = redirected_error.getvalue() 71 | sys.stdout = old_stdout 72 | sys.stderr = old_stderr 73 | evaluation = "" 74 | if exc: 75 | evaluation = exc 76 | elif stderr: 77 | evaluation = stderr 78 | elif stdout: 79 | evaluation = stdout 80 | else: 81 | evaluation = "Success" 82 | final_output = f"**OUTPUT**:\n```{evaluation.strip()}```" 83 | if len(final_output) > 4096: 84 | filename = "output.txt" 85 | with open(filename, "w+", encoding="utf8") as out_file: 86 | out_file.write(str(evaluation.strip())) 87 | t2 = time() 88 | keyboard = InlineKeyboardMarkup( 89 | [ 90 | [ 91 | InlineKeyboardButton( 92 | text="⏳", 93 | callback_data=f"runtime {t2-t1} Seconds", 94 | ) 95 | ] 96 | ] 97 | ) 98 | await message.reply_document( 99 | document=filename, 100 | caption=f"**INPUT:**\n`{cmd[0:980]}`\n\n**OUTPUT:**\n`Attached Document`", 101 | quote=False, 102 | reply_markup=keyboard, 103 | ) 104 | await message.delete() 105 | os.remove(filename) 106 | else: 107 | t2 = time() 108 | keyboard = InlineKeyboardMarkup( 109 | [ 110 | [ 111 | InlineKeyboardButton( 112 | text="⏳", 113 | callback_data=f"runtime {round(t2-t1, 3)} Seconds", 114 | ) 115 | ] 116 | ] 117 | ) 118 | await edit_or_reply( 119 | message, text=final_output, reply_markup=keyboard 120 | ) 121 | 122 | 123 | @spr.on_callback_query(filters.regex(r"runtime")) 124 | async def runtime_func_cq(_, cq): 125 | runtime = cq.data.split(None, 1)[1] 126 | await cq.answer(runtime, show_alert=True) 127 | 128 | 129 | @spr.on_message( 130 | filters.user(SUDOERS) 131 | & ~filters.forwarded 132 | & ~filters.via_bot 133 | & filters.command("sh"), 134 | group=50, 135 | ) 136 | async def shellrunner(client, message): 137 | if len(message.command) < 2: 138 | return await edit_or_reply( 139 | message, text="**Usage:**\n/sh git pull" 140 | ) 141 | text = message.text.split(None, 1)[1] 142 | if "\n" in text: 143 | code = text.split("\n") 144 | output = "" 145 | for x in code: 146 | shell = re.split( 147 | """ (?=(?:[^'"]|'[^']*'|"[^"]*")*$)""", x 148 | ) 149 | try: 150 | process = subprocess.Popen( 151 | shell, 152 | stdout=subprocess.PIPE, 153 | stderr=subprocess.PIPE, 154 | ) 155 | except Exception as err: 156 | print(err) 157 | await edit_or_reply( 158 | message, text=f"**ERROR:**\n```{err}```" 159 | ) 160 | output += f"**{code}**\n" 161 | output += process.stdout.read()[:-1].decode("utf-8") 162 | output += "\n" 163 | else: 164 | shell = re.split(""" (?=(?:[^'"]|'[^']*'|"[^"]*")*$)""", text) 165 | for a in range(len(shell)): 166 | shell[a] = shell[a].replace('"', "") 167 | try: 168 | process = subprocess.Popen( 169 | shell, 170 | stdout=subprocess.PIPE, 171 | stderr=subprocess.PIPE, 172 | ) 173 | except Exception as err: 174 | print(err) 175 | exc_type, exc_obj, exc_tb = sys.exc_info() 176 | errors = traceback.format_exception( 177 | etype=exc_type, 178 | value=exc_obj, 179 | tb=exc_tb, 180 | ) 181 | return await edit_or_reply( 182 | message, text=f"**ERROR:**\n```{''.join(errors)}```" 183 | ) 184 | output = process.stdout.read()[:-1].decode("utf-8") 185 | if str(output) == "\n": 186 | output = None 187 | if output: 188 | if len(output) > 4096: 189 | with open("output.txt", "w+") as file: 190 | file.write(output) 191 | await message.reply_document( 192 | "output.txt", caption="`Output`", quote=False 193 | ) 194 | return os.remove("output.txt") 195 | await edit_or_reply( 196 | message, text=f"**OUTPUT:**\n```{output}```" 197 | ) 198 | else: 199 | await edit_or_reply(message, text="**OUTPUT: **\n`No output`") 200 | -------------------------------------------------------------------------------- /spr/modules/info.py: -------------------------------------------------------------------------------- 1 | from time import ctime 2 | 3 | from pyrogram import filters 4 | from pyrogram.types import (InlineQuery, InlineQueryResultArticle, 5 | InputTextMessageContent, Message) 6 | 7 | from spr import SUDOERS, spr 8 | from spr.utils.db import (add_chat, add_user, chat_exists, 9 | get_blacklist_event, get_nsfw_count, 10 | get_reputation, get_user_trust, 11 | is_chat_blacklisted, is_user_blacklisted, 12 | user_exists) 13 | 14 | __MODULE__ = "Info" 15 | __HELP__ = """ 16 | **Get Info About A Chat Or User** 17 | 18 | /info [CHAT_ID/Username|USER_ID/Username] 19 | 20 | or you can use inline mode >> 21 | @SpamProtectionRobot [CHAT_ID/Username|USER_ID/Username] 22 | """ 23 | 24 | 25 | async def get_user_info(user): 26 | try: 27 | user = await spr.get_users(user) 28 | except Exception: 29 | return 30 | if not user_exists(user.id): 31 | add_user(user.id) 32 | trust = get_user_trust(user.id) 33 | blacklisted = is_user_blacklisted(user.id) 34 | reason = None 35 | if blacklisted: 36 | reason, time = get_blacklist_event(user.id) 37 | data = f""" 38 | **ID:** {user.id} 39 | **DC:** {user.dc_id} 40 | **Username:** {user.username} 41 | **Mention: ** {user.mention("Link")} 42 | 43 | **Is Sudo:** {user.id in SUDOERS} 44 | **Trust:** {trust} 45 | **Spammer:** {True if trust < 50 else False} 46 | **Reputation:** {get_reputation(user.id)} 47 | **NSFW Count:** {get_nsfw_count(user.id)} 48 | **Potential Spammer:** {True if trust < 70 else False} 49 | **Blacklisted:** {blacklisted} 50 | """ 51 | data += ( 52 | f"**Blacklist Reason:** {reason} | {ctime(time)}" 53 | if reason 54 | else "" 55 | ) 56 | return data 57 | 58 | 59 | async def get_chat_info(chat): 60 | try: 61 | chat = await spr.get_chat(chat) 62 | except Exception: 63 | return 64 | if not chat_exists(chat.id): 65 | add_chat(chat.id) 66 | blacklisted = is_chat_blacklisted(chat.id) 67 | reason = None 68 | if blacklisted: 69 | reason, time = get_blacklist_event(chat.id) 70 | data = f""" 71 | **ID:** {chat.id} 72 | **Username:** {chat.username} 73 | **Type:** {chat.type} 74 | **Members:** {chat.members_count} 75 | **Scam:** {chat.is_scam} 76 | **Restricted:** {chat.is_restricted} 77 | **Blacklisted:** {blacklisted} 78 | """ 79 | data += ( 80 | f"**Blacklist Reason:** {reason} | {ctime(time)}" 81 | if reason 82 | else "" 83 | ) 84 | return data 85 | 86 | 87 | async def get_info(entity): 88 | user = await get_user_info(entity) 89 | if user: 90 | return user 91 | chat = await get_chat_info(entity) 92 | return chat 93 | 94 | 95 | @spr.on_message(filters.command("info"), group=3) 96 | async def info_func(_, message: Message): 97 | if message.reply_to_message: 98 | reply = message.reply_to_message 99 | user = reply.from_user 100 | entity = user.id or message.chat.id 101 | elif len(message.command) == 1: 102 | user = message.from_user 103 | entity = user.id or message.chat.id 104 | elif len(message.command) == 2: 105 | entity = message.text.split(None, 1)[1] 106 | else: 107 | return await message.reply_text("Read the help menu") 108 | entity = await get_info(entity) 109 | entity = entity or "I haven't seen this chat/user." 110 | await message.reply_text(entity) 111 | 112 | 113 | @spr.on_inline_query() 114 | async def inline_info_func(_, query: InlineQuery): 115 | query_ = query.query.strip() 116 | entity = await get_info(query_) 117 | if not entity: 118 | err = "I haven't seen this user/chat." 119 | results = [ 120 | InlineQueryResultArticle( 121 | err, 122 | input_message_content=InputTextMessageContent(err), 123 | ) 124 | ] 125 | else: 126 | results = [ 127 | InlineQueryResultArticle( 128 | "Found Entity", 129 | input_message_content=InputTextMessageContent(entity), 130 | ) 131 | ] 132 | await query.answer(results=results, cache_time=3) 133 | -------------------------------------------------------------------------------- /spr/modules/manage.py: -------------------------------------------------------------------------------- 1 | from os import remove 2 | 3 | from pyrogram import filters 4 | from pyrogram.types import Message 5 | 6 | from spr import SUDOERS, arq, spr 7 | from spr.utils.db import (disable_nsfw, disable_spam, enable_nsfw, 8 | enable_spam, is_nsfw_enabled, 9 | is_spam_enabled) 10 | from spr.utils.misc import admins, get_file_id 11 | 12 | __MODULE__ = "Manage" 13 | __HELP__ = """ 14 | /anti_nsfw [ENABLE|DISABLE] - Enable or disable NSFW Detection. 15 | /anti_spam [ENABLE|DISABLE] - Enable or disable Spam Detection. 16 | 17 | /nsfw_scan - Classify a media. 18 | /spam_scan - Get Spam predictions of replied message. 19 | """ 20 | 21 | 22 | @spr.on_message( 23 | filters.command("anti_nsfw") & ~filters.private, group=3 24 | ) 25 | async def nsfw_toggle_func(_, message: Message): 26 | if len(message.command) != 2: 27 | return await message.reply_text( 28 | "Usage: /anti_nsfw [ENABLE|DISABLE]" 29 | ) 30 | if message.from_user: 31 | user = message.from_user 32 | chat_id = message.chat.id 33 | if user.id not in SUDOERS and user.id not in ( 34 | await admins(chat_id) 35 | ): 36 | return await message.reply_text( 37 | "You don't have enough permissions" 38 | ) 39 | status = message.text.split(None, 1)[1].strip() 40 | status = status.lower() 41 | chat_id = message.chat.id 42 | if status == "enable": 43 | if is_nsfw_enabled(chat_id): 44 | return await message.reply("Already enabled.") 45 | enable_nsfw(chat_id) 46 | await message.reply_text("Enabled NSFW Detection.") 47 | elif status == "disable": 48 | if not is_nsfw_enabled(chat_id): 49 | return await message.reply("Already disabled.") 50 | disable_nsfw(chat_id) 51 | await message.reply_text("Disabled NSFW Detection.") 52 | else: 53 | await message.reply_text( 54 | "Unknown Suffix, Use /anti_nsfw [ENABLE|DISABLE]" 55 | ) 56 | 57 | 58 | @spr.on_message( 59 | filters.command("anti_spam") & ~filters.private, group=3 60 | ) 61 | async def spam_toggle_func(_, message: Message): 62 | if len(message.command) != 2: 63 | return await message.reply_text( 64 | "Usage: /anti_spam [ENABLE|DISABLE]" 65 | ) 66 | if message.from_user: 67 | user = message.from_user 68 | chat_id = message.chat.id 69 | if user.id not in SUDOERS and user.id not in ( 70 | await admins(chat_id) 71 | ): 72 | return await message.reply_text( 73 | "You don't have enough permissions" 74 | ) 75 | status = message.text.split(None, 1)[1].strip() 76 | status = status.lower() 77 | chat_id = message.chat.id 78 | if status == "enable": 79 | if is_spam_enabled(chat_id): 80 | return await message.reply("Already enabled.") 81 | enable_spam(chat_id) 82 | await message.reply_text("Enabled Spam Detection.") 83 | elif status == "disable": 84 | if not is_spam_enabled(chat_id): 85 | return await message.reply("Already disabled.") 86 | disable_spam(chat_id) 87 | await message.reply_text("Disabled Spam Detection.") 88 | else: 89 | await message.reply_text( 90 | "Unknown Suffix, Use /anti_spam [ENABLE|DISABLE]" 91 | ) 92 | 93 | 94 | @spr.on_message(filters.command("nsfw_scan"), group=3) 95 | async def nsfw_scan_command(_, message: Message): 96 | err = "Reply to an image/document/sticker/animation to scan it." 97 | if not message.reply_to_message: 98 | await message.reply_text(err) 99 | return 100 | reply = message.reply_to_message 101 | if ( 102 | not reply.document 103 | and not reply.photo 104 | and not reply.sticker 105 | and not reply.animation 106 | and not reply.video 107 | ): 108 | await message.reply_text(err) 109 | return 110 | m = await message.reply_text("Scanning") 111 | file_id = get_file_id(reply) 112 | if not file_id: 113 | return await m.edit("Something went wrong.") 114 | file = await spr.download_media(file_id) 115 | try: 116 | results = await arq.nsfw_scan(file=file) 117 | except Exception as e: 118 | return await m.edit(str(e)) 119 | remove(file) 120 | if not results.ok: 121 | return await m.edit(results.result) 122 | results = results.result 123 | await m.edit( 124 | f""" 125 | **Neutral:** `{results.neutral} %` 126 | **Porn:** `{results.porn} %` 127 | **Hentai:** `{results.hentai} %` 128 | **Sexy:** `{results.sexy} %` 129 | **Drawings:** `{results.drawings} %` 130 | **NSFW:** `{results.is_nsfw}` 131 | """ 132 | ) 133 | 134 | 135 | @spr.on_message(filters.command("spam_scan"), group=3) 136 | async def scanNLP(_, message: Message): 137 | if not message.reply_to_message: 138 | return await message.reply("Reply to a message to scan it.") 139 | r = message.reply_to_message 140 | text = r.text or r.caption 141 | if not text: 142 | return await message.reply("Can't scan that") 143 | data = await arq.nlp(text) 144 | data = data.result[0] 145 | msg = f""" 146 | **Is Spam:** {data.is_spam} 147 | **Spam Probability:** {data.spam_probability} % 148 | **Spam:** {data.spam} 149 | **Ham:** {data.ham} 150 | **Profanity:** {data.profanity} 151 | """ 152 | await message.reply(msg, quote=True) 153 | -------------------------------------------------------------------------------- /spr/modules/vote.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | from pyrogram.types import CallbackQuery 3 | 4 | from spr import NSFW_LOG_CHANNEL, SPAM_LOG_CHANNEL, SUDOERS, spr 5 | from spr.core import ikb 6 | from spr.utils.db import downvote, ignore_nsfw, upvote, user_voted 7 | from spr.utils.misc import clean, get_file_unique_id 8 | 9 | 10 | @spr.on_callback_query(filters.regex(r"^upvote_")) 11 | async def upvote_cb_func(_, cq: CallbackQuery): 12 | if cq.message.chat.id not in [SPAM_LOG_CHANNEL, NSFW_LOG_CHANNEL]: 13 | return await cq.answer() 14 | data = cq.data.split("_")[1] 15 | user_id = cq.from_user.id 16 | mid = cq.message.message_id 17 | if data == "spam": 18 | if user_voted(mid, user_id): 19 | return await cq.answer("You've already casted your vote.") 20 | upvote(mid, user_id) 21 | kb = cq.message.reply_markup.inline_keyboard 22 | upvotes = clean(kb[0][0]) 23 | downvotes = clean(kb[0][1]) 24 | link = kb[1][0].url 25 | 26 | keyb = ikb( 27 | { 28 | f"Correct ({upvotes + 1})": "upvote_spam", 29 | f"Incorrect ({downvotes})": "downvote_spam", 30 | "Chat": link, 31 | }, 32 | 2 33 | ) 34 | await cq.edit_message_reply_markup(keyb) 35 | elif data == "nsfw": 36 | if user_id in SUDOERS: 37 | await cq.message.delete() 38 | await cq.answer() 39 | else: 40 | await cq.answer() 41 | 42 | 43 | @spr.on_callback_query(filters.regex(r"^downvote_")) 44 | async def downvote_cb_func(_, cq: CallbackQuery): 45 | if cq.message.chat.id not in [SPAM_LOG_CHANNEL, NSFW_LOG_CHANNEL]: 46 | return await cq.answer() 47 | data = cq.data.split("_")[1] 48 | user_id = cq.from_user.id 49 | mid = cq.message.message_id 50 | 51 | if data == "spam": 52 | if user_voted(mid, user_id): 53 | return await cq.answer("You've already casted your vote.") 54 | downvote(mid, user_id) 55 | kb = cq.message.reply_markup.inline_keyboard 56 | upvotes = clean(kb[0][0]) 57 | downvotes = clean(kb[0][1]) 58 | link = kb[1][0].url 59 | keyb = ikb( 60 | { 61 | f"Correct ({upvotes})": "upvote_spam", 62 | f"Incorrect ({downvotes + 1})": "downvote_spam", 63 | "Chat": link, 64 | }, 65 | 2 66 | ) 67 | await cq.edit_message_reply_markup(keyb) 68 | elif data == "nsfw": 69 | if user_id in SUDOERS: 70 | file_id = get_file_unique_id(cq.message) 71 | ignore_nsfw(file_id) 72 | await cq.message.delete() 73 | await cq.answer() 74 | else: 75 | await cq.answer() 76 | -------------------------------------------------------------------------------- /spr/modules/watcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyrogram import filters 4 | from pyrogram.types import Message 5 | 6 | from spr import SUDOERS, arq, spr 7 | from spr.utils.db import (add_chat, add_user, chat_exists, 8 | is_chat_blacklisted, is_nsfw_downvoted, 9 | is_nsfw_enabled, is_spam_enabled, 10 | is_user_blacklisted, update_spam_data, 11 | user_exists) 12 | from spr.utils.functions import (delete_nsfw_notify, 13 | delete_spam_notify, kick_user_notify) 14 | from spr.utils.misc import admins, get_file_id, get_file_unique_id 15 | 16 | 17 | @spr.on_message( 18 | ( 19 | filters.document 20 | | filters.photo 21 | | filters.sticker 22 | | filters.animation 23 | | filters.video 24 | | filters.text 25 | ) 26 | ) 27 | async def message_watcher(_, message: Message): 28 | user_id = None 29 | chat_id = None 30 | 31 | if message.chat.type in ["group", "supergroup"]: 32 | chat_id = message.chat.id 33 | if not chat_exists(chat_id): 34 | add_chat(chat_id) 35 | if is_chat_blacklisted(chat_id): 36 | await spr.leave_chat(chat_id) 37 | 38 | if message.from_user: 39 | if message.from_user.id: 40 | user_id = message.from_user.id 41 | if not user_exists(user_id): 42 | add_user(user_id) 43 | if is_user_blacklisted(user_id) and chat_id: 44 | if user_id not in (await admins(chat_id)): 45 | await kick_user_notify(message) 46 | 47 | if not chat_id or not user_id: 48 | return 49 | 50 | file_id = get_file_id(message) 51 | file_unique_id = get_file_unique_id(message) 52 | if file_id and file_unique_id: 53 | if user_id in SUDOERS or user_id in (await admins(chat_id)): 54 | return 55 | if is_nsfw_downvoted(file_unique_id): 56 | return 57 | file = await spr.download_media(file_id) 58 | try: 59 | resp = await arq.nsfw_scan(file=file) 60 | except Exception: 61 | try: 62 | return os.remove(file) 63 | except Exception: 64 | return 65 | os.remove(file) 66 | if resp.ok: 67 | if resp.result.is_nsfw: 68 | if is_nsfw_enabled(chat_id): 69 | return await delete_nsfw_notify( 70 | message, resp.result 71 | ) 72 | 73 | text = message.text or message.caption 74 | if not text: 75 | return 76 | resp = await arq.nlp(text) 77 | if not resp.ok: 78 | return 79 | result = resp.result[0] 80 | update_spam_data(user_id, result.spam) 81 | if not result.is_spam: 82 | return 83 | if not is_spam_enabled(chat_id): 84 | return 85 | if user_id in SUDOERS or user_id in (await admins(chat_id)): 86 | return 87 | await delete_spam_notify(message, result.spam_probability) 88 | -------------------------------------------------------------------------------- /spr/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHamkerCat/SpamProtectionRobot/cde850aa922471eb8897d40ff0f1014c5ef349fa/spr/utils/__init__.py -------------------------------------------------------------------------------- /spr/utils/db.py: -------------------------------------------------------------------------------- 1 | # Ignore repetitions 2 | 3 | from json import dumps, loads 4 | from time import time 5 | 6 | from spr import conn 7 | 8 | conn.execute( 9 | """ 10 | CREATE 11 | TABLE 12 | IF NOT EXISTS 13 | users 14 | (user_id, spam_data, nsfw_count, reputation, blacklisted) 15 | """ 16 | ) 17 | 18 | conn.execute( 19 | """ 20 | CREATE 21 | TABLE 22 | IF NOT EXISTS 23 | chats 24 | (chat_id, spam_enabled, nsfw_enabled, blacklisted) 25 | """ 26 | ) 27 | 28 | # For reports in SPAM log channel 29 | conn.execute( 30 | """ 31 | CREATE 32 | TABLE 33 | IF NOT EXISTS 34 | reports 35 | (message_id, upvote, downvote, user_id) 36 | """ 37 | ) 38 | 39 | # For false NSFW reports 40 | conn.execute( 41 | """ 42 | CREATE 43 | TABLE 44 | IF NOT EXISTS 45 | ignored_media 46 | (file_id, time) 47 | """ 48 | ) 49 | 50 | # For blacklist reasons 51 | conn.execute( 52 | """ 53 | CREATE 54 | TABLE 55 | IF NOT EXISTS 56 | reasons 57 | (id, reason, time) 58 | """ 59 | ) 60 | 61 | 62 | def user_exists(user_id: int) -> bool: 63 | """ 64 | CHECK IF A USER EXISTS IN DB 65 | """ 66 | c = conn.cursor() 67 | return c.execute( 68 | """ 69 | SELECT * 70 | FROM users 71 | WHERE user_id=? 72 | """, 73 | (user_id,), 74 | ).fetchone() 75 | 76 | 77 | def chat_exists(chat_id: int) -> bool: 78 | """ 79 | CHECK IF A CHAT EXISTS IN DB 80 | """ 81 | c = conn.cursor() 82 | return c.execute( 83 | """ 84 | SELECT * 85 | FROM chats 86 | WHERE chat_id=? 87 | """, 88 | (chat_id,), 89 | ).fetchone() 90 | 91 | 92 | def add_user(user_id: int): 93 | """ 94 | ADD A USER IN DB 95 | """ 96 | c = conn.cursor() 97 | c.execute( 98 | """ 99 | INSERT 100 | INTO users 101 | VALUES (?, ?, ?, ?, ?) 102 | """, 103 | (user_id, "[]", 0, 0, 0), 104 | ) 105 | 106 | 107 | def add_chat(chat_id: int): 108 | """ 109 | ADD A CHAT IN DB 110 | """ 111 | c = conn.cursor() 112 | c.execute( 113 | """ 114 | INSERT 115 | INTO chats 116 | VALUES (?, ?, ?, ?) 117 | """, 118 | (chat_id, 1, 1, 0), 119 | ) 120 | 121 | 122 | # the below function below gets called on each message update, 123 | # and stores the spam prediction of that message, 124 | # It stores last 50 messages sent by a user. 125 | 126 | 127 | def update_spam_data(user_id: int, spam_value: float): 128 | """ 129 | UPDATE SPAM DATA OF A USER 130 | """ 131 | c = conn.cursor() 132 | data = c.execute( 133 | """ 134 | SELECT spam_data 135 | FROM users 136 | WHERE user_id=? 137 | """, 138 | (user_id,), 139 | ).fetchone()[0] 140 | data = loads(data) 141 | data = data or [] 142 | if len(data) >= 50: 143 | data = data[1:50] 144 | data.append(spam_value) 145 | data = [ 146 | i for i in data if isinstance(i, float) or isinstance(i, int) 147 | ] 148 | data = dumps(data) 149 | c.execute( 150 | """ 151 | UPDATE users 152 | SET spam_data=? 153 | WHERE user_id=? 154 | """, 155 | (data, user_id), 156 | ) 157 | 158 | 159 | def get_user_trust(user_id: int) -> float: 160 | """ 161 | GET TRUST PREDICTION OF A USER 162 | """ 163 | c = conn.cursor() 164 | data = c.execute( 165 | """ 166 | SELECT spam_data 167 | FROM users 168 | WHERE user_id=? 169 | """, 170 | (user_id,), 171 | ).fetchone()[0] 172 | data = loads(data) 173 | return ( 174 | 100 if not data else round((100 - (sum(data) / len(data))), 4) 175 | ) 176 | 177 | 178 | # Each nsfw media user sends, adds 2 spam count and 179 | # decrements his reputation by 10 180 | 181 | 182 | def increment_nsfw_count(user_id: int): 183 | """ 184 | INCREMENT NSFW MESSAGES COUNT OF A USER 185 | """ 186 | c = conn.cursor() 187 | c.execute( 188 | """ 189 | UPDATE users 190 | SET nsfw_count = nsfw_count + 1, 191 | reputation = reputation - 10 192 | WHERE user_id=? 193 | """, 194 | (user_id,), 195 | ) 196 | # no need to commit changes, the function below does that. 197 | return [update_spam_data(user_id, 100) for _ in range(2)] 198 | 199 | 200 | def get_nsfw_count(user_id: int): 201 | """ 202 | GET NSFW MESSAGES COUNT OF A USER 203 | """ 204 | c = conn.cursor() 205 | return c.execute( 206 | """ 207 | SELECT nsfw_count 208 | FROM users 209 | WHERE user_id=? 210 | """, 211 | (user_id,), 212 | ).fetchone()[0] 213 | 214 | 215 | def get_reputation(user_id: int) -> int: 216 | """ 217 | GET REPUTATION OF A USER 218 | """ 219 | c = conn.cursor() 220 | return c.execute( 221 | """ 222 | SELECT reputation 223 | FROM users 224 | WHERE user_id=? 225 | """, 226 | (user_id,), 227 | ).fetchone()[0] 228 | 229 | 230 | def increment_reputation(user_id: int): 231 | """ 232 | INCREMENT REPUTATION OF A USER 233 | """ 234 | c = conn.cursor() 235 | c.execute( 236 | """ 237 | UPDATE users 238 | SET reputation = reputation + 1 239 | WHERE user_id=? 240 | """, 241 | (user_id,), 242 | ) 243 | 244 | 245 | def decrement_reputation(user_id: int): 246 | """ 247 | DECREMENT REPUTATION OF A USER 248 | """ 249 | c = conn.cursor() 250 | c.execute( 251 | """ 252 | UPDATE users 253 | SET reputation = reputation - 1 254 | WHERE user_id=? 255 | """, 256 | (user_id,), 257 | ) 258 | 259 | 260 | def blacklist_user(user_id: int, reason: str): 261 | """ 262 | BLACKLIST A USER 263 | """ 264 | c = conn.cursor() 265 | c.execute( 266 | """ 267 | UPDATE users 268 | SET blacklisted = 1 269 | WHERE user_id=? 270 | """, 271 | (user_id,), 272 | ) 273 | c.execute( 274 | """ 275 | INSERT 276 | INTO reasons 277 | VALUES (?, ?, ?) 278 | """, 279 | (user_id, reason, time()), 280 | ) 281 | return conn.commit() 282 | 283 | 284 | def get_blacklist_event(id: int): 285 | """ 286 | GET REASON AND TIME FOR A BLACKLIST EVENT 287 | """ 288 | c = conn.cursor() 289 | return c.execute( 290 | """ 291 | SELECT reason, time 292 | FROM reasons 293 | WHERE id = ? 294 | """, 295 | (id,), 296 | ).fetchone() 297 | 298 | 299 | def blacklist_chat(chat_id: int, reason: str): 300 | """ 301 | BLACKLIST A CHAT 302 | """ 303 | c = conn.cursor() 304 | c.execute( 305 | """ 306 | UPDATE chats 307 | SET blacklisted = 1 308 | WHERE chat_id=? 309 | """, 310 | (chat_id,), 311 | ) 312 | c.execute( 313 | """ 314 | INSERT 315 | INTO reasons 316 | VALUES (?, ?, ?) 317 | """, 318 | (chat_id, reason, time()), 319 | ) 320 | return conn.commit() 321 | 322 | 323 | def whitelist_user(user_id: int): 324 | """ 325 | WHITELIST A USER 326 | """ 327 | c = conn.cursor() 328 | c.execute( 329 | """ 330 | UPDATE users 331 | SET blacklisted = 0 332 | WHERE user_id=? 333 | """, 334 | (user_id,), 335 | ) 336 | c.execute( 337 | """ 338 | DELETE 339 | FROM reasons 340 | WHERE id = ? 341 | """, 342 | (user_id,), 343 | ) 344 | return conn.commit() 345 | 346 | 347 | def whitelist_chat(chat_id: int): 348 | """ 349 | WHITELIST A CHAT 350 | """ 351 | c = conn.cursor() 352 | c.execute( 353 | """ 354 | UPDATE chats 355 | SET blacklisted = 0 356 | WHERE chat_id=? 357 | """, 358 | (chat_id,), 359 | ) 360 | c.execute( 361 | """ 362 | DELETE 363 | FROM reasons 364 | WHERE id = ? 365 | """, 366 | (chat_id,), 367 | ) 368 | return conn.commit() 369 | 370 | 371 | def is_user_blacklisted(user_id: int) -> bool: 372 | """ 373 | CHECK IF A USER IS BLACKLISTED 374 | """ 375 | c = conn.cursor() 376 | return bool( 377 | c.execute( 378 | """ 379 | SELECT blacklisted 380 | FROM users 381 | WHERE user_id=? 382 | """, 383 | (user_id,), 384 | ).fetchone()[0] 385 | ) 386 | 387 | 388 | def is_chat_blacklisted(chat_id: int) -> bool: 389 | """ 390 | CHECK IF A CHAT IS BLACKLISTED 391 | """ 392 | c = conn.cursor() 393 | return bool( 394 | c.execute( 395 | """ 396 | SELECT blacklisted 397 | FROM chats 398 | WHERE chat_id=? 399 | """, 400 | (chat_id,), 401 | ).fetchone()[0] 402 | ) 403 | 404 | 405 | def is_spam_enabled(chat_id: int) -> bool: 406 | """ 407 | CHECK IF SPAM PROTECTION IS ENABLED IN A CHAT 408 | """ 409 | c = conn.cursor() 410 | return bool( 411 | c.execute( 412 | """ 413 | SELECT spam_enabled 414 | FROM chats 415 | WHERE chat_id=? 416 | """, 417 | (chat_id,), 418 | ).fetchone()[0] 419 | ) 420 | 421 | 422 | def is_nsfw_enabled(chat_id: int) -> bool: 423 | """ 424 | CHECK IF NSFW DETECTION IS ENABLED IN A CHAT 425 | """ 426 | c = conn.cursor() 427 | return bool( 428 | c.execute( 429 | """ 430 | SELECT nsfw_enabled 431 | FROM chats 432 | WHERE chat_id=? 433 | """, 434 | (chat_id,), 435 | ).fetchone()[0] 436 | ) 437 | 438 | 439 | def enable_nsfw(chat_id: int): 440 | """ 441 | ENABLE NSFW DETECTION IN A CHAT 442 | """ 443 | c = conn.cursor() 444 | c.execute( 445 | """ 446 | UPDATE chats 447 | SET nsfw_enabled = 1 448 | WHERE chat_id=? 449 | """, 450 | (chat_id,), 451 | ) 452 | return conn.commit() 453 | 454 | 455 | def disable_nsfw(chat_id: int): 456 | """ 457 | DISABLE NSFW DETECTION IN A CHAT 458 | """ 459 | c = conn.cursor() 460 | c.execute( 461 | """ 462 | UPDATE chats 463 | SET nsfw_enabled = 0 464 | WHERE chat_id=? 465 | """, 466 | (chat_id,), 467 | ) 468 | return conn.commit() 469 | 470 | 471 | def enable_spam(chat_id: int): 472 | """ 473 | ENABLE SPAM PROTECTION IN A CHAT 474 | """ 475 | c = conn.cursor() 476 | c.execute( 477 | """ 478 | UPDATE chats 479 | SET spam_enabled = 1 480 | WHERE chat_id=? 481 | """, 482 | (chat_id,), 483 | ) 484 | return conn.commit() 485 | 486 | 487 | def disable_spam(chat_id: int): 488 | """ 489 | DISABLE SPAM PROTECTION IN A CHAT 490 | """ 491 | c = conn.cursor() 492 | c.execute( 493 | """ 494 | UPDATE chats 495 | SET spam_enabled = 0 496 | WHERE chat_id=? 497 | """, 498 | (chat_id,), 499 | ) 500 | return conn.commit() 501 | 502 | 503 | def upvote(message_id: int, user_id: int): 504 | """ 505 | UPVOTE A DETECTION REPORT 506 | """ 507 | c = conn.cursor() 508 | c.execute( 509 | """ 510 | INSERT 511 | INTO reports 512 | VALUES (?, ?, ?, ?) 513 | """, 514 | (message_id, 1, 0, user_id), 515 | ) 516 | return increment_reputation(user_id) 517 | 518 | 519 | def downvote(message_id: int, user_id: int): 520 | """ 521 | DOWNVOTE A DETECTION REPORT 522 | """ 523 | c = conn.cursor() 524 | c.execute( 525 | """ 526 | INSERT 527 | INTO reports 528 | VALUES (?, ?, ?, ?) 529 | """, 530 | (message_id, 0, 1, user_id), 531 | ) 532 | return increment_reputation(user_id) 533 | 534 | 535 | def user_voted(message_id: int, user_id: int) -> bool: 536 | """ 537 | CHECK IF A USER VOTED TO A DETECTION REPORT 538 | """ 539 | c = conn.cursor() 540 | return bool( 541 | c.execute( 542 | """ 543 | SELECT * 544 | FROM reports 545 | WHERE message_id=? AND user_id=? 546 | """, 547 | (message_id, user_id), 548 | ).fetchone() 549 | ) 550 | 551 | 552 | def ignore_nsfw(file_id: str): 553 | """ 554 | IGNORE NSFW FALSE REPORTS 555 | """ 556 | c = conn.cursor() 557 | c.execute( 558 | """ 559 | INSERT 560 | INTO 561 | ignored_media 562 | VALUES (?, ?) 563 | """, 564 | (file_id, int(time())), 565 | ) 566 | return conn.commit() 567 | 568 | 569 | def is_nsfw_downvoted(file_id: str) -> bool: 570 | """ 571 | CHECK IF NSFW IS MARKED AS FALSE IN DB 572 | """ 573 | c = conn.cursor() 574 | return c.execute( 575 | """ 576 | SELECT * 577 | FROM ignored_media 578 | WHERE file_id = ? 579 | """, 580 | (file_id,), 581 | ).fetchone() 582 | -------------------------------------------------------------------------------- /spr/utils/functions.py: -------------------------------------------------------------------------------- 1 | from time import ctime 2 | 3 | from pyrogram.errors import (ChatAdminRequired, ChatWriteForbidden, 4 | UserAdminInvalid) 5 | from pyrogram.types import Message 6 | 7 | from spr import NSFW_LOG_CHANNEL, SPAM_LOG_CHANNEL, spr 8 | from spr.core import ikb 9 | from spr.utils.db import (get_blacklist_event, get_nsfw_count, 10 | get_reputation, get_user_trust, 11 | increment_nsfw_count, is_user_blacklisted) 12 | 13 | 14 | async def get_user_info(message): 15 | user = message.from_user 16 | trust = get_user_trust(user.id) 17 | user_ = f"{('@' + user.username) if user.username else user.mention} [`{user.id}`]" 18 | blacklisted = is_user_blacklisted(user.id) 19 | reason = None 20 | if blacklisted: 21 | reason, time = get_blacklist_event(user.id) 22 | data = f""" 23 | **User:** 24 | **Username:** {user_} 25 | **Trust:** {trust} 26 | **Spammer:** {True if trust < 50 else False} 27 | **Reputation:** {get_reputation(user.id)} 28 | **NSFW Count:** {get_nsfw_count(user.id)} 29 | **Potential Spammer:** {True if trust < 70 else False} 30 | **Blacklisted:** {is_user_blacklisted(user.id)} 31 | """ 32 | data += ( 33 | f" **Blacklist Reason:** {reason} | {ctime(time)}" 34 | if reason 35 | else "" 36 | ) 37 | return data 38 | 39 | 40 | async def delete_get_info(message: Message): 41 | try: 42 | await message.delete() 43 | except (ChatAdminRequired, UserAdminInvalid): 44 | try: 45 | return await message.reply_text( 46 | "I don't have enough permission to delete " 47 | + "this message which is Flagged as Spam." 48 | ) 49 | except ChatWriteForbidden: 50 | return await spr.leave_chat(message.chat.id) 51 | return await get_user_info(message) 52 | 53 | 54 | async def delete_nsfw_notify( 55 | message: Message, 56 | result, 57 | ): 58 | await message.copy( 59 | NSFW_LOG_CHANNEL, 60 | reply_markup=ikb( 61 | {"Correct": "upvote_nsfw", "Incorrect": "downvote_nsfw"} 62 | ), 63 | ) 64 | info = await delete_get_info(message) 65 | if not info: 66 | return 67 | msg = f""" 68 | 🚨 **NSFW ALERT** 🚔 69 | {info} 70 | **Prediction:** 71 | **Safe:** `{result.neutral} %` 72 | **Porn:** `{result.porn} %` 73 | **Adult:** `{result.sexy} %` 74 | **Hentai:** `{result.hentai} %` 75 | **Drawings:** `{result.drawings} %` 76 | """ 77 | await spr.send_message(message.chat.id, text=msg) 78 | increment_nsfw_count(message.from_user.id) 79 | 80 | 81 | async def delete_spam_notify( 82 | message: Message, 83 | spam_probability: float, 84 | ): 85 | info = await delete_get_info(message) 86 | if not info: 87 | return 88 | msg = f""" 89 | 🚨 **SPAM ALERT** 🚔 90 | {info} 91 | **Spam Probability:** {spam_probability} % 92 | 93 | __Message has been deleted__ 94 | """ 95 | content = message.text or message.caption 96 | content = content[:400] + "..." 97 | report = f""" 98 | **SPAM DETECTION** 99 | {info} 100 | **Content:** 101 | {content} 102 | """ 103 | 104 | keyb = ikb( 105 | { 106 | "Correct (0)": "upvote_spam", 107 | "Incorrect (0)": "downvote_spam", 108 | "Chat": "https://t.me/" + (message.chat.username or "SpamProtectionLog/93"), 109 | }, 110 | 2 111 | ) 112 | m = await spr.send_message( 113 | SPAM_LOG_CHANNEL, 114 | report, 115 | reply_markup=keyb, 116 | disable_web_page_preview=True, 117 | ) 118 | 119 | keyb = ikb({"View Message": m.link}) 120 | await spr.send_message( 121 | message.chat.id, text=msg, reply_markup=keyb 122 | ) 123 | 124 | 125 | async def kick_user_notify(message: Message): 126 | try: 127 | await spr.ban_chat_member( 128 | message.chat.id, message.from_user.id 129 | ) 130 | except (ChatAdminRequired, UserAdminInvalid): 131 | try: 132 | return await message.reply_text( 133 | "I don't have enough permission to ban " 134 | + "this user who is Blacklisted and Flagged as Spammer." 135 | ) 136 | except ChatWriteForbidden: 137 | return await spr.leave_chat(message.chat.id) 138 | info = await get_user_info(message) 139 | msg = f""" 140 | 🚨 **SPAMMER ALERT** 🚔 141 | {info} 142 | 143 | __User has been banned__ 144 | """ 145 | await spr.send_message(message.chat.id, msg) 146 | -------------------------------------------------------------------------------- /spr/utils/misc.py: -------------------------------------------------------------------------------- 1 | from asyncio import gather, sleep 2 | from datetime import datetime 3 | from math import ceil 4 | from time import time, ctime 5 | 6 | from pyrogram import enums 7 | from pyrogram.types import InlineKeyboardButton, ChatMemberUpdated 8 | 9 | 10 | from spr import DB_NAME, SESSION_NAME, SUDOERS, spr 11 | from spr.utils.db import conn 12 | 13 | async def backup(): 14 | for user in SUDOERS: 15 | try: 16 | await gather( 17 | spr.send_document(user, DB_NAME), 18 | spr.send_document(user, SESSION_NAME + ".session"), 19 | ) 20 | except Exception: 21 | pass 22 | 23 | admins_in_chat = {} 24 | 25 | 26 | async def admins(chat_id: int): 27 | global admins_in_chat 28 | if chat_id in admins_in_chat: 29 | interval = time() - admins_in_chat[chat_id]["last_updated_at"] 30 | if interval < 3600: 31 | return admins_in_chat[chat_id]["data"] 32 | 33 | admins_in_chat[chat_id] = { 34 | "last_updated_at": time(), 35 | "data": [ 36 | member.user.id 37 | async for member in spr.get_chat_members( 38 | chat_id, filter=enums.ChatMembersFilter.ADMINISTRATORS 39 | ) 40 | ], 41 | } 42 | return admins_in_chat[chat_id]["data"] 43 | 44 | 45 | # Admin cache reload 46 | 47 | 48 | @spr.on_chat_member_updated() 49 | async def admin_cache_func(_, cmu: ChatMemberUpdated): 50 | if cmu.old_chat_member and cmu.old_chat_member.promoted_by: 51 | admins_in_chat[cmu.chat.id] = { 52 | "last_updated_at": time(), 53 | "data": [ 54 | member.user.id 55 | async for member in spr.get_chat_members( 56 | cmu.chat.id, filter=enums.ChatMembersFilter.ADMINISTRATORS 57 | ) 58 | ], 59 | } 60 | print(f"Updated admin cache for {cmu.chat.id} [{cmu.chat.title}]") 61 | 62 | 63 | async def once_a_minute(): 64 | while True: 65 | conn.commit() 66 | print(f"Commited to database at {ctime(time())}") 67 | await sleep(60) 68 | 69 | 70 | async def once_a_day(): 71 | print("BACKING UP DB...") 72 | await backup() 73 | dt = datetime.now() 74 | seconds_till_twelve = ( 75 | ((24 - dt.hour - 1) * 60 * 60) 76 | + ((60 - dt.minute - 1) * 60) 77 | + (60 - dt.second) 78 | ) 79 | print( 80 | "BACKED UP, NEXT BACKUP WILL HAPPEN AFTER " 81 | + f"{round(seconds_till_twelve/60/60, 4)} HOUR(S)" 82 | ) 83 | await sleep(int(seconds_till_twelve)) # Sleep till 12 AM 84 | while True: 85 | print("DB BACKED UP!, NEXT BACKUP WILL HAPPEN AFTER 24 HOURS") 86 | await backup() 87 | await sleep(86400) # sleep for a day 88 | 89 | 90 | def get_file_id(message): 91 | if message.document: 92 | if int(message.document.file_size) > 3145728: 93 | return 94 | mime_type = message.document.mime_type 95 | if mime_type != "image/png" and mime_type != "image/jpeg": 96 | return 97 | return message.document.file_id 98 | 99 | if message.sticker: 100 | if message.sticker.is_animated: 101 | if not message.sticker.thumbs: 102 | return 103 | return message.sticker.thumbs[0].file_id 104 | return message.sticker.file_id 105 | 106 | if message.photo: 107 | return message.photo.file_id 108 | 109 | if message.animation: 110 | if not message.animation.thumbs: 111 | return 112 | return message.animation.thumbs[0].file_id 113 | 114 | if message.video: 115 | if not message.video.thumbs: 116 | return 117 | return message.video.thumbs[0].file_id 118 | 119 | 120 | def get_file_unique_id(message): 121 | m = message 122 | m = m.sticker or m.video or m.document or m.animation or m.photo 123 | if not m: 124 | return 125 | return m.file_unique_id 126 | 127 | 128 | class EqInlineKeyboardButton(InlineKeyboardButton): 129 | def __eq__(self, other): 130 | return self.text == other.text 131 | 132 | def __lt__(self, other): 133 | return self.text < other.text 134 | 135 | def __gt__(self, other): 136 | return self.text > other.text 137 | 138 | 139 | def paginate_modules(page_n, module_dict, prefix, chat=None): 140 | if not chat: 141 | modules = sorted( 142 | [ 143 | EqInlineKeyboardButton( 144 | x.__MODULE__, 145 | callback_data="{}_module({})".format( 146 | prefix, x.__MODULE__.lower() 147 | ), 148 | ) 149 | for x in module_dict.values() 150 | ] 151 | ) 152 | else: 153 | modules = sorted( 154 | [ 155 | EqInlineKeyboardButton( 156 | x.__MODULE__, 157 | callback_data="{}_module({},{})".format( 158 | prefix, chat, x.__MODULE__.lower() 159 | ), 160 | ) 161 | for x in module_dict.values() 162 | ] 163 | ) 164 | 165 | pairs = list(zip(modules[::3], modules[1::3], modules[2::3])) 166 | i = 0 167 | for m in pairs: 168 | for _ in m: 169 | i += 1 170 | if len(modules) - i == 1: 171 | pairs.append((modules[-1],)) 172 | elif len(modules) - i == 2: 173 | pairs.append( 174 | ( 175 | modules[-2], 176 | modules[-1], 177 | ) 178 | ) 179 | 180 | max_num_pages = ceil(len(pairs) / 7) 181 | modulo_page = page_n % max_num_pages 182 | 183 | # can only have a certain amount of buttons side by side 184 | if len(pairs) > 7: 185 | pairs = pairs[modulo_page * 7 : 7 * (modulo_page + 1)] + [ 186 | ( 187 | EqInlineKeyboardButton( 188 | "<", 189 | callback_data="{}_prev({})".format( 190 | prefix, modulo_page 191 | ), 192 | ), 193 | EqInlineKeyboardButton( 194 | ">", 195 | callback_data="{}_next({})".format( 196 | prefix, modulo_page 197 | ), 198 | ), 199 | ) 200 | ] 201 | 202 | return pairs 203 | 204 | 205 | """ TO GET UPVOTES/DOWNVOTES FROM MESSAGE """ 206 | clean = lambda x: int( 207 | x.text.split()[1].replace("(", "").replace(")", "") 208 | ) 209 | --------------------------------------------------------------------------------