commit 7f3192f64cf6bfc1127d6d8bf5382574d87eb5e9 Author: Sergey Bahmatov Date: Fri Mar 28 13:55:39 2025 +0500 initial commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..4e122cb --- /dev/null +++ b/.drone.yml @@ -0,0 +1,50 @@ +kind: pipeline +type: docker +name: default + +steps: + - name: build image + image: docker:24 + volumes: + - name: dockersock + path: /var/run/docker.sock + environment: + DOCKER_USERNAME: + from_secret: DOCKER_USERNAME + CICD_TOKEN: + from_secret: CICD_TOKEN + commands: + - docker login git.iamninja.ru -u "$DOCKER_USERNAME" -p "$CICD_TOKEN" + - docker build -t git.iamninja.ru/iamninja/pomodoro_tg_bot:latest . + + - name: push image + image: plugins/docker + settings: + repo: git.iamninja.ru/iamninja/pomodoro_tg_bot + tags: latest + username: + from_secret: DOCKER_USERNAME + password: + from_secret: CICD_TOKEN + + - name: deploy to server + image: appleboy/drone-ssh + settings: + host: + from_secret: DOCKER_DEPLOY + username: + from_secret: DOCKER_USERNAME + port: 22025 + key: + from_secret: DOCKER_SSH_KEY + script: + - mkdir -p /home/iamninja/pomodoro_tg_bot + - cd /home/iamninja/pomodoro_tg_bot + - git pull origin main + - docker compose pull + - docker compose up -d + +volumes: + - name: dockersock + host: + path: /var/run/docker.sock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8d29e89 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..0b6d4f6 --- /dev/null +++ b/bot.py @@ -0,0 +1,43 @@ +from aiogram import Bot, Dispatcher, types +from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils import executor +from aiogram.dispatcher.filters import Text +from user_manager import user_manager +from timer_manager import timer_manager +import os + +API_TOKEN = os.getenv("BOT_TOKEN") + +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + +menu_kb = InlineKeyboardMarkup(row_width=2) +menu_kb.add( + InlineKeyboardButton("▶️ Pomodoro", callback_data="pomodoro"), + InlineKeyboardButton("☕ Short Break", callback_data="shortbreak"), + InlineKeyboardButton("😌 Long Break", callback_data="longbreak"), + InlineKeyboardButton("⏹ Stop", callback_data="stop") +) + +@dp.message_handler(commands=['start', 'help']) +async def send_welcome(message: Message): + await message.reply("Привет! Я твой личный Pomodoro-бот. Выбирай действие:", reply_markup=menu_kb) + +@dp.callback_query_handler(Text(equals=["pomodoro", "shortbreak", "longbreak", "stop"])) +async def handle_callback(call: types.CallbackQuery): + if call.data == "pomodoro": + await timer_manager.start_timer(call.from_user.id, 25*60, call.message.chat.id, 'Pomodoro') + await call.message.answer("Начался 25-минутный Pomodoro! 🔥") + elif call.data == "shortbreak": + await timer_manager.start_timer(call.from_user.id, 5*60, call.message.chat.id, 'Short Break') + await call.message.answer("Начался 5-минутный перерыв ☕") + elif call.data == "longbreak": + await timer_manager.start_timer(call.from_user.id, 15*60, call.message.chat.id, 'Long Break') + await call.message.answer("Начался длинный перерыв 😌") + elif call.data == "stop": + await timer_manager.stop_timer(call.from_user.id) + await call.message.answer("Таймер остановлен ⏹") + await call.answer() + +def run_bot(): + executor.start_polling(dp, skip_updates=True) diff --git a/main.py b/main.py new file mode 100644 index 0000000..b32487d --- /dev/null +++ b/main.py @@ -0,0 +1,7 @@ +import asyncio +from bot import run_bot +from redis_client import init_redis + +if __name__ == '__main__': + asyncio.run(init_redis()) + run_bot() \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..f0bbd9b --- /dev/null +++ b/models.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + +@dataclass +class UserState: + current_timer: str = None + task: any = None \ No newline at end of file diff --git a/redis_client.py b/redis_client.py new file mode 100644 index 0000000..33c9770 --- /dev/null +++ b/redis_client.py @@ -0,0 +1,10 @@ +import aioredis +import os + +redis = None + +async def init_redis(): + global redis + redis = await aioredis.from_url( + os.getenv("REDIS_URL", "redis://localhost"), decode_responses=True + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8d2391c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +aiogram==2.25.2 +aioredis==2.0.1 \ No newline at end of file diff --git a/time_magager.py b/time_magager.py new file mode 100644 index 0000000..2181b68 --- /dev/null +++ b/time_magager.py @@ -0,0 +1,34 @@ +import asyncio +from user_manager import user_manager +from models import UserState +from aiogram import Bot +import os + +bot = Bot(token=os.getenv("BOT_TOKEN")) + +class TimerManager: + def __init__(self): + self.tasks = {} + + async def start_timer(self, user_id, duration, chat_id, label): + await self.stop_timer(user_id) + + async def timer(): + await asyncio.sleep(duration) + if label == 'Pomodoro': + await user_manager.increment_pomodoros(user_id) + await bot.send_message(chat_id, f"⏰ {label} завершён!") + + task = asyncio.create_task(timer()) + user = user_manager.get_user(user_id) + user.current_timer = label + user.task = task + self.tasks[user_id] = task + + async def stop_timer(self, user_id): + user = user_manager.get_user(user_id) + if user.task: + user.task.cancel() + user.task = None + user.current_timer = None + self.tasks.pop(user_id, None) \ No newline at end of file diff --git a/user_mager.py b/user_mager.py new file mode 100644 index 0000000..b9b8392 --- /dev/null +++ b/user_mager.py @@ -0,0 +1,20 @@ +from models import UserState +from redis_client import redis + +class UserManager: + def __init__(self): + self.users = {} + + def get_user(self, user_id): + if user_id not in self.users: + self.users[user_id] = UserState() + return self.users[user_id] + + async def increment_pomodoros(self, user_id): + await redis.incr(f"user:{user_id}:pomodoros") + + async def get_pomodoros(self, user_id): + val = await redis.get(f"user:{user_id}:pomodoros") + return int(val) if val else 0 + +user_manager = UserManager() \ No newline at end of file