From 591d5ea01adce8d40f8480328939f7057ba93bee Mon Sep 17 00:00:00 2001 From: Dmitry Afanasyev Date: Sun, 28 Aug 2022 06:38:00 +0300 Subject: [PATCH] bot reworked to class --- Makefile | 10 ++- README.md | 4 +- app/core/application.py | 10 +-- app/core/bot.py | 154 +++++++++++++++++++++------------------- app/core/routes.py | 9 +-- app/core/scheduler.py | 75 +++++++++---------- 6 files changed, 139 insertions(+), 123 deletions(-) diff --git a/Makefile b/Makefile index c1ad9af..e14c38b 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,14 @@ PY_TARGET_FILES= PORT=8000 .PHONY: app -app: - poetry run python app/main.py +app-up: + docker-compose up -d --build + +app-down: + docker-compose down -v + +app-clean: + docker-compose down -v && docker-clean run # standard commands to run on every commit format: diff --git a/README.md b/README.md index 64fef7e..0dd23f9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ killall python ## TODO -- [ ] Добавить очередь сообщений +- [x] Добавить очередь сообщений - [x] Исправить запуск локально - [ ] Добавить тестов -- [ ] Close connection +- [x] Close connection diff --git a/app/core/application.py b/app/core/application.py index 8d8f190..c82eb46 100644 --- a/app/core/application.py +++ b/app/core/application.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from aiogram import Dispatcher from aiogram.utils.executor import start_polling from aiohttp import web -from app.core.bot import bot, dispatcher +from app.core.bot import TransportBot from app.core.routes import Handler from app.core.scheduler import BotScheduler, bot_scheduler from app.core.utils import logger @@ -18,7 +18,7 @@ class Application: async def _on_startup(self, dp: Dispatcher) -> None: logger.info("Start bot with webhook") - await bot.set_webhook(WEBHOOK_URL) + await TransportBot.bot.set_webhook(WEBHOOK_URL) loop = asyncio.get_running_loop() loop.create_task(self.handler.get_updates_from_queue()) logger.info( @@ -32,9 +32,9 @@ class Application: logger.warning('Shutting down..') # Remove webhook (not acceptable in some cases) - await bot.delete_webhook() + await TransportBot.bot.delete_webhook() - session = await bot.get_session() + session = await TransportBot.bot.get_session() if session and not session.closed: await session.close() await asyncio.sleep(0.2) @@ -45,7 +45,7 @@ class Application: def bot_polling() -> None: logger.info("Start bot in polling mode") start_polling( - dispatcher=dispatcher, + dispatcher=TransportBot.dispatcher, skip_updates=True, ) diff --git a/app/core/bot.py b/app/core/bot.py index f894a24..c9017ca 100644 --- a/app/core/bot.py +++ b/app/core/bot.py @@ -1,4 +1,5 @@ import asyncio +from dataclasses import dataclass from aiogram import Bot, types from aiogram.contrib.middlewares.logging import LoggingMiddleware @@ -7,87 +8,96 @@ from aiogram.utils.callback_data import CallbackData from app.core.parse_web import get_driver, parse_yandex_maps from app.settings import TELEGRAM_API_TOKEN -bot = Bot(token=TELEGRAM_API_TOKEN) -dispatcher = Dispatcher(bot) -dispatcher.middleware.setup(LoggingMiddleware()) +@dataclass +class TransportBot: + bot: Bot = Bot(TELEGRAM_API_TOKEN) + dispatcher: Dispatcher = Dispatcher(bot) + dispatcher.middleware.setup(LoggingMiddleware()) -stations_cb = CallbackData('station', 'direction') + stations_cb: CallbackData = CallbackData('station', 'direction') + @staticmethod + @dispatcher.message_handler(commands=['chatid']) + async def chat_id(message: types.Message) -> types.Message: + return await TransportBot.bot.send_message(message.chat.id, message.chat.id) -def get_keyboard() -> types.InlineKeyboardMarkup: - """ - Generate keyboard with list of posts - """ - markup = types.InlineKeyboardMarkup() + @staticmethod + def get_keyboard() -> types.InlineKeyboardMarkup: + """ + Generate keyboard with list of posts + """ + markup = types.InlineKeyboardMarkup() - markup.row( - types.InlineKeyboardButton( - 'Дом -> Офис', callback_data=stations_cb.new(direction='home->office') - ), - types.InlineKeyboardButton( - 'Офис -> Дом', callback_data=stations_cb.new(direction='office->home') - ), - ) - return markup + markup.row( + types.InlineKeyboardButton( + 'Дом -> Офис', + callback_data=TransportBot.stations_cb.new(direction='home->office'), + ), + types.InlineKeyboardButton( + 'Офис -> Дом', + callback_data=TransportBot.stations_cb.new(direction='office->home'), + ), + ) + return markup + @staticmethod + @dispatcher.callback_query_handler(stations_cb.filter(direction='home->office')) + async def home_office( + query: types.CallbackQuery, callback_data: dict[str, str] + ) -> types.Message: + driver = get_driver() + text = parse_yandex_maps( + driver=driver, + url='https://yandex.ru/maps/213/moscow/stops/stop__9640740/?ll=37.527924%2C55.823470&tab=overview&z=21', + message='Остановка Б. Академическая ул, д. 15', + ) -@dispatcher.callback_query_handler(stations_cb.filter(direction='home->office')) -async def home_office( - query: types.CallbackQuery, callback_data: dict[str, str] -) -> types.Message: - driver = get_driver() - text = parse_yandex_maps( - driver=driver, - url='https://yandex.ru/maps/213/moscow/stops/stop__9640740/?ll=37.527924%2C55.823470&tab=overview&z=21', - message='Остановка Б. Академическая ул, д. 15', - ) + return await TransportBot.bot.send_message( + query.message.chat.id, text, reply_markup=TransportBot.get_keyboard() + ) - return await bot.send_message( - query.message.chat.id, text, reply_markup=get_keyboard() - ) + @staticmethod + @dispatcher.callback_query_handler(stations_cb.filter(direction='office->home')) + async def office_home( + query: types.CallbackQuery, callback_data: dict[str, str] + ) -> types.Message: + driver = get_driver() + text = parse_yandex_maps( + driver=driver, + url='https://yandex.ru/maps/213/moscow/stops/stop__9640288/?ll=37.505402%2C55.800214&tab=overview&z=21', + message='Остановка Улица Алабяна', + ) + return await TransportBot.bot.send_message( + query.message.chat.id, text, reply_markup=TransportBot.get_keyboard() + ) -@dispatcher.callback_query_handler(stations_cb.filter(direction='office->home')) -async def office_home( - query: types.CallbackQuery, callback_data: dict[str, str] -) -> types.Message: - driver = get_driver() - text = parse_yandex_maps( - driver=driver, - url='https://yandex.ru/maps/213/moscow/stops/stop__9640288/?ll=37.505402%2C55.800214&tab=overview&z=21', - message='Остановка Улица Алабяна', - ) + @staticmethod + @dispatcher.message_handler() + async def echo(message: types.Message) -> types.Message: + return await TransportBot.bot.send_message( + message.chat.id, + 'Выбери остановку', + reply_markup=TransportBot.get_keyboard(), + ) - return await bot.send_message( - query.message.chat.id, text, reply_markup=get_keyboard() - ) + @staticmethod + async def morning_bus_mailing(chat_ids: list[int] | None) -> None: + if not chat_ids: + return None - -@dispatcher.message_handler(commands=['chatid']) -async def chat_id(message: types.Message) -> types.Message: - return await bot.send_message(message.chat.id, message.chat.id) - - -@dispatcher.message_handler() -async def echo(message: types.Message) -> types.Message: - return await bot.send_message( - message.chat.id, 'Выбери остановку', reply_markup=get_keyboard() - ) - - -async def morning_bus_mailing(chat_ids: list[int]) -> None: - driver = get_driver() - text = parse_yandex_maps( - driver=driver, - url='https://yandex.ru/maps/213/moscow/stops/stop__9640740/?ll=37.527924%2C55.823470&tab=overview&z=21', - message='Остановка Б. Академическая ул, д. 15', - ) - await asyncio.gather( - *[ - bot.send_message( - chat_id=chat_id, text=text, parse_mode=types.ParseMode.HTML - ) - for chat_id in chat_ids - ] - ) + driver = get_driver() + text = parse_yandex_maps( + driver=driver, + url='https://yandex.ru/maps/213/moscow/stops/stop__9640740/?ll=37.527924%2C55.823470&tab=overview&z=21', + message='Остановка Б. Академическая ул, д. 15', + ) + await asyncio.gather( + *[ + TransportBot.bot.send_message( + chat_id=chat_id, text=text, parse_mode=types.ParseMode.HTML + ) + for chat_id in chat_ids + ] + ) diff --git a/app/core/routes.py b/app/core/routes.py index cfec7b3..a4be81b 100644 --- a/app/core/routes.py +++ b/app/core/routes.py @@ -3,7 +3,7 @@ from http import HTTPStatus from aiogram.types import Update from aiohttp import web -from app.core.bot import dispatcher +from app.core.bot import TransportBot class Handler: @@ -16,10 +16,7 @@ class Handler: async def put_updates_on_queue(self, request: web.Request) -> web.Response: """ - Listen {WEBHOOK_PATH} and proxy post request to bot - - :param request: - :return: + Listen {WEBHOOK_PATH}/{TELEGRAM_WEB_TOKEN} path and proxy post request to bot """ data = await request.json() tg_update = Update(**data) @@ -30,5 +27,5 @@ class Handler: async def get_updates_from_queue(self) -> None: while True: update = await self.queue.get() - await dispatcher.process_update(update) + await TransportBot.dispatcher.process_update(update) await asyncio.sleep(0.1) diff --git a/app/core/scheduler.py b/app/core/scheduler.py index e6d7b61..ad6c1a2 100644 --- a/app/core/scheduler.py +++ b/app/core/scheduler.py @@ -1,64 +1,67 @@ from typing import Any -from app.core.bot import morning_bus_mailing +from app.core.bot import TransportBot from app.core.utils import logger from apscheduler.schedulers.asyncio import AsyncIOScheduler bot_cron_jobs = { - 'morning_home->work_bus': [ - { - 'trigger': 'cron', - 'day_of_week': 'mon-fri', - 'hour': 8, - 'minute': 59, - 'second': 0, + 'morning_home->work_bus': { + 'job': TransportBot.morning_bus_mailing, + 'cron': [ + { + 'trigger': 'cron', + 'day_of_week': 'mon-fri', + 'hour': 8, + 'minute': 59, + 'second': 0, + }, + { + 'trigger': 'cron', + 'day_of_week': 'mon-fri', + 'hour': 9, + 'minute': 4, + 'second': 0, + }, + { + 'trigger': 'cron', + 'day_of_week': 'mon-fri', + 'hour': 9, + 'minute': 9, + 'second': 0, + }, + ], + 'func_kwargs': { + 'chat_ids': [ + 417070387, # me + # 431571617, # Lenok + ] }, - { - 'trigger': 'cron', - 'day_of_week': 'mon-fri', - 'hour': 9, - 'minute': 4, - 'second': 0, - }, - { - 'trigger': 'cron', - 'day_of_week': 'mon-fri', - 'hour': 9, - 'minute': 9, - 'second': 0, - }, - ] -} -user_chat_ids = { - 'chat_ids': [ - 417070387, # me - # 431571617, # Lenok - ] + } } class BotScheduler: def __init__( self, - cron_jobs: dict[str, list[dict[str, Any]]], - chat_ids: dict[str, list[int]] | None = None, + cron_jobs: dict[str, dict[str, Any]], ): self.cron_jobs = cron_jobs - self.chat_ids = chat_ids self.scheduler = AsyncIOScheduler() def add_scheduler_jobs(self, jobs_name: str) -> None: cron_jobs = self.cron_jobs.get(jobs_name) if not cron_jobs: return None - for cron in cron_jobs: - self.scheduler.add_job(morning_bus_mailing, kwargs=user_chat_ids, **cron) - logger.info(f'Added scheduled job {cron}') + for cron in cron_jobs['cron']: + self.scheduler.add_job( + cron_jobs['job'], kwargs=cron_jobs.get('func_kwargs'), **cron + ) + logger.info(f'Added scheduled job: {cron_jobs["job"].__name__} {cron}') def start(self) -> None: self.scheduler.start() logger.info('Scheduler started') -bot_scheduler = BotScheduler(cron_jobs=bot_cron_jobs, chat_ids=user_chat_ids) +bot_scheduler = BotScheduler(cron_jobs=bot_cron_jobs) bot_scheduler.add_scheduler_jobs(jobs_name='morning_home->work_bus')