├── .env.dist ├── .gitignore ├── Dockerfile ├── README.md ├── app.py ├── data ├── __init__.py ├── config.py ├── emojies.py ├── permissions.py └── phrases.py ├── docker-compose.yaml ├── handlers ├── __init__.py ├── errors │ ├── __init__.py │ └── error_handler.py ├── essential │ ├── __init__.py │ └── misc.py ├── group │ ├── __init__.py │ └── main_logic.py └── private │ ├── __init__.py │ ├── basic.py │ └── quick_guide.py ├── keyboards ├── __init__.py └── inline │ ├── __init__.py │ └── guardian_keyboard.py ├── loader.py ├── middlewares ├── __init__.py └── throttling.py ├── requirements.txt ├── states ├── ConfirmUserState.py └── __init__.py └── utils ├── __init__.py ├── logger_config.py ├── misc ├── __init__.py ├── phrase_generator.py └── throttling.py ├── notify_admins.py └── set_bot_commands.py /.env.dist: -------------------------------------------------------------------------------- 1 | BOT_TOKEN= 2 | ADMINS_ID=525340304 3 | 4 | # Необязательныеагументы 5 | SKIP_UPDATES=True 6 | NUM_BUTTONS=2 7 | ENTRY_TIME=5 8 | BAN_TIME=30 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea/$CACHE_FILE$ 131 | .idea/.gitignore 132 | .idea/aiogram-bot-template.iml 133 | .idea/codeStyles/ 134 | .idea/deployment.xml 135 | .idea/dictionaries 136 | .idea/inspectionProfiles/ 137 | .idea/misc.xml 138 | .idea/modules.xml 139 | .idea/vagrant.xml 140 | .idea/vcs.xml 141 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.3 2 | 3 | RUN mkdir -p /usr/src/antirobot 4 | WORKDIR /usr/src/antirobot 5 | 6 | COPY . /usr/src/antirobot 7 | 8 | RUN pip install -r requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Antirobot 2 | 3 | Телеграм бот для блокировки спама. 4 | 5 | ## Принцип работы 6 | 7 | При входе нового пользователя в чат, ему отправляется сообщение 8 | с просьбой выбрать один _emoji_ из нескольких предложенных. 9 | 10 | Список emoji находится в файле *data/emojies.py*, шаблоны сообщений в *data/phrases.py*. 11 | 12 | ## Необходимые разрешения 13 | 14 | Боту требуются права администратора с возможностями: 15 | 16 | - Удалять сообщения 17 | - Блокировать участников 18 | 19 | ## Docker 20 | 21 | `docker-compose up -d` 22 | 23 | ## Установка и запуск 24 | 25 | Переименовать `.env.dist` в `.env` 26 | 27 | ```shell 28 | python -m venv venv 29 | source venv/bin/activate 30 | # venv\bin\activate.bat - для Windows 31 | pip install -r requirements.txt 32 | ``` 33 | 34 | ```shell 35 | python app.py 36 | ``` 37 | 38 | Настройка логирования в файле *utils/logger_config.py* 39 | 40 | ## Environment 41 | 42 | | Переменная | Тип | По умолчанию | Обязательная | 43 | |--------------|-------------|--------------|--------------| 44 | | BOT_TOKEN | str | | Да | 45 | | ADMINS_ID | list of IDs | | Да | 46 | | SKIP_UPDATES | bool | False | Нет | 47 | | NUM_BUTTONS | int | 5 | Нет | 48 | | ENTRY_TIME | int | 300 | Нет | 49 | | BAN_TIME | int | 30 | Нет | 50 | 51 | `BOT_TOKEN` - 52 | `ADMINS_ID` - 53 | `SKIP_UPDATES` - 54 | `NUM_BUTTONS` - кол-во предлагаемых вариантов emoji в сообщении от бота. От 2 до 7 включительно. 55 | `ENTRY_TIME` - 56 | `BAN_TIME` - 57 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | from aiogram import executor 3 | 4 | from data.config import SKIP_UPDATES, NUM_BUTTONS 5 | from loguru import logger 6 | from loader import dp 7 | 8 | from utils.notify_admins import on_startup_notify 9 | from utils.set_bot_commands import set_default_commands 10 | from utils.logger_config import setup_logger 11 | from middlewares import setup_middlewares 12 | 13 | 14 | async def on_startup(dispatcher: Dispatcher): 15 | setup_logger() 16 | logger.info("Установка обработчиков...") 17 | # Установка обработчиков производится посредством декораторов. Для этого достаточно просто импортировать модуль 18 | import handlers 19 | setup_middlewares(dispatcher) 20 | 21 | await on_startup_notify(dispatcher) 22 | await set_default_commands(dispatcher) 23 | logger.info(f"Бот успешно запущен...") 24 | 25 | 26 | if __name__ == '__main__': 27 | if NUM_BUTTONS in range(2, 8): 28 | executor.start_polling(dp, on_startup=on_startup, skip_updates=SKIP_UPDATES) 29 | else: 30 | raise AttributeError('количество кнопок не может быть меньше 2х или больше 7и') 31 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/antirobot_aiogram/02c85bcad6975473b5d264ed90d10515ad4b3625/data/__init__.py -------------------------------------------------------------------------------- /data/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from environs import Env 4 | 5 | env = Env() 6 | env.read_env() 7 | 8 | BOT_TOKEN = env("BOT_TOKEN") 9 | ADMINS_ID = env.list("ADMINS_ID") 10 | 11 | 12 | SKIP_UPDATES = env.bool("SKIP_UPDATES", False) 13 | NUM_BUTTONS = env.int("NUM_BUTTONS", 5) 14 | ENTRY_TIME = env.int("ENTRY_TIME", 300) 15 | BAN_TIME = env.int("BAN_TIME", 30) 16 | 17 | WORK_PATH = os.getcwd() 18 | -------------------------------------------------------------------------------- /data/emojies.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | __all__ = [ 5 | 'emojies' 6 | ] 7 | 8 | Emoji = namedtuple('Emoji', ['unicode', 'subject', 'name']) 9 | 10 | 11 | emojies = ( 12 | Emoji(unicode=u'\U0001F48D', subject='ring', name='кольцо'), 13 | Emoji(unicode=u'\U0001F460', subject='shoe', name='туфлю'), 14 | Emoji(unicode=u'\U0001F451', subject='crown', name='корону'), 15 | Emoji(unicode=u'\U00002702', subject='scissors', name='ножницы'), 16 | Emoji(unicode=u'\U0001F941', subject='drum', name='барабан'), 17 | 18 | Emoji(unicode=u'\U0001F48A', subject='pill', name='пилюлю'), 19 | Emoji(unicode=u'\U0001F338', subject='blossom', name='цветок'), 20 | Emoji(unicode=u'\U0001F9C0', subject='cheese', name='сыр'), 21 | Emoji(unicode=u'\U0001F3A7', subject='headphone', name='наушники'), 22 | Emoji(unicode=u'\U000023F0', subject='clock', name='будильник'), 23 | 24 | Emoji(unicode=u'\U0001F951', subject='avocado', name='авокадо'), 25 | Emoji(unicode=u'\U0001F334', subject='palm', name='пальму'), 26 | Emoji(unicode=u'\U0001F45C', subject='handbag', name='сумку'), 27 | Emoji(unicode=u'\U0001F9E6', subject='socks', name='носки'), 28 | Emoji(unicode=u'\U0001FA93', subject='axe', name='топор'), 29 | 30 | Emoji(unicode=u'\U0001F308', subject='rainbow', name='радугу'), 31 | Emoji(unicode=u'\U0001F4A7', subject='droplet', name='каплю'), 32 | Emoji(unicode=u'\U0001F525', subject='fire', name='огонь'), 33 | Emoji(unicode=u'\U000026C4', subject='snowman', name='снеговика'), 34 | Emoji(unicode=u'\U0001F9F2', subject='magnet', name='магнит'), 35 | 36 | Emoji(unicode=u'\U0001F389', subject='popper', name='хлопушку'), 37 | Emoji(unicode=u'\U0001F339', subject='rose', name='розу'), 38 | Emoji(unicode=u'\U0000270E', subject='pencil', name='карандаш'), 39 | Emoji(unicode=u'\U00002709', subject='envelope', name='конверт'), 40 | Emoji(unicode=u'\U0001F680', subject='rocket', name='ракету'), 41 | ) 42 | -------------------------------------------------------------------------------- /data/permissions.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | 3 | # Права пользователя, только вошедшего в чат 4 | new_user_added = types.ChatPermissions( 5 | can_send_messages=False, 6 | can_send_media_messages=False, 7 | can_send_polls=False, 8 | can_send_other_messages=False, 9 | can_add_web_page_previews=False, 10 | can_invite_users=False, 11 | can_change_info=False, 12 | can_pin_messages=False, 13 | ) 14 | 15 | # Права пользователя, подтвердившего, что он не бот 16 | user_allowed = types.ChatPermissions( 17 | can_send_messages=True, 18 | can_send_media_messages=True, 19 | can_send_polls=True, 20 | can_send_other_messages=True, 21 | can_add_web_page_previews=True, 22 | can_invite_users=True, 23 | can_change_info=False, 24 | can_pin_messages=False, 25 | ) 26 | 27 | __all__ = [ 28 | 'new_user_added', 29 | 'user_allowed', 30 | ] 31 | -------------------------------------------------------------------------------- /data/phrases.py: -------------------------------------------------------------------------------- 1 | """ 2 | Кортежи с фразами для вывода бота: 3 | 4 | :var users_entrance: используется при приветствии вошедшего пользователя. 5 | Обязан иметь {mention} и {subject} в каждом из элементов. 6 | Вместо {mention} будет вставлено обращение к пользователю, 7 | а вместо {subject} -- объет, на который нужно нажать (В винительном падеже) 8 | 9 | :var throttled_answers: используется когда пользователь спамит коммандами бота. Стандартный Throttling. 10 | Обязан иметь {limit} в каждом из элементов. 11 | На его месте установится число, которое говорит о том, сколько секунд бот игнорит пользователя 12 | 13 | """ 14 | 15 | users_entrance = ( 16 | '{mention}, добро пожаловать в чат!\nНажми на {subject} чтобы получить доступ к сообщениям', 17 | 'А, {mention}, это снова ты? А, извините, обознался. Нажмите на {subject} и можете пройти', 18 | 'Братишь, {mention}, я тебя так долго ждал. Жми на {subject} и пробегай', 19 | 'Разве это не тот {mention}? Тыкай на {subject} и проходи, пожалуйста, мы ждали', 20 | 'Даже не верится, что это ты, {mention}. Мне сказали не пускать ботов, поэтому нажми на {subject}', 21 | 22 | '{mention}, это правда ты? Мы тебя ждали. Или не ты? Настоящий {mention} сможет нажать на {subject}. ' + 23 | 'Докажи, что ты не бот!', 24 | 'Кого я вижу? Это же {mention}! Тыкай на {subject} и можешь идти', 25 | 'Идёт проверка {mention}.\nПроверка почти завершена. Чтобы продолжить, {mention}, пожалуйста нажмите на {subject}', 26 | 'О, {mention}, мы тебя ждали. Докажи что ты не бот и проходи. Для этого нажми на {subject}', 27 | 'Да {mention}, ты меня уже бесишь! А, прошу прощения, обознался. Чтобы я мог вас впустить, нажмите на {subject}' 28 | ) 29 | 30 | throttled_answers = ( 31 | 'Вот чё ты спамишь? Ну всё, я обиделся на {limit} секунд', 32 | 'Админы, я бы забанил этого спамера... Спамершу... В общем ЭТО! А я игнорю это {limit} секунд', 33 | 'Ты сейчас серьёзно? Ну это бан на {limit} с. И даже не пытайся меня отговорить', 34 | 'Да ты шутишь?! А если я буду спамить, приятно тебе будет? Игнор на {limit} секунд.', 35 | 'Да пошёл ты. Пошла. Всё в общем, это конец, {limit} секунд пошло', 36 | 37 | 'Да ты можешь меня хоть {limit} секунд не трогать? Задолбали \U0001F620', 38 | 'Опять ты? Не не не. Подожди {limit} секунд хотябы, потом поговорим \U0001F612', 39 | 'Ещё одно сообщение и меня вырвет! Дай мне отойти {limit} секунд \U0001F922', 40 | 'Опять? Блин... Дай мне {limit} секунд \U0001F635', 41 | 'А ты уже спрашивал. Вот не отвечу! \U0001F92A' 42 | ) 43 | 44 | 45 | __all__ = [ 46 | 'users_entrance', 47 | 'throttled_answers', 48 | ] 49 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | antirobot_aiogram: 5 | build: . 6 | restart: always 7 | env_file: 8 | - .env 9 | command: python app.py 10 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import dp 2 | from .essential import dp 3 | from .group import dp 4 | from .private import dp 5 | 6 | 7 | __all__ = ["dp"] 8 | -------------------------------------------------------------------------------- /handlers/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from .error_handler import dp 2 | 3 | __all__ = ["dp"] -------------------------------------------------------------------------------- /handlers/errors/error_handler.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | 3 | from loader import dp 4 | 5 | 6 | @dp.errors_handler() 7 | async def errors_handler(update, exception): 8 | """ 9 | Exceptions handler. Catches all exceptions within task factory tasks. 10 | :param dispatcher: 11 | :param update: 12 | :param exception: 13 | :return: stdout logging 14 | """ 15 | from aiogram.utils.exceptions import (Unauthorized, InvalidQueryID, TelegramAPIError, 16 | CantDemoteChatCreator, MessageNotModified, MessageToDeleteNotFound, 17 | MessageTextIsEmpty, RetryAfter, 18 | CantParseEntities, MessageCantBeDeleted) 19 | 20 | if isinstance(exception, CantDemoteChatCreator): 21 | logger.debug("Can't demote chat creator") 22 | return True 23 | 24 | if isinstance(exception, MessageNotModified): 25 | logger.debug('Message is not modified') 26 | return True 27 | if isinstance(exception, MessageCantBeDeleted): 28 | logger.debug('Message cant be deleted') 29 | return True 30 | 31 | if isinstance(exception, MessageToDeleteNotFound): 32 | logger.debug('Message to delete not found') 33 | return True 34 | 35 | if isinstance(exception, MessageTextIsEmpty): 36 | logger.debug('MessageTextIsEmpty') 37 | return True 38 | 39 | if isinstance(exception, Unauthorized): 40 | logger.info(f'Unauthorized: {exception}') 41 | return True 42 | 43 | if isinstance(exception, InvalidQueryID): 44 | logger.exception(f'InvalidQueryID: {exception} \nUpdate: {update}') 45 | return True 46 | 47 | if isinstance(exception, TelegramAPIError): 48 | logger.exception(f'TelegramAPIError: {exception} \nUpdate: {update}') 49 | return True 50 | if isinstance(exception, RetryAfter): 51 | logger.exception(f'RetryAfter: {exception} \nUpdate: {update}') 52 | return True 53 | if isinstance(exception, CantParseEntities): 54 | logger.exception(f'CantParseEntities: {exception} \nUpdate: {update}') 55 | return True 56 | logger.exception(f'Update: {update} \n{exception}') 57 | -------------------------------------------------------------------------------- /handlers/essential/__init__.py: -------------------------------------------------------------------------------- 1 | from .misc import dp 2 | 3 | __all__ = ['dp'] -------------------------------------------------------------------------------- /handlers/essential/misc.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters import Command 3 | from aiogram.utils.markdown import hlink 4 | from loguru import logger 5 | 6 | from loader import dp 7 | from utils.misc import rate_limit 8 | 9 | 10 | @rate_limit(limit=60) 11 | @dp.message_handler(Command('developer')) 12 | async def developer(message: types.Message): 13 | logger.debug(f'User @{message.from_user.username}:{message.from_user.id} looking for a developer') 14 | await message.answer(f'Меня создал {hlink(title="Forzend", url="tg://user?id=525340304")}') 15 | -------------------------------------------------------------------------------- /handlers/group/__init__.py: -------------------------------------------------------------------------------- 1 | from .main_logic import dp 2 | 3 | __all__ = ["dp"] 4 | -------------------------------------------------------------------------------- /handlers/group/main_logic.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import asyncio 3 | 4 | from aiogram.utils.exceptions import CantRestrictSelf 5 | 6 | from aiogram import types 7 | from loguru import logger 8 | 9 | from data.config import ENTRY_TIME 10 | from data.config import BAN_TIME 11 | 12 | from data.permissions import new_user_added 13 | from data.permissions import user_allowed 14 | from keyboards.inline import generate_confirm_markup 15 | from keyboards.inline import confirming_callback 16 | from loader import bot 17 | from loader import dp 18 | from loader import storage 19 | from states import ConfirmUserState 20 | from utils.misc import users_entrance_generator 21 | 22 | 23 | @dp.message_handler(chat_type=['group', 'supergroup'], content_types=types.ContentTypes.NEW_CHAT_MEMBERS) 24 | async def new_chat_member(message: types.Message): 25 | """ 26 | Обрабатываем вход нового пользователя 27 | """ 28 | 29 | logger.debug( 30 | f"New chat member: @{message.from_user.username}:{message.from_user.id} -> " 31 | f"{', '.join([f'@{user.username}:{user.id}' for user in message.new_chat_members])} " 32 | f'in chat "{message.chat.title}@{message.chat.username}" chat_id:{message.chat.id}' 33 | ) 34 | # Пропускаем старые запросы 35 | if message.date < datetime.datetime.now() - datetime.timedelta(minutes=1): 36 | return logger.debug('Old updates was skipped') 37 | 38 | for new_member in message.new_chat_members: 39 | try: 40 | # сразу выдаём ему права, неподтверждённого пользователя 41 | await bot.restrict_chat_member( 42 | chat_id=message.chat.id, 43 | user_id=new_member.id, 44 | permissions=new_user_added, 45 | ) 46 | logger.debug(f'User @{new_member.username}:{new_member.id} cannot send messages now') 47 | except CantRestrictSelf: 48 | return logger.debug('Can\'t restrict self') 49 | 50 | service_messages = list() 51 | 52 | # Каждому пользователю отсылаем кнопку 53 | for new_member in message.new_chat_members: 54 | generated_tuple = generate_confirm_markup(new_member.id) 55 | markup = generated_tuple[0] 56 | subject = generated_tuple[1] 57 | answer = users_entrance_generator(mention=new_member.get_mention(as_html=True), subject=subject) 58 | service_message: types.Message = await message.reply( 59 | text=answer, 60 | reply_markup=markup 61 | ) 62 | logger.debug(f'User @{new_member.username}:{new_member.id} ' 63 | f'got message {service_message.message_id} with keyboard') 64 | await storage.set_state(chat=message.chat.id, user=new_member.id, state=ConfirmUserState.IncomerUser) 65 | logger.debug(f'User @{new_member.username}:{new_member.id} in state "IncomerUser"') 66 | state = dp.current_state(user=new_member.id, chat=message.chat.id) 67 | await state.update_data(user_id=new_member.id) 68 | logger.debug(f'@{new_member.username}:{new_member.id} user data has been updated') 69 | service_messages.append(service_message) 70 | 71 | logger.debug(f'The bot waits {ENTRY_TIME} seconds ' 72 | f'for {", ".join([str(user.username) for user in message.new_chat_members])}') 73 | await asyncio.sleep(ENTRY_TIME) 74 | for new_member in message.new_chat_members: 75 | state = dp.current_state(user=new_member.id, chat=message.chat.id) 76 | data = await state.get_data() 77 | if data.get('user_id', None): 78 | logger.debug(f'User @{new_member.username}:{new_member.id} data: {data}') 79 | until_date = datetime.datetime.now() + datetime.timedelta(seconds=BAN_TIME) 80 | # Получаем информацию о пользователе и провереям не заблокировал ли его кто-то, 81 | # пока бот ожидал ответа на капчу. 82 | # asyncio так как требуются свежие данные, а не на время входа в цикл. 83 | asyncio.user = await bot.get_chat_member(chat_id=message.chat.id, user_id=new_member.id) 84 | if asyncio.user['status'] == 'kicked': 85 | # Если пользователь заблокирован в чате то ничего не делаем и просто выдаём сообщение в консоль 86 | logger.debug(f'User @{new_member.username}:{new_member.id} already kicked from the chat \ 87 | ("{message.chat.title}@{message.chat.username}" chat_id:{message.chat.id}) by other bot or admin') 88 | else: 89 | # Если пользователь не был заблокирован то назначается его блокировка на указанное в конфигурации время 90 | await bot.kick_chat_member(chat_id=message.chat.id, user_id=new_member.id, until_date=until_date) 91 | logger.debug(f'User was kicked from chat @{new_member.username}:{new_member.id} on {BAN_TIME} seconds') 92 | # Завершаем состояние "новый пользователь", если пользователь просто проигнорировал капчу 93 | state = dp.current_state(user=new_member.id, chat=message.chat.id) 94 | await state.finish() 95 | logger.debug(f'User @{new_member.username}:{new_member.id} is out of the state') 96 | 97 | for service_message in service_messages: 98 | logger.debug(f'Message {service_message.message_id} was deleted') 99 | await service_message.delete() 100 | await message.delete() 101 | 102 | 103 | @dp.callback_query_handler(confirming_callback.filter()) 104 | async def user_confirm(query: types.CallbackQuery, callback_data: dict): 105 | """ 106 | Хэндлер обрабатывающий нажатие на кнопку 107 | """ 108 | 109 | # сразу получаем все необходимые нам переменные, а именно 110 | # предмет, на который нажал пользователь 111 | subject = callback_data.get("subject") 112 | # предмет на который пользователь должен был нажать 113 | necessary_subject = callback_data.get('necessary_subject') 114 | # айди пользователя (приходит строкой, поэтому используем int) 115 | user_id = int(callback_data.get("user_id")) 116 | # и айди чата, для последнующей выдачи прав 117 | chat_id = int(query.message.chat.id) 118 | logger.debug(f'User {query.from_user.username} clicked on button: {subject}({necessary_subject}) in chat {chat_id}') 119 | # если на кнопку нажал не только что вошедший пользователь, говорим, чтобы не лез и игнорируем (выходим из функции). 120 | if query.from_user.id != user_id: 121 | logger.debug(f'The wrong user clicked on the button @{query.from_user.username}:{query.from_user.id}') 122 | return await query.answer("Эта кнопочка не для тебя", show_alert=True) 123 | 124 | # не забываем выдать юзеру необходимые права если он нажал на правильную кнопку 125 | if subject == necessary_subject: 126 | await bot.restrict_chat_member( 127 | chat_id=chat_id, 128 | user_id=user_id, 129 | permissions=user_allowed, 130 | ) 131 | logger.debug(f'Rights have been granted to the user @{query.from_user.username}:{query.from_user.id}') 132 | else: 133 | until_date = datetime.datetime.now() + datetime.timedelta(seconds=BAN_TIME) 134 | await bot.kick_chat_member(chat_id=chat_id, user_id=user_id, until_date=until_date) 135 | logger.debug(f'The user @{query.from_user.username}:{query.from_user.id} clicked on the wrong object ' 136 | f'and was banned until {until_date}') 137 | 138 | state = dp.current_state(user=query.from_user.id, chat=query.message.chat.id) 139 | await state.finish() 140 | logger.debug(f'User @{query.from_user.username}:{query.from_user.id} is out of the state') 141 | 142 | # и убираем часики 143 | await query.answer() 144 | 145 | # а также удаляем сообщение, чтобы пользователь в RO не мог получить права 146 | service_message = query.message 147 | await service_message.delete() 148 | logger.debug(f'Message {service_message.message_id} was deleted') 149 | -------------------------------------------------------------------------------- /handlers/private/__init__.py: -------------------------------------------------------------------------------- 1 | from .quick_guide import dp 2 | from .basic import dp 3 | 4 | __all__ = ["dp"] 5 | -------------------------------------------------------------------------------- /handlers/private/basic.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.utils.markdown import hbold 3 | from loguru import logger 4 | 5 | from loader import dp 6 | 7 | 8 | @dp.message_handler(chat_type='private') 9 | async def default(message: types.Message): 10 | logger.debug(f"@{message.from_user.username}:{message.from_user.id} in default handler") 11 | admin_markup = types.InlineKeyboardMarkup(row_width=3) 12 | admin_markup.insert( 13 | types.InlineKeyboardButton( 14 | text="Написать разработчику", 15 | url="https://t.me/Forzend" 16 | ) 17 | ) 18 | 19 | await message.answer( 20 | text=''.join( 21 | ( 22 | f"Привет, {hbold(message.from_user.full_name)}\n\n", 23 | "Я простой антибот\n", 24 | "Я могу помочь тебе с установкой бота, для этого отправь мне /quick_guide\n\n", 25 | "Об ошибке вы можете сообщить разработчику" 26 | ) 27 | ), 28 | reply_markup=admin_markup 29 | ) 30 | -------------------------------------------------------------------------------- /handlers/private/quick_guide.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters import Command 3 | 4 | from loguru import logger 5 | 6 | from loader import dp 7 | 8 | 9 | @dp.message_handler(Command('quick_guide'), chat_type='private') 10 | async def quick_guide(message: types.Message): 11 | logger.debug(f'User @{message.from_user.username}:{message.from_user.id} needs help') 12 | answer = '\n'.join([ 13 | 'Окей, давай по порядку. Для начала, тебе нужно добавить меня в группу. Я думаю с этим ты разберёшься сам', 14 | 'Потом, чтобы я мог кикать спамящих мудаков, мне нужны некоторые права администратора:\n', 15 | '1) В первую очередь возможность отправлять сообщения, это само собой\n', 16 | '2) Далее, я думаю ты понимаешь, что у меня должна быть возможность банить пользователя ' 17 | 'и это право мне тоже необходимо\n', 18 | '3) Не будем забывать о лишних сообщениях, которые по-хорошему б удалять, чтобы не было спама. ' 19 | 'Так сказать затирать следы. И для этого мне нужна возможность удалять сообщения. ' 20 | 'Я думаю ты понимаешь, что делать\n', 21 | 'Завершив это, я смогу кикать тех, кто не способен нажать на кнопку, и в твоём чате не будет ботов' 22 | ]) 23 | await message.answer(text=answer) 24 | -------------------------------------------------------------------------------- /keyboards/__init__.py: -------------------------------------------------------------------------------- 1 | from . import inline 2 | -------------------------------------------------------------------------------- /keyboards/inline/__init__.py: -------------------------------------------------------------------------------- 1 | from .guardian_keyboard import generate_confirm_markup, confirming_callback 2 | -------------------------------------------------------------------------------- /keyboards/inline/guardian_keyboard.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Tuple 3 | 4 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 5 | from aiogram.utils.callback_data import CallbackData 6 | 7 | from data.emojies import emojies 8 | from data.config import NUM_BUTTONS 9 | 10 | # создём CallbackData для удобного парсинга калбеков 11 | confirming_callback = CallbackData("confirm", "subject", "necessary_subject", "user_id") 12 | 13 | 14 | def generate_confirm_markup(user_id: int) -> Tuple[InlineKeyboardMarkup, str]: 15 | """ 16 | Функция, создающая клавиатуру для подтверждения, что пользователь не является ботом 17 | """ 18 | # создаём инлайн клавиатуру 19 | confirm_user_markup = InlineKeyboardMarkup(row_width=NUM_BUTTONS) 20 | # генерируем список объектов по которым будем итерироваться 21 | subjects = random.sample(emojies, NUM_BUTTONS) 22 | # из них выбираем один рандомный объект, на который должен нажать пользователь 23 | necessary_subject = random.choice(subjects) 24 | for emoji in subjects: 25 | button = InlineKeyboardButton( 26 | text=emoji.unicode, 27 | callback_data=confirming_callback.new(subject=emoji.subject, necessary_subject=necessary_subject.subject, user_id=user_id) 28 | ) 29 | confirm_user_markup.insert(button) 30 | 31 | # отдаём клавиатуру после создания 32 | return confirm_user_markup, necessary_subject.name 33 | -------------------------------------------------------------------------------- /loader.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 3 | 4 | from data import config 5 | 6 | bot = Bot(token=config.BOT_TOKEN, parse_mode=types.ParseMode.HTML) 7 | storage = MemoryStorage() 8 | dp = Dispatcher(bot, storage=storage) 9 | -------------------------------------------------------------------------------- /middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | from loguru import logger 3 | 4 | from .throttling import ThrottlingMiddleware 5 | 6 | 7 | def setup_middlewares(dp: Dispatcher): 8 | logger.info("Установка middlewares...") 9 | dp.middleware.setup(ThrottlingMiddleware()) 10 | -------------------------------------------------------------------------------- /middlewares/throttling.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import types, Dispatcher 4 | from aiogram.dispatcher import DEFAULT_RATE_LIMIT 5 | from aiogram.dispatcher.handler import CancelHandler, current_handler 6 | from aiogram.dispatcher.middlewares import BaseMiddleware 7 | from aiogram.utils.exceptions import Throttled 8 | from loguru import logger 9 | 10 | from utils.misc.phrase_generator import throttled_answers_generator 11 | 12 | 13 | class ThrottlingMiddleware(BaseMiddleware): 14 | """ 15 | Стандартный middleware для предотвращение спама через throttling 16 | """ 17 | 18 | def __init__(self, limit: int = DEFAULT_RATE_LIMIT, key_prefix: str = 'antiflood_'): 19 | self.rate_limit = limit 20 | self.prefix = key_prefix 21 | super(ThrottlingMiddleware, self).__init__() 22 | 23 | # noinspection PyUnusedLocal 24 | async def on_process_message(self, message: types.Message, data: dict): 25 | handler = current_handler.get() 26 | dispatcher = Dispatcher.get_current() 27 | if handler: 28 | limit = getattr(handler, 'throttling_rate_limit', self.rate_limit) 29 | key = getattr(handler, 'throttling_key', f"{self.prefix}_{handler.__name__}") 30 | else: 31 | limit = self.rate_limit 32 | key = f"{self.prefix}_message" 33 | try: 34 | await dispatcher.throttle(key, rate=limit) 35 | except Throttled as t: 36 | await self.message_throttled(message, t) 37 | raise CancelHandler() 38 | 39 | async def message_throttled(self, message: types.Message, throttled: Throttled): 40 | handler = current_handler.get() 41 | logger.debug(f'@{message.from_user.username}:{message.from_user.id} спамит командой {message.text}') 42 | limit = getattr(handler, 'throttling_rate_limit', self.rate_limit) 43 | delta = throttled.rate - throttled.delta 44 | if throttled.exceeded_count <= 2: 45 | 46 | await message.reply(text=throttled_answers_generator(limit=limit)) 47 | await asyncio.sleep(delta) 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==2.9.2 2 | environs==8.0.0 3 | loguru==0.5.1 4 | -------------------------------------------------------------------------------- /states/ConfirmUserState.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | 4 | class ConfirmUserState(StatesGroup): 5 | IncomerUser = State() 6 | -------------------------------------------------------------------------------- /states/__init__.py: -------------------------------------------------------------------------------- 1 | from .ConfirmUserState import ConfirmUserState 2 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import misc 2 | from .notify_admins import on_startup_notify 3 | -------------------------------------------------------------------------------- /utils/logger_config.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Optional 2 | 3 | from loguru import logger 4 | from sys import stderr # stdin, stdout or stderr 5 | 6 | 7 | def setup_logger(level: Union[str, int] = 'DEBUG', colorize: Optional[bool] = True): 8 | logger.remove() 9 | logger.add(sink=stderr, level=level, colorize=colorize, enqueue=True) 10 | logger.info("Логгирование успешно настроено\n") 11 | -------------------------------------------------------------------------------- /utils/misc/__init__.py: -------------------------------------------------------------------------------- 1 | from .throttling import rate_limit 2 | from .phrase_generator import users_entrance_generator 3 | -------------------------------------------------------------------------------- /utils/misc/phrase_generator.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | 3 | from aiogram.utils.markdown import hbold 4 | 5 | from data.phrases import users_entrance 6 | from data.phrases import throttled_answers 7 | 8 | 9 | def users_entrance_generator(mention: str, subject: str) -> str: 10 | return choice(users_entrance).format(mention=mention, subject=hbold(subject)) 11 | 12 | 13 | def throttled_answers_generator(limit: int) -> str: 14 | return choice(throttled_answers).format(limit=limit) 15 | -------------------------------------------------------------------------------- /utils/misc/throttling.py: -------------------------------------------------------------------------------- 1 | def rate_limit(limit: int = None, key: str = None): 2 | """ 3 | Decorator for configuring rate limit and key in different functions. 4 | """ 5 | 6 | def decorator(func): 7 | setattr(func, 'throttling_rate_limit', limit) 8 | if key: 9 | setattr(func, 'throttling_key', key) 10 | return func 11 | 12 | return decorator 13 | -------------------------------------------------------------------------------- /utils/notify_admins.py: -------------------------------------------------------------------------------- 1 | from aiogram.utils.exceptions import ChatNotFound 2 | from loguru import logger 3 | 4 | from aiogram import Dispatcher 5 | from asyncio import sleep 6 | 7 | from data.config import ADMINS_ID 8 | 9 | 10 | async def on_startup_notify(dp: Dispatcher): 11 | logger.info("Оповещение администрации...") 12 | for admin_id in ADMINS_ID: 13 | try: 14 | await dp.bot.send_message(admin_id, "Бот был успешно запущен", disable_notification=True) 15 | logger.debug(f"Сообщение отправлено {admin_id}") 16 | except ChatNotFound: 17 | logger.debug("Чат с админом не найден") 18 | 19 | await sleep(0.3) 20 | -------------------------------------------------------------------------------- /utils/set_bot_commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from loguru import logger 3 | 4 | 5 | async def set_default_commands(dp): 6 | logger.info('Установка комманд бота...') 7 | await dp.bot.set_my_commands([ 8 | types.BotCommand("developer", "Разработчик"), 9 | ]) 10 | --------------------------------------------------------------------------------