├── tgbot ├── __init__.py ├── misc │ ├── __init__.py │ ├── states.py │ ├── req_func.py │ └── bot_commands.py ├── filters │ └── __init__.py ├── keyboards │ ├── __init__.py │ ├── reply │ │ ├── __init__.py │ │ └── reply.py │ └── inline │ │ ├── __init__.py │ │ └── inline.py ├── routes │ ├── admin │ │ ├── __init__.py │ │ └── admin_check.py │ ├── user │ │ └── __init__.py │ ├── errors.py │ ├── welcome.py │ └── __init__.py ├── middlewares │ ├── __init__.py │ └── throttling.py ├── config.py └── bot.py ├── logs ├── file │ ├── users.log │ └── all.log ├── logger_conf.py └── logger.yml ├── requirements.txt ├── .env_dist ├── main.py └── .gitignore /tgbot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/file/users.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/misc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/keyboards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/keyboards/reply/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/routes/admin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/routes/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/keyboards/inline/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tgbot/misc/states.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.fsm.state import StatesGroup, State 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandico21/aiogram3_template/HEAD/requirements.txt -------------------------------------------------------------------------------- /.env_dist: -------------------------------------------------------------------------------- 1 | BOT_NAME=mybotname 2 | BOT_TOKEN=1875608932: 3 | ADMINS=131233,345345 4 | 5 | USE_REDIS=False 6 | REDIS_HOST=redis -------------------------------------------------------------------------------- /logs/file/all.log: -------------------------------------------------------------------------------- 1 | 2022-02-10 22:41:06 | INFO | dispatcher.py:398 | Polling stopped 2 | 2022-02-10 22:41:06 | ERROR | main.py:15 | Bot stopped! 3 | -------------------------------------------------------------------------------- /tgbot/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | 3 | from tgbot.middlewares.throttling import ThrottlingMiddleware 4 | 5 | 6 | def setup_middlwares(dp: Dispatcher): 7 | dp.message.middleware(ThrottlingMiddleware()) 8 | -------------------------------------------------------------------------------- /tgbot/misc/req_func.py: -------------------------------------------------------------------------------- 1 | def check_number(text: str) -> bool: 2 | if text.isdigit(): 3 | return True 4 | else: 5 | try: 6 | float(text) 7 | return True 8 | except ValueError: 9 | return False 10 | -------------------------------------------------------------------------------- /tgbot/routes/admin/admin_check.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram.types import Message 3 | 4 | admin_check_router = Router() 5 | 6 | 7 | @admin_check_router.message(commands='check') 8 | async def on_check_admin(msg: Message) -> None: 9 | await msg.reply('У Вас есть статус администратора') 10 | -------------------------------------------------------------------------------- /logs/logger_conf.py: -------------------------------------------------------------------------------- 1 | from logging import config as logging_config 2 | 3 | import yaml 4 | 5 | 6 | def setup_logging(logging_config_path: str) -> None: 7 | with open(logging_config_path, "r") as stream: 8 | logging_config_yaml = yaml.load(stream, Loader=yaml.FullLoader) 9 | 10 | logging_config.dictConfig(logging_config_yaml) 11 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from logs.logger_conf import setup_logging 5 | from tgbot.bot import main 6 | 7 | logger = logging.getLogger() 8 | 9 | 10 | if __name__ == '__main__': 11 | try: 12 | setup_logging('logs/logger.yml') 13 | asyncio.run(main()) 14 | except (KeyboardInterrupt, SystemExit): 15 | logger.error("Bot stopped!") 16 | -------------------------------------------------------------------------------- /tgbot/routes/errors.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram import Router 4 | from aiogram.exceptions import TelegramAPIError 5 | from aiogram.types import Update 6 | 7 | logger = logging.getLogger(__name__) 8 | errors_router = Router() 9 | 10 | 11 | @errors_router.errors() 12 | async def errors_handler(update: Update, exception: TelegramAPIError): 13 | logger.exception( 14 | f"{str(exception)}.\n" 15 | f"Update: {update.dict()}" 16 | ) 17 | -------------------------------------------------------------------------------- /tgbot/keyboards/reply/reply.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardMarkup, KeyboardButton 2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 3 | 4 | # Example 5 | # def rcancel() -> ReplyKeyboardMarkup: 6 | # keyboard = ReplyKeyboardBuilder().row( 7 | # KeyboardButton(text='Отмена'), 8 | # KeyboardButton(text='Ок'), 9 | # ) 10 | # 11 | # keyboard.adjust(1, repeat=True) 12 | # return keyboard.as_markup(resize_keyboard=True) 13 | -------------------------------------------------------------------------------- /tgbot/routes/welcome.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram.dispatcher.filters.command import CommandStart 3 | from aiogram.types import Message 4 | 5 | welcome_router = Router() 6 | 7 | 8 | @welcome_router.message(CommandStart()) 9 | async def on_start_command(msg: Message) -> None: 10 | await msg.answer(f'Здравствуйте, {msg.from_user.full_name}') 11 | 12 | 13 | @welcome_router.message(commands='help') 14 | async def on_help_command(msg: Message) -> None: 15 | await msg.answer("Помощь") 16 | -------------------------------------------------------------------------------- /tgbot/keyboards/inline/inline.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | # Example 6 | # def proceed() -> InlineKeyboardMarkup: 7 | # keyboard = InlineKeyboardBuilder() 8 | # keyboard.add( 9 | # InlineKeyboardButton(text='Продолжить >>', callback_data='proceed') 10 | # ) 11 | # keyboard.adjust(1, repeat=True) 12 | # return keyboard.as_markup() 13 | -------------------------------------------------------------------------------- /tgbot/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher, Router, F 2 | 3 | from tgbot.config import Config 4 | from tgbot.routes.admin.admin_check import admin_check_router 5 | from tgbot.routes.errors import errors_router 6 | from tgbot.routes.welcome import welcome_router 7 | 8 | 9 | def register_all_routes(dp: Dispatcher, config: Config) -> None: 10 | master_router = Router() 11 | admin_router = Router() 12 | dp.include_router(master_router) 13 | dp.include_router(errors_router) 14 | 15 | master_router.include_router(welcome_router) 16 | master_router.include_router(admin_router) 17 | # Administrator routers 18 | admin = config.tg_bot.admin_ids 19 | admin_check_router.message.filter(F.from_user.id.in_(admin)) 20 | admin_router.include_router(admin_check_router) 21 | -------------------------------------------------------------------------------- /tgbot/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from environs import Env 4 | 5 | 6 | @dataclass 7 | class TgBot: 8 | token: str 9 | admin_ids: list[int] 10 | use_redis: bool 11 | 12 | 13 | @dataclass 14 | class Redis: 15 | host: str 16 | 17 | 18 | @dataclass 19 | class Miscellaneous: 20 | other_params: str = None 21 | 22 | 23 | @dataclass 24 | class Config: 25 | tg_bot: TgBot 26 | redis: Redis 27 | misc: Miscellaneous 28 | 29 | 30 | def load_config(path: str = None): 31 | env = Env() 32 | env.read_env(path) 33 | 34 | return Config( 35 | tg_bot=TgBot( 36 | token=env.str("BOT_TOKEN"), 37 | admin_ids=list(map(int, env.list("ADMINS"))), 38 | use_redis=env.bool("USE_REDIS"), 39 | ), 40 | redis=Redis( 41 | host=env.str("REDIS_HOST") 42 | ), 43 | misc=Miscellaneous() 44 | ) 45 | -------------------------------------------------------------------------------- /tgbot/misc/bot_commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot 2 | from aiogram.types import BotCommand, BotCommandScopeDefault, BotCommandScopeChat 3 | 4 | from tgbot.config import Config 5 | 6 | 7 | async def set_commands( 8 | bot: Bot, 9 | config: Config 10 | ) -> None: 11 | commands = [ 12 | BotCommand( 13 | command="start", 14 | description="Запустить бота 🟢" 15 | ), 16 | BotCommand( 17 | command="help", 18 | description="Вывести справку 📃" 19 | ), 20 | ] 21 | await bot.set_my_commands(commands=commands, scope=BotCommandScopeDefault()) 22 | for admin in config.tg_bot.admin_ids: 23 | commands.append( 24 | BotCommand( 25 | command="check", 26 | description="Проверить статус ⚡" 27 | ) 28 | ) 29 | await bot.set_my_commands(commands=commands, scope=BotCommandScopeChat(chat_id=admin)) 30 | -------------------------------------------------------------------------------- /logs/logger.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | disable_existing_loggers: False 3 | 4 | formatters: 5 | console: 6 | (): 'colorlog.ColoredFormatter' 7 | format: '%(cyan)s%(asctime)s %(blue)s| %(log_color)s%(levelname)s %(blue)s | %(purple)s%(filename)s:%(lineno)d %(blue)s| %(message)s' 8 | datefmt: '%Y-%m-%d %H:%M:%S' 9 | 10 | default: 11 | format: '%(asctime)s | %(levelname)s | %(filename)s:%(lineno)d | %(message)s' 12 | datefmt: '%Y-%m-%d %H:%M:%S' 13 | 14 | handlers: 15 | console: 16 | class : logging.StreamHandler 17 | formatter: console 18 | 19 | file: 20 | class : logging.handlers.RotatingFileHandler 21 | formatter: default 22 | filename: logs/file/all.log 23 | maxBytes: 52_428_800 24 | backupCount: 3 25 | 26 | users: 27 | class: logging.handlers.RotatingFileHandler 28 | formatter: default 29 | filename: logs/file/users.log 30 | maxBytes: 52_428_800 31 | backupCount: 1 32 | 33 | loggers: 34 | root: 35 | level: INFO 36 | handlers: [console, file] 37 | 38 | users: 39 | handlers: [console, users] 40 | -------------------------------------------------------------------------------- /tgbot/bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram import Bot, Dispatcher 4 | from aiogram.dispatcher.fsm.storage.memory import MemoryStorage 5 | from aiogram.dispatcher.fsm.storage.redis import RedisStorage 6 | 7 | from tgbot.config import Config, load_config 8 | from tgbot.misc.bot_commands import set_commands 9 | from tgbot.routes import register_all_routes 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | async def main(): 15 | config: Config = load_config('.env') 16 | bot = Bot(token=config.tg_bot.token, parse_mode='HTML') 17 | 18 | storage = RedisStorage.from_url(f"redis://{config.redis.host}") if config.tg_bot.use_redis else MemoryStorage() 19 | dp = Dispatcher(storage=storage) 20 | 21 | register_all_routes(dp, config) 22 | await set_commands(bot, config) 23 | try: 24 | logger.info('Starting bot') 25 | await bot.get_updates(offset=-1) 26 | await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) 27 | finally: 28 | await storage.close() 29 | await bot.session.close() 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 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Translations 35 | *.mo 36 | *.pot 37 | 38 | 39 | # pyenv 40 | .python-version 41 | 42 | # pipenv 43 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 44 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 45 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 46 | # install all needed dependencies. 47 | #Pipfile.lock 48 | 49 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 50 | __pypackages__/ 51 | 52 | # Environments 53 | .venv 54 | env/ 55 | venv/ 56 | ENV/ 57 | env.bak/ 58 | venv.bak/ 59 | 60 | # Pyre type checker 61 | .pyre/ 62 | .idea/* -------------------------------------------------------------------------------- /tgbot/middlewares/throttling.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from aiogram import BaseMiddleware, types 4 | from aiogram.dispatcher.event.handler import HandlerObject 5 | 6 | 7 | class ThrottlingMiddleware(BaseMiddleware): 8 | def __init__(self, default_rate: int = 0.5) -> None: 9 | self.limiters = {} 10 | 11 | self.default_rate = default_rate 12 | self.count_throttled = 1 13 | self.last_throttled = 0 14 | 15 | async def __call__(self, handler, event: types.Message, data): 16 | real_handler: HandlerObject = data["handler"] 17 | skip_pass = True 18 | 19 | if real_handler.flags.get("skip_pass") is not None: 20 | skip_pass = real_handler.flags.get("skip_pass") 21 | 22 | if skip_pass: 23 | if int(time.time()) - self.last_throttled >= self.default_rate: 24 | self.last_throttled = int(time.time()) 25 | self.default_rate = 0.5 26 | self.count_throttled = 0 27 | return await handler(event, data) 28 | else: 29 | if self.count_throttled >= 2: 30 | self.default_rate = 3 31 | else: 32 | self.count_throttled += 1 33 | await event.reply("Пожалуйста, не спамьте.") 34 | 35 | self.last_throttled = int(time.time()) 36 | else: 37 | return await handler(event, data) 38 | --------------------------------------------------------------------------------