From 9a05d542e26e182f89b53281552ec629c6328069 Mon Sep 17 00:00:00 2001 From: Alexey <54932673+ksemele@users.noreply.github.com> Date: Fri, 1 Apr 2022 23:02:03 +0400 Subject: [PATCH] Initial commit --- .env.dist | 6 ++ .gitignore | 131 +++++++++++++++++++++++++++++++ app.py | 19 +++++ data/__init__.py | 0 data/config.py | 10 +++ filters/__init__.py | 9 +++ handlers/__init__.py | 5 ++ handlers/channels/__init__.py | 0 handlers/errors/__init__.py | 1 + handlers/errors/error_handler.py | 37 +++++++++ handlers/groups/__init__.py | 0 handlers/users/__init__.py | 3 + handlers/users/asa.py | 0 handlers/users/echo.py | 21 +++++ handlers/users/help.py | 13 +++ handlers/users/start.py | 9 +++ keyboards/__init__.py | 2 + keyboards/default/__init__.py | 0 keyboards/inline/__init__.py | 1 + loader.py | 8 ++ middlewares/__init__.py | 8 ++ middlewares/throttling.py | 37 +++++++++ requirements.txt | 2 + states/__init__.py | 0 utils/__init__.py | 3 + utils/db_api/__init__.py | 0 utils/misc/__init__.py | 2 + utils/misc/logging.py | 6 ++ utils/misc/throttling.py | 16 ++++ utils/notify_admins.py | 14 ++++ utils/set_bot_commands.py | 10 +++ 31 files changed, 373 insertions(+) create mode 100644 .env.dist create mode 100644 .gitignore create mode 100644 app.py create mode 100644 data/__init__.py create mode 100644 data/config.py create mode 100644 filters/__init__.py create mode 100644 handlers/__init__.py create mode 100644 handlers/channels/__init__.py create mode 100644 handlers/errors/__init__.py create mode 100644 handlers/errors/error_handler.py create mode 100644 handlers/groups/__init__.py create mode 100644 handlers/users/__init__.py create mode 100644 handlers/users/asa.py create mode 100644 handlers/users/echo.py create mode 100644 handlers/users/help.py create mode 100644 handlers/users/start.py create mode 100644 keyboards/__init__.py create mode 100644 keyboards/default/__init__.py create mode 100644 keyboards/inline/__init__.py create mode 100644 loader.py create mode 100644 middlewares/__init__.py create mode 100644 middlewares/throttling.py create mode 100644 requirements.txt create mode 100644 states/__init__.py create mode 100644 utils/__init__.py create mode 100644 utils/db_api/__init__.py create mode 100644 utils/misc/__init__.py create mode 100644 utils/misc/logging.py create mode 100644 utils/misc/throttling.py create mode 100644 utils/notify_admins.py create mode 100644 utils/set_bot_commands.py diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..e8807d4 --- /dev/null +++ b/.env.dist @@ -0,0 +1,6 @@ +# ЭТО ПРИМЕР ФАЙЛА .env !! ВАМ НАДО ЭТОТ ФАЙЛ ПЕРЕИМЕНОВАТЬ И ВСТАВИТЬ ТУДА ЗНАЧЕНИЯ. +# ЭТИ КОММЕНТАРИИ НАДО УДАЛИТЬ! + +ADMINS=12345678,12345677,12345676 +BOT_TOKEN=123452345243:Asdfasdfasf +ip=localhost diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..913d692 --- /dev/null +++ b/.gitignore @@ -0,0 +1,131 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +*/.env +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +.idea/* +.env \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..b080cca --- /dev/null +++ b/app.py @@ -0,0 +1,19 @@ +from aiogram import executor + +from loader import dp +import middlewares, filters, handlers +from utils.notify_admins import on_startup_notify +from utils.set_bot_commands import set_default_commands + + +async def on_startup(dispatcher): + # Устанавливаем дефолтные команды + await set_default_commands(dispatcher) + + # Уведомляет про запуск + await on_startup_notify(dispatcher) + + +if __name__ == '__main__': + executor.start_polling(dp, on_startup=on_startup) + diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/config.py b/data/config.py new file mode 100644 index 0000000..1cb02ee --- /dev/null +++ b/data/config.py @@ -0,0 +1,10 @@ +from environs import Env + +# Теперь используем вместо библиотеки python-dotenv библиотеку environs +env = Env() +env.read_env() + +BOT_TOKEN = env.str("BOT_TOKEN") # Забираем значение типа str +ADMINS = env.list("ADMINS") # Тут у нас будет список из админов +IP = env.str("ip") # Тоже str, но для айпи адреса хоста + diff --git a/filters/__init__.py b/filters/__init__.py new file mode 100644 index 0000000..40de5d0 --- /dev/null +++ b/filters/__init__.py @@ -0,0 +1,9 @@ +from aiogram import Dispatcher + +from loader import dp +# from .is_admin import AdminFilter + + +if __name__ == "filters": + # dp.filters_factory.bind(AdminFilter) + pass diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..ed49383 --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1,5 @@ +from . import errors +from . import users +from . import groups +from . import channels + diff --git a/handlers/channels/__init__.py b/handlers/channels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/handlers/errors/__init__.py b/handlers/errors/__init__.py new file mode 100644 index 0000000..96f9c99 --- /dev/null +++ b/handlers/errors/__init__.py @@ -0,0 +1 @@ +from . import error_handler diff --git a/handlers/errors/error_handler.py b/handlers/errors/error_handler.py new file mode 100644 index 0000000..761ebd2 --- /dev/null +++ b/handlers/errors/error_handler.py @@ -0,0 +1,37 @@ +import logging +from aiogram.utils.exceptions import (TelegramAPIError, + MessageNotModified, + CantParseEntities) + + +from loader import dp + + +@dp.errors_handler() +async def errors_handler(update, exception): + """ + Exceptions handler. Catches all exceptions within task factory tasks. + :param dispatcher: + :param update: + :param exception: + :return: stdout logging + """ + + + if isinstance(exception, MessageNotModified): + logging.exception('Message is not modified') + # do something here? + return True + + if isinstance(exception, CantParseEntities): + # or here + logging.exception(f'CantParseEntities: {exception} \nUpdate: {update}') + return True + + # MUST BE THE LAST CONDITION (ЭТО УСЛОВИЕ ВСЕГДА ДОЛЖНО БЫТЬ В КОНЦЕ) + if isinstance(exception, TelegramAPIError): + logging.exception(f'TelegramAPIError: {exception} \nUpdate: {update}') + return True + + # At least you have tried. + logging.exception(f'Update: {update} \n{exception}') diff --git a/handlers/groups/__init__.py b/handlers/groups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/handlers/users/__init__.py b/handlers/users/__init__.py new file mode 100644 index 0000000..34bd645 --- /dev/null +++ b/handlers/users/__init__.py @@ -0,0 +1,3 @@ +from . import help +from . import start +from . import echo diff --git a/handlers/users/asa.py b/handlers/users/asa.py new file mode 100644 index 0000000..e69de29 diff --git a/handlers/users/echo.py b/handlers/users/echo.py new file mode 100644 index 0000000..f7fa79c --- /dev/null +++ b/handlers/users/echo.py @@ -0,0 +1,21 @@ +from aiogram import types +from aiogram.dispatcher import FSMContext + +from loader import dp + + +# Эхо хендлер, куда летят текстовые сообщения без указанного состояния +@dp.message_handler(state=None) +async def bot_echo(message: types.Message): + await message.answer(f"Эхо без состояния." + f"Сообщение:\n" + f"{message.text}") + + +# Эхо хендлер, куда летят ВСЕ сообщения с указанным состоянием +@dp.message_handler(state="*", content_types=types.ContentTypes.ANY) +async def bot_echo_all(message: types.Message, state: FSMContext): + state = await state.get_state() + await message.answer(f"Эхо в состоянии {state}.\n" + f"\nСодержание сообщения:\n" + f"{message}") diff --git a/handlers/users/help.py b/handlers/users/help.py new file mode 100644 index 0000000..5420ccb --- /dev/null +++ b/handlers/users/help.py @@ -0,0 +1,13 @@ +from aiogram import types +from aiogram.dispatcher.filters.builtin import CommandHelp + +from loader import dp + + +@dp.message_handler(CommandHelp()) +async def bot_help(message: types.Message): + text = ("Список команд: ", + "/start - Начать диалог", + "/help - Получить справку") + + await message.answer("\n".join(text)) diff --git a/handlers/users/start.py b/handlers/users/start.py new file mode 100644 index 0000000..539ab70 --- /dev/null +++ b/handlers/users/start.py @@ -0,0 +1,9 @@ +from aiogram import types +from aiogram.dispatcher.filters.builtin import CommandStart + +from loader import dp + + +@dp.message_handler(CommandStart()) +async def bot_start(message: types.Message): + await message.answer(f"Привет, {message.from_user.full_name}!") diff --git a/keyboards/__init__.py b/keyboards/__init__.py new file mode 100644 index 0000000..0b8947f --- /dev/null +++ b/keyboards/__init__.py @@ -0,0 +1,2 @@ +from . import default +from . import inline diff --git a/keyboards/default/__init__.py b/keyboards/default/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keyboards/inline/__init__.py b/keyboards/inline/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/keyboards/inline/__init__.py @@ -0,0 +1 @@ + diff --git a/loader.py b/loader.py new file mode 100644 index 0000000..ba42d33 --- /dev/null +++ b/loader.py @@ -0,0 +1,8 @@ +from aiogram import Bot, Dispatcher, types +from aiogram.contrib.fsm_storage.memory import MemoryStorage + +from data import config + +bot = Bot(token=config.BOT_TOKEN, parse_mode=types.ParseMode.HTML) +storage = MemoryStorage() +dp = Dispatcher(bot, storage=storage) diff --git a/middlewares/__init__.py b/middlewares/__init__.py new file mode 100644 index 0000000..0631e10 --- /dev/null +++ b/middlewares/__init__.py @@ -0,0 +1,8 @@ +from aiogram import Dispatcher + +from loader import dp +from .throttling import ThrottlingMiddleware + + +if __name__ == "middlewares": + dp.middleware.setup(ThrottlingMiddleware()) diff --git a/middlewares/throttling.py b/middlewares/throttling.py new file mode 100644 index 0000000..4037c01 --- /dev/null +++ b/middlewares/throttling.py @@ -0,0 +1,37 @@ +import asyncio + +from aiogram import types, Dispatcher +from aiogram.dispatcher import DEFAULT_RATE_LIMIT +from aiogram.dispatcher.handler import CancelHandler, current_handler +from aiogram.dispatcher.middlewares import BaseMiddleware +from aiogram.utils.exceptions import Throttled + + +class ThrottlingMiddleware(BaseMiddleware): + """ + Simple middleware + """ + + def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix='antiflood_'): + self.rate_limit = limit + self.prefix = key_prefix + super(ThrottlingMiddleware, self).__init__() + + async def on_process_message(self, message: types.Message, data: dict): + handler = current_handler.get() + dispatcher = Dispatcher.get_current() + if handler: + limit = getattr(handler, "throttling_rate_limit", self.rate_limit) + key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}") + else: + limit = self.rate_limit + key = f"{self.prefix}_message" + try: + await dispatcher.throttle(key, rate=limit) + except Throttled as t: + await self.message_throttled(message, t) + raise CancelHandler() + + async def message_throttled(self, message: types.Message, throttled: Throttled): + if throttled.exceeded_count <= 2: + await message.reply("Too many requests!") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ee44160 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +aiogram<3.0 +environs~=8.0.0 \ No newline at end of file diff --git a/states/__init__.py b/states/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..47874ab --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,3 @@ +from . import db_api +from . import misc +from .notify_admins import on_startup_notify diff --git a/utils/db_api/__init__.py b/utils/db_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/misc/__init__.py b/utils/misc/__init__.py new file mode 100644 index 0000000..3f7248a --- /dev/null +++ b/utils/misc/__init__.py @@ -0,0 +1,2 @@ +from .throttling import rate_limit +from . import logging diff --git a/utils/misc/logging.py b/utils/misc/logging.py new file mode 100644 index 0000000..e5b24b9 --- /dev/null +++ b/utils/misc/logging.py @@ -0,0 +1,6 @@ +import logging + +logging.basicConfig(format=u'%(filename)s [LINE:%(lineno)d] #%(levelname)-8s [%(asctime)s] %(message)s', + level=logging.INFO, + # level=logging.DEBUG, # Можно заменить на другой уровень логгирования. + ) diff --git a/utils/misc/throttling.py b/utils/misc/throttling.py new file mode 100644 index 0000000..c881c9e --- /dev/null +++ b/utils/misc/throttling.py @@ -0,0 +1,16 @@ +def rate_limit(limit: int, key=None): + """ + Decorator for configuring rate limit and key in different functions. + + :param limit: + :param key: + :return: + """ + + def decorator(func): + setattr(func, 'throttling_rate_limit', limit) + if key: + setattr(func, 'throttling_key', key) + return func + + return decorator diff --git a/utils/notify_admins.py b/utils/notify_admins.py new file mode 100644 index 0000000..97e1309 --- /dev/null +++ b/utils/notify_admins.py @@ -0,0 +1,14 @@ +import logging + +from aiogram import Dispatcher + +from data.config import ADMINS + + +async def on_startup_notify(dp: Dispatcher): + for admin in ADMINS: + try: + await dp.bot.send_message(admin, "Бот Запущен") + + except Exception as err: + logging.exception(err) diff --git a/utils/set_bot_commands.py b/utils/set_bot_commands.py new file mode 100644 index 0000000..d9143e3 --- /dev/null +++ b/utils/set_bot_commands.py @@ -0,0 +1,10 @@ +from aiogram import types + + +async def set_default_commands(dp): + await dp.bot.set_my_commands( + [ + types.BotCommand("start", "Запустить бота"), + types.BotCommand("help", "Вывести справку"), + ] + )