diff --git a/database.db b/database.db index 2b7efcc..94e4140 100644 Binary files a/database.db and b/database.db differ diff --git a/excel_parser.py b/excel_parser.py index ab46ffc..6f580c5 100644 --- a/excel_parser.py +++ b/excel_parser.py @@ -109,7 +109,7 @@ class ExcelParser: return {"price": price, "vat": int(data['percent_vat']), "max_days": int(data['maxdays']), - "transport_delivery_date": df["Дата загрузки"]} + "transport_delivery_date": str(df["Дата загрузки"][0])} self.add_link_to_database(query, answer=data) return None diff --git a/main.py b/main.py index eb68476..92f749e 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,14 @@ from storage import Storage from telegram_logs import logger import dotenv +import asyncio +import threading +from webserver import dp + +from aiogram.filters import CommandStart +from aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton + +from webserver import bot dotenv.load_dotenv('.env') IS_PROD = os.environ.get('PROD_ENV') == '1' @@ -51,7 +59,8 @@ class Parser: return self def __exit__(self, exc_type, exc_val, exc_tb): - logger.error("Бот остановлен. Причина: " + str(exc_val)) + if exc_val is not None: + logger.error("Бот остановлен. Причина: " + str(exc_val)) print("Gracefully shutting down...") self._driver.close() @@ -100,13 +109,18 @@ class Parser: 0] not in self.storage.get_links()] for link in links: + if PARSER_ALIVE is False: + raise KeyboardInterrupt("Бот остановлен по запросу") logger.info("Обработка заявки: " + link) try: self.accept_documentation(link) except Exception as exc: logger.error("Не удалось обработать заявку. Подробности: " + str(exc)) + logger.info("Все LTL заявки обработаны, обновление через 60сек") def parse(self, url: str = None) -> dict: + if PARSER_ALIVE is False: + raise KeyboardInterrupt("Бот остановлен по запросу") fp = self.download_documentation() e_parser = ExcelParser(fp, url) price = e_parser.calculate() @@ -116,6 +130,9 @@ class Parser: return price def accept_documentation(self, url: str): + if PARSER_ALIVE is False: + raise KeyboardInterrupt("Бот остановлен по запросу") + time.sleep(3) self._driver.get(url) @@ -142,6 +159,8 @@ class Parser: delivery_range=price['max_days']) def download_documentation(self) -> list[pathlib.Path]: + if PARSER_ALIVE is False: + raise KeyboardInterrupt("Бот остановлен по запросу") try: all_files_1 = set( pathlib.Path('./downloads') / pathlib.Path(file) for tree in os.walk('./downloads') for file in tree[2]) @@ -168,6 +187,8 @@ class Parser: raise KeyboardInterrupt() def send_offer_link(self, price: int, nds: int, delivery_range: str, delivery_time: str): + if PARSER_ALIVE is False: + raise KeyboardInterrupt("Бот остановлен по запросу") try: logger.info( f"Предварительные данные по заявке: Цена: {price}, НДС: {nds}%, Доставка: {delivery_range} дн., Подача машины {delivery_time}") @@ -220,10 +241,71 @@ class Parser: time.sleep(10) -if __name__ == "__main__": +PARSER_ALIVE = True + + +def parse_runner(): with Parser() as parser: parser.login() while True: parser.search() - logger.info("Все LTL заявки обработаны, обновление через 60сек") time.sleep(60) + + +parser_thread = threading.Thread(target=parse_runner, daemon=True) + + +@dp.message(CommandStart()) +async def start_handler(message: Message): + s = Storage() + if message.from_user.id not in s.get_users(): + await message.answer("Вы не зарегистрированы, обратитесь к администратору") + return + markup = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text="Запустить Бот")]]) + await message.answer(f"Hello, {message.from_user.full_name}!", reply_markup=markup) + + +@dp.message() +async def message_handler(message: Message): + global PARSER_ALIVE + s = Storage() + if message.from_user.id not in s.get_users(): + await message.answer("Вы не зарегистрированы, обратитесь к администратору") + return + + markup = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text="Запустить Бот")]]) + if message.text == "Запустить Бот": + PARSER_ALIVE = True + for chat_id in s.get_users(): + await bot.send_message(chat_id, + f"Пользователь {message.from_user.full_name} запускает бот", + reply_markup=markup) + parser_thread.start() + return + if message.text == "Остановить Бот": + for chat_id in s.get_users(): + await bot.send_message(chat_id, + f"Пользователь {message.from_user.full_name} остановил бот", + reply_markup=markup) + PARSER_ALIVE = False + await message.answer("Бот остановлен", reply_markup=markup) + return + await message.answer("Неизвестная команда") + + +async def main(): + storage = Storage() + markup = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text="Запустить Бот")]]) + for chat_id in storage.get_users(): + await bot.send_message(chat_id, + "Контроллер запущен, бот ожидает включения", + reply_markup=markup, + disable_notification=True) + await dp.start_polling(bot) + +if __name__ == "__main__": + # with Parser() as p: + # p.login() + # p.search() + asyncio.run(main()) + diff --git a/requirements.in b/requirements.in index 4f03502..143a01f 100644 --- a/requirements.in +++ b/requirements.in @@ -11,4 +11,5 @@ pyexcel pyexcel-xls pyexcel-xlsx loguru -pydotenv \ No newline at end of file +pydotenv +aiogram \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 95dfbef..c18c8a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,14 +4,28 @@ # # pip-compile # +aiofiles==23.2.1 + # via aiogram +aiogram==3.4.1 + # via -r requirements.in +aiohttp==3.9.3 + # via aiogram +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +async-timeout==4.0.3 + # via aiohttp attrs==23.2.0 # via + # aiohttp # outcome # trio beautifulsoup4==4.12.3 # via xls2xlsx certifi==2023.11.17 # via + # aiogram # requests # selenium chardet==5.2.0 @@ -32,18 +46,29 @@ exceptiongroup==1.2.0 # trio-websocket fonttools==4.47.2 # via xls2xlsx +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal h11==0.14.0 # via wsproto idna==3.6 # via # requests # trio + # yarl lml==0.1.0 # via # pyexcel # pyexcel-io loguru==0.7.2 # via -r requirements.in +magic-filter==1.0.12 + # via aiogram +multidict==6.0.5 + # via + # aiohttp + # yarl numpy==1.26.3 # via pandas openpyxl==3.1.2 @@ -59,6 +84,10 @@ pandas==2.2.0 # via -r requirements.in pillow==10.2.0 # via xls2xlsx +pydantic==2.5.3 + # via aiogram +pydantic-core==2.14.6 + # via pydantic pydotenv==0.0.7 # via -r requirements.in pyexcel==0.7.0 @@ -108,7 +137,11 @@ trio==0.24.0 trio-websocket==0.11.1 # via selenium typing-extensions==4.9.0 - # via selenium + # via + # aiogram + # pydantic + # pydantic-core + # selenium tzdata==2023.4 # via pandas urllib3[socks]==2.1.0 @@ -137,3 +170,5 @@ xlwt==1.3.0 # -r requirements.in # pyexcel-xls # xlutils +yarl==1.9.4 + # via aiohttp diff --git a/storage.py b/storage.py index 83f8f0d..cd9bf83 100644 --- a/storage.py +++ b/storage.py @@ -5,10 +5,15 @@ from typing import ContextManager class Storage: def __init__(self): + self.con = None + + def set_connection(self): self.con = sqlite3.connect("database.db") @contextmanager def get_cursor(self) -> ContextManager[sqlite3.Cursor]: + if self.con is None: + self.set_connection() cur = self.con.cursor() try: yield cur diff --git a/telegram_logs.py b/telegram_logs.py index 379dce2..406e11d 100644 --- a/telegram_logs.py +++ b/telegram_logs.py @@ -1,3 +1,5 @@ +import json + import requests from loguru import logger from storage import Storage @@ -19,11 +21,11 @@ def _log(message): r = requests.post("{0}{1}/sendMessage".format(_URL, _TOKEN), { "chat_id": int(chat_id), "disable_notification": True, - "text": icon + message + "text": icon + message, + "reply_markup": json.dumps({"keyboard": [[{"text": "Остановить Бот"}]]}) }) - - if r.status_code >= 400: - logger.error("Failed to send message: {0} {1}".format(r.status_code, r.text)) + if r.status_code != 200: + print(r.json()) def _filter_info_only(record): diff --git a/webserver.py b/webserver.py new file mode 100644 index 0000000..066ef1a --- /dev/null +++ b/webserver.py @@ -0,0 +1,4 @@ +from aiogram import Bot, Dispatcher + +dp = Dispatcher() +bot = Bot(token="6767909836:AAFpsqtWeBNIBgSSi2_19rltEHOF0mrvTg0")