From 5dc9c8f0d1a2665261d18a2bcbdc990bcee0660f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AD=D0=B4=D1=83=D0=B0=D1=80=D0=B4?= Date: Thu, 30 Apr 2026 16:34:57 +0300 Subject: [PATCH] =?UTF-8?q?=D0=91=D0=BE=D1=82=20+=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=D1=81=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes bot/__init__.py | 0 bot/__main__.py | 29 ++++++++++++++++++++++++++++ bot/commands.py | 30 +++++++++++++++++++++++++++++ bot/factory.py | 12 ++++++++++++ config/__init__.py | 3 +++ env.example | 5 ++++- main.py | 18 +++++++++++++++++ requirements.txt | 13 +++++++++++++ track.py => tracker/commands.py | 33 ++++++++++++++++---------------- utils.py | 5 ++++- 11 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 .DS_Store create mode 100644 bot/__init__.py create mode 100644 bot/__main__.py create mode 100644 bot/commands.py create mode 100644 bot/factory.py create mode 100644 main.py rename track.py => tracker/commands.py (72%) diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..71f8971e12d638bafb8535b02bb1d7b69f5b20d6 GIT binary patch literal 6148 zcmeHKF-sgl6n=Ae#v>PS%b*}D#3D`Lnuy&hC!`dvvIr)7XYLO6+zY2dD)%=Cf{-HM zFOXPTXrZJ{s2OrY^bW6pSKuxx!0&F2o>7kmwBx+Lm8CCsY?!uM zagxUH)fcI;|77FSiW~3#F+S>BYr~W*5mTtUM_aT_dv;~Dc|C)ft*Bl5?cvqsqeZFh zWaQOv&#>#O!wdzeL7O;6^o~088m$j5@Yr04mU)#c$+UTWo%;}KHx9evCrHlOE7^m#$wBQtK8=JktFx866dHw{e^)Ewj3qOn^yI4EY=^`D*| ze-B>BeVUlZ=24#I@`;$w1Mo@lw7kKy+Ut%xNt~uhA>h(%?oJN0N%qceNIH|-6w7j4 zg7QFDs7tX;OdJlO+4mJ5#)_X>Cg zyaG20@cIyOFF%QVUmbY?r26W)p+qlqCF@(D09m}!Iu=KlzY48HRU{8t5j E15sn?lmGw# literal 0 HcmV?d00001 diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/__main__.py b/bot/__main__.py new file mode 100644 index 0000000..57fe2b8 --- /dev/null +++ b/bot/__main__.py @@ -0,0 +1,29 @@ +import logging + +import asyncio +import sys + +from bot.commands import router as command_router +from bot.factory import bot, dp, init_bot_and_dp +from config import config + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s', + handlers=[ + logging.FileHandler('app.log', encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] +) + + +logger = logging.getLogger(__name__) + +async def main(): + bot, dp = init_bot_and_dp(f"socks5://{config.PROXY_URL}", config.BOT_TOKEN) + dp.include_router(command_router) + await dp.start_polling(bot) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/bot/commands.py b/bot/commands.py new file mode 100644 index 0000000..9e429be --- /dev/null +++ b/bot/commands.py @@ -0,0 +1,30 @@ +from argparse import Namespace + +from aiogram import Router +from aiogram.filters.command import Command, CommandStart +from aiogram.types import Message + +from bot.factory import bot +from tracker.commands import end_workday + + +router = Router() + +@router.message(CommandStart()) +async def start_command(message: Message): + await message.answer(f"Привет, {message.from_user.first_name}!") + + +@router.message(Command("track")) +async def track_command(message: Message): + await message.answer("Эта фича пока в разработке, но скоро она поможет стартовать и останавливать таймер.") + + +@router.message(Command("end")) +async def end_command(message: Message): + args = message.text.replace("/end", "").strip().split() + if "-i" in args or "--init-db" in args: + args = Namespace(init_db=True) + await message.answer("Начинаю процесс занесения трудочасов...") + await end_workday(args, bot, message.from_user.id) + await message.answer("Трудочасы занесены!") diff --git a/bot/factory.py b/bot/factory.py new file mode 100644 index 0000000..61c9591 --- /dev/null +++ b/bot/factory.py @@ -0,0 +1,12 @@ +from aiogram import Bot, Dispatcher +from aiogram.client.session.aiohttp import AiohttpSession + + +bot: Bot = None # type: ignore +dp: Dispatcher = None + +def init_bot_and_dp(proxy_url: str, token: str): + global bot, dp + bot = Bot(token=token, session=AiohttpSession(proxy=proxy_url) if proxy_url else None) + dp = Dispatcher() + return bot, dp diff --git a/config/__init__.py b/config/__init__.py index 83977d4..de5d45b 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -16,6 +16,9 @@ class Config(BaseSettings): REDMINE_TOKEN: str USER_ID: str = None WORKSPACE_ID: str = None + BOT_TOKEN: str + + PROXY_URL: str MODE: str = "prod" diff --git a/env.example b/env.example index 5a5dff9..cf2ef93 100644 --- a/env.example +++ b/env.example @@ -2,4 +2,7 @@ REDMINE_TOKEN= CLOCKIFY_TOKEN= USER_ID= WORKSPACE_ID= -TRIAL_END_DATE= \ No newline at end of file +TRIAL_END_DATE= + +BOT_TOKEN= +PROXY_URL= \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..f2126da --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +import asyncio +import logging +import sys + +from tracker.commands import end_workday + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s', + handlers=[ + logging.FileHandler('app.log', encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] +) + + +if __name__ == "__main__": + asyncio.run(end_workday()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5020b36..b0e8627 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,28 @@ +aiofiles==25.1.0 +aiogram==3.27.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.13.5 +aiohttp_socks==0.11.0 +aiosignal==1.4.0 annotated-types==0.7.0 asarPy==1.0.1 +attrs==26.1.0 certifi==2025.4.26 charset-normalizer==3.4.2 colorama==0.4.6 +frozenlist==1.8.0 idna==3.10 +magic-filter==1.0.12 +multidict==6.7.1 +propcache==0.4.1 pydantic==2.11.4 pydantic-settings==2.9.1 pydantic_core==2.33.2 python-dotenv==1.1.0 +python-socks==2.8.1 requests==2.32.3 tqdm==4.67.1 typing-inspection==0.4.0 typing_extensions==4.13.2 urllib3==2.4.0 +yarl==1.23.0 diff --git a/track.py b/tracker/commands.py similarity index 72% rename from track.py rename to tracker/commands.py index 64492bc..768225c 100644 --- a/track.py +++ b/tracker/commands.py @@ -1,24 +1,20 @@ import logging -import sys - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s', - handlers=[ - logging.FileHandler('app.log', encoding='utf-8'), - logging.StreamHandler(sys.stdout) - ] -) - +from argparse import Namespace +from aiogram import Bot from colorama import Fore # noqa: E402 +from bot.factory import bot from config import arg_parser, clock, config, db, red # noqa: E402 from utils import check_envs, db_update, format_date, get_start_end_dates # noqa: E402 -def main(): - args = arg_parser.parse_args() +async def end_workday( + args: Namespace = None, + bot: Bot = None, + user_id: int = None +): + args = args or arg_parser.parse_args() if is_debug := args.debug: config.MODE = "debug" @@ -47,7 +43,7 @@ def main(): } db.insert_user(**clock_user, **red_user) if args.init_db: - db_update() + await db_update(bot) activities = sorted(clock.get_activities( **{ @@ -58,16 +54,19 @@ def main(): ), key=lambda x: x.date_start) if not activities: logger.info(Fore.YELLOW + "Нет задач для занесения трудочасов" + Fore.RESET) + if bot: + await bot.send_message(user_id, "Нет задач для занесения трудочасов") return db.insert_activities(activities) try: tracked_activities = red.track_activities(activities) + if bot: + await bot.send_message(user_id, f"Трудочасы занесены: {len(tracked_activities)}/{len(activities)}.") clock.mark_tracked(tracked_activities) if not is_debug: db.insert_time_entries(tracked_activities) db.insert_activities(tracked_activities) except Exception as e: + if bot: + await bot.send_message(user_id, f"Ошибка при трекинге трудочасов: {e}") logger.error(e) - -if __name__ == "__main__": - main() diff --git a/utils.py b/utils.py index bc12d7f..3f1ed96 100644 --- a/utils.py +++ b/utils.py @@ -5,6 +5,7 @@ import os import re from typing import Optional +from aiogram import Bot from colorama import Fore from config import config @@ -40,9 +41,11 @@ def format_date(date: datetime) -> datetime: def parse_task_id(tag: str) -> int: return int(re.search(r"^(?P\d{5})", tag).group("task_id")) -def db_update(): +async def db_update(bot: Bot = None, user_id: int = None): from config import clock, db, red logger.info(Fore.YELLOW + "Обновление базы данных..." + Fore.RESET) + if bot: + await bot.send_message(user_id, "Обновление базы данных...") try: # Обновление Тегов