├── .env.dist ├── .gitignore ├── LICENSE ├── app ├── __init__.py ├── __main__.py ├── config.py ├── filters │ └── __init__.py ├── handlers │ ├── __init__.py │ ├── errors │ │ ├── __init__.py │ │ └── retry_after.py │ └── private │ │ ├── __init__.py │ │ └── start.py ├── keyboards │ ├── __init__.py │ ├── inline │ │ └── __init__.py │ └── reply │ │ └── __init__.py ├── loader.py ├── middlewares │ └── __init__.py ├── states │ └── __init__.py └── utils │ ├── __init__.py │ ├── default_commands.py │ ├── logger.py │ └── notify_admins.py └── requirements.txt /.env.dist: -------------------------------------------------------------------------------- 1 | BOT_TOKEN = 2 | SKIP_UPDATES = False 3 | SUPERUSER_IDS = -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # PyCharm varibles 141 | .idea/ 142 | 143 | # data 144 | pgdata/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Forzend Mainer 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 | THE SOFTWARE. -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = """ 2 | Forzend Mainer 3 | https://github.com/0Kit/ 4 | """ 5 | __version__ = '1.0' 6 | -------------------------------------------------------------------------------- /app/__main__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | from aiogram.utils import executor 3 | 4 | from app import utils, config 5 | from app.loader import dp 6 | 7 | # The configuration of the modules using import 8 | from app import middlewares, filters, handlers 9 | 10 | 11 | async def on_startup(dispatcher: Dispatcher): 12 | await utils.setup_default_commands(dispatcher) 13 | await utils.notify_admins(config.SUPERUSER_IDS) 14 | 15 | 16 | if __name__ == '__main__': 17 | utils.setup_logger("INFO", ["sqlalchemy.engine", "aiogram.bot.api"]) 18 | executor.start_polling( 19 | dp, on_startup=on_startup, skip_updates=config.SKIP_UPDATES 20 | ) 21 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from environs import Env 4 | 5 | env = Env() 6 | env.read_env() 7 | 8 | 9 | BOT_TOKEN = env.str("BOT_TOKEN") 10 | SKIP_UPDATES = env.bool("SKIP_UPDATES", False) 11 | WORK_PATH: Path = Path(__file__).parent.parent 12 | 13 | SUPERUSER_IDS = env.list("SUPERUSER_IDS") 14 | -------------------------------------------------------------------------------- /app/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | 3 | from app.loader import dp 4 | 5 | if __name__ == "app.filters": 6 | logger.info('Filters are successfully configured') 7 | -------------------------------------------------------------------------------- /app/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | 3 | from .errors import retry_after 4 | from .private import start 5 | 6 | logger.info("Handlers are successfully configured") 7 | -------------------------------------------------------------------------------- /app/handlers/errors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/articles/2519d1b2bcadae6ba5684c5a2035746d5c9037bb/app/handlers/errors/__init__.py -------------------------------------------------------------------------------- /app/handlers/errors/retry_after.py: -------------------------------------------------------------------------------- 1 | from aiogram.utils import exceptions 2 | from loguru import logger 3 | 4 | from app.loader import dp 5 | 6 | 7 | @dp.errors_handler(exception=exceptions.RetryAfter) 8 | async def retry_after_error(update, exception): 9 | logger.exception(f'RetryAfter: {exception} \nUpdate: {update}') 10 | return True 11 | -------------------------------------------------------------------------------- /app/handlers/private/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/articles/2519d1b2bcadae6ba5684c5a2035746d5c9037bb/app/handlers/private/__init__.py -------------------------------------------------------------------------------- /app/handlers/private/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters import CommandStart 3 | 4 | from app.loader import dp 5 | 6 | 7 | @dp.message_handler(CommandStart()) 8 | async def command_start_handler(msg: types.Message): 9 | await msg.answer(f'Hello, {msg.from_user.full_name}!') 10 | -------------------------------------------------------------------------------- /app/keyboards/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/articles/2519d1b2bcadae6ba5684c5a2035746d5c9037bb/app/keyboards/__init__.py -------------------------------------------------------------------------------- /app/keyboards/inline/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/articles/2519d1b2bcadae6ba5684c5a2035746d5c9037bb/app/keyboards/inline/__init__.py -------------------------------------------------------------------------------- /app/keyboards/reply/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/articles/2519d1b2bcadae6ba5684c5a2035746d5c9037bb/app/keyboards/reply/__init__.py -------------------------------------------------------------------------------- /app/loader.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 3 | 4 | 5 | from app import config 6 | 7 | bot = Bot( 8 | token=config.BOT_TOKEN, 9 | parse_mode=types.ParseMode.HTML, 10 | ) 11 | 12 | storage = MemoryStorage() 13 | 14 | dp = Dispatcher( 15 | bot=bot, 16 | storage=storage, 17 | ) 18 | 19 | __all__ = ( 20 | "bot", 21 | "storage", 22 | "dp", 23 | ) 24 | -------------------------------------------------------------------------------- /app/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram.contrib.middlewares.logging import LoggingMiddleware 2 | from loguru import logger 3 | 4 | from app.loader import dp 5 | 6 | if __name__ == "app.middlewares": 7 | dp.middleware.setup(LoggingMiddleware()) 8 | logger.info('Middlewares are successfully configured') 9 | -------------------------------------------------------------------------------- /app/states/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F0rzend/articles/2519d1b2bcadae6ba5684c5a2035746d5c9037bb/app/states/__init__.py -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .default_commands import setup_default_commands 2 | from .logger import setup_logger 3 | from .notify_admins import notify_admins 4 | 5 | __all__ = [ 6 | "setup_logger", 7 | "setup_default_commands", 8 | "notify_admins", 9 | ] 10 | -------------------------------------------------------------------------------- /app/utils/default_commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from loguru import logger 3 | 4 | 5 | async def setup_default_commands(dp): 6 | await dp.bot.set_my_commands( 7 | [ 8 | types.BotCommand("start", "start"), 9 | ] 10 | ) 11 | logger.info('Standard commands are successfully configured') 12 | -------------------------------------------------------------------------------- /app/utils/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List, Union 3 | 4 | from loguru import logger 5 | 6 | 7 | class InterceptHandler(logging.Handler): 8 | def emit(self, record): 9 | # Get corresponding Loguru level if it exists 10 | try: 11 | level = logger.level(record.levelname).name 12 | except ValueError: 13 | level = record.levelno 14 | 15 | # Find caller from where originated the logged message 16 | frame, depth = logging.currentframe(), 2 17 | while frame.f_code.co_filename == logging.__file__: 18 | frame = frame.f_back 19 | depth += 1 20 | 21 | logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) 22 | 23 | 24 | def setup_logger(level: Union[str, int] = "DEBUG", ignored: List[str] = ""): 25 | logging.basicConfig(handlers=[InterceptHandler()], level=logging.getLevelName(level)) 26 | for ignore in ignored: 27 | logger.disable(ignore) 28 | logger.info('Logging is successfully configured') 29 | -------------------------------------------------------------------------------- /app/utils/notify_admins.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | from contextlib import suppress 3 | from aiogram.utils.exceptions import ChatNotFound 4 | 5 | from loguru import logger 6 | 7 | from app.loader import dp 8 | 9 | 10 | async def notify_admins(admins: Union[List[int], List[str], int, str]): 11 | count = 0 12 | for admin in admins: 13 | with suppress(ChatNotFound): 14 | await dp.bot.send_message(admin, "Bot started") 15 | count += 1 16 | logger.info(f"{count} admins received messages") 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==2.11.2 2 | environs==9.2.0 3 | loguru==0.5.3 --------------------------------------------------------------------------------