diff --git a/daemons/fetch.py b/daemons/fetch.py index 838cd20..55b683a 100644 --- a/daemons/fetch.py +++ b/daemons/fetch.py @@ -1,14 +1,14 @@ import datetime -import zoneinfo from time import sleep +from helpers import now from helpers.mongo import mongo from helpers.ruz import ruz +from settings import MOSCOW_TIMEZONE def fetch_schedule_for_user(user_hse_id: int): - zone = zoneinfo.ZoneInfo("Europe/Moscow") - today = datetime.datetime.now(zone) + today = now() next_day = today + datetime.timedelta(days=7) schedule = ruz.get_schedule(user_hse_id, today, next_day) if schedule is None: @@ -27,7 +27,8 @@ def fetch_schedule_for_user(user_hse_id: int): month=int(month), day=int(day), hour=int(begin_hour), - minute=int(begin_minute) + minute=int(begin_minute), + tzinfo=MOSCOW_TIMEZONE ) }) if lesson is None: @@ -40,14 +41,16 @@ def fetch_schedule_for_user(user_hse_id: int): month=int(month), day=int(day), hour=int(begin_hour), - minute=int(begin_minute) + minute=int(begin_minute), + tzinfo=MOSCOW_TIMEZONE ), "end": datetime.datetime( year=int(year), month=int(month), day=int(day), hour=int(end_hour), - minute=int(end_minute) + minute=int(end_minute), + tzinfo=MOSCOW_TIMEZONE ), "building": element['building'], "lecturer": element['lecturer'], @@ -66,9 +69,7 @@ def process(): def delete_old(): - zone = zoneinfo.ZoneInfo("Europe/Moscow") - today = datetime.datetime.now(zone) - mongo.lessons_collection.delete_many({"end": {"$lte": today - datetime.timedelta(days=1)}}) + mongo.lessons_collection.delete_many({"end": {"$lte": now() - datetime.timedelta(days=1)}}) def fetch(): diff --git a/daemons/notify.py b/daemons/notify.py index 1016b2b..fb9e320 100644 --- a/daemons/notify.py +++ b/daemons/notify.py @@ -2,19 +2,42 @@ import datetime import zoneinfo from time import sleep +import croniter +import pytz from telebot.apihelper import ApiTelegramException from daemons.bot import bot +from helpers import now +from helpers.keyboards import main_keyboard +from helpers.models import UserSchema from helpers.mongo import mongo +from helpers.ruz import ruz def process(): + for user in mongo.users_collection.find({"hse_id": {"$ne": None}, "next_notify_time": {"$lte": now()}}): + try: + lessons = mongo.get_today_lessons(UserSchema().load(user)) + if len(lessons) == 0: + ans = "Сегодня у тебя нет пар." + else: + ans = ruz.schedule_builder(lessons) + bot.send_message( + user['chat_id'], + "Напоминаю о занятиях сегодня!\n" + ans, + reply_markup=main_keyboard() + ) + except: + pass + hours, minutes = user['notify_daily'].split(':') + cron = croniter.croniter(f"{minutes} {hours} * * *", now()) + next_date = cron.get_next(datetime.datetime) + next_date = next_date.replace(tzinfo=pytz.timezone('Europe/Moscow')) + mongo.users_collection.update_one({"chat_id": user['chat_id']}, {"$set": {"next_notify_time": next_date}}) for user in mongo.users_collection.find({"notify_minutes": {"$ne": None}, "hse_id": {"$ne": None}}): - zone = zoneinfo.ZoneInfo("Europe/Moscow") - now = datetime.datetime.now(zone) for lesson in mongo.lessons_collection.find({ "hse_user_id": user["hse_id"], - "begin": {"$lte": now + datetime.timedelta(minutes=5)}, + "begin": {"$lte": now() + datetime.timedelta(minutes=5)}, "notified": False }): ans = "" diff --git a/helpers/__init__.py b/helpers/__init__.py index e69de29..8e0d027 100644 --- a/helpers/__init__.py +++ b/helpers/__init__.py @@ -0,0 +1,8 @@ +import datetime +import zoneinfo + + +def now(): + zone = zoneinfo.ZoneInfo("Europe/Moscow") + today = datetime.datetime.now(zone) + return today diff --git a/helpers/answer.py b/helpers/answer.py index cb5a6d7..0c7064e 100644 --- a/helpers/answer.py +++ b/helpers/answer.py @@ -1,8 +1,14 @@ -import telebot +import datetime + +import croniter +import pytz from telebot.types import Message from daemons.bot import bot from daemons.fetch import fetch_schedule_for_user +from helpers import now +from helpers.keyboards import main_keyboard, notify_keyboard, yes_no_keyboard, again_keyboard, groups_keyboard, \ + no_daily_notify from helpers.models import UserSchema, User from helpers.mongo import mongo from helpers.ruz import ruz @@ -12,9 +18,21 @@ class BaseAnswer: def process(self, message: Message): user = mongo.users_collection.find_one({"chat_id": message.chat.id}) if user is None: - user = User(chat_id=message.chat.id) + cron = croniter.croniter("0 9 * * *", now()) + next_date = cron.get_next(datetime.datetime) + next_date = next_date.replace(tzinfo=pytz.timezone('Europe/Moscow')) + user = User(chat_id=message.chat.id, next_notify_time=next_date) mongo.users_collection.insert_one(UserSchema().dump(user)) else: + if "next_notify_time" not in user: + cron = croniter.croniter("0 9 * * *", now()) + next_date = cron.get_next(datetime.datetime) + next_date = next_date.replace(tzinfo=pytz.timezone('Europe/Moscow')) + user["next_notify_time"] = next_date + mongo.users_collection.update_one( + {"chat_id": message.chat.id}, + {"$set": {"next_notify_time": next_date}} + ) user = UserSchema().load(user) attr = getattr(self, "handle_state_" + user.state, None) if attr is None: @@ -36,7 +54,6 @@ class Answer(BaseAnswer): self.set_state(user, "wait_for_name") def handle_state_wait_for_name(self, message: Message, user: User): - kb = telebot.types.ReplyKeyboardMarkup(True, False) data = ruz.find_person(message.text) if data is None: bot.send_message( @@ -47,8 +64,6 @@ class Answer(BaseAnswer): if len(data) == 0: bot.send_message(user.chat_id, "К сожалению, в РУЗе не нашлось такого студента, попробуй еще раз.") return - for entity in data: - kb.row(entity['description']) user.name = message.text mongo.users_collection.update_one( {"chat_id": user.chat_id}, @@ -56,7 +71,7 @@ class Answer(BaseAnswer): bot.send_message( user.chat_id, "Отлично! Теперь выбери из списка свою группу.", - reply_markup=kb + reply_markup=groups_keyboard(data) ) self.set_state(user, "wait_for_group") @@ -90,55 +105,46 @@ class Answer(BaseAnswer): ) success = fetch_schedule_for_user(user.hse_id) if success: - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Пары сегодня") - kb.row("Уведомления") - kb.row("Сброс настроек") lessons = mongo.get_today_lessons(user) if len(lessons) == 0: bot.send_message( user.chat_id, "Сегодня у тебя нет пар.", - reply_markup=kb + reply_markup=main_keyboard() ) else: bot.send_message( user.chat_id, ruz.schedule_builder(lessons), - reply_markup=kb + reply_markup=main_keyboard() ) self.set_state(user, "ready") def handle_state_ready(self, message: Message, user: User): - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Пары сегодня") - kb.row("Уведомления") - kb.row("Сброс настроек") if message.text == "Пары сегодня": lessons = mongo.get_today_lessons(user) if len(lessons) == 0: text = "Сегодня у тебя нет пар." else: text = ruz.schedule_builder(lessons) - elif message.text == "Уведомления": - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Не уведомлять") - kb.row("5 минут") - kb.row("10 минут") - kb.row("15 минут") - kb.row("20 минут") + elif message.text == "Уведомления о парах": bot.send_message( user.chat_id, "Выбери когда мне нужно тебе написать о предстоящей паре", - reply_markup=kb + reply_markup=notify_keyboard() ) self.set_state(user, "wait_for_notify") return + elif message.text == "Ежедневные уведомления": + bot.send_message( + user.chat_id, + "Пришли время в формате чч:мм во сколько ты хочешь получать уведомления о парах в этот день. Например, 09:30", + reply_markup=no_daily_notify() + ) + self.set_state(user, "wait_for_daily_notify") + return elif message.text == "Сброс настроек": - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Да") - kb.row("Нет") - bot.send_message(user.chat_id, "Ты уверен что хочешь сбросить все настройки и больше не получать уведомления?", reply_markup=kb) + bot.send_message(user.chat_id, "Ты уверен что хочешь сбросить все настройки и больше не получать уведомления?", reply_markup=yes_no_keyboard()) self.set_state(user, "reset") return else: @@ -146,7 +152,7 @@ class Answer(BaseAnswer): bot.send_message( user.chat_id, text, - reply_markup=kb + reply_markup=main_keyboard() ) def handle_state_wait_for_notify(self, message: Message, user: User): @@ -162,46 +168,71 @@ class Answer(BaseAnswer): elif text == "20 минут": user.notify_minutes = 20 else: - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Не уведомлять") - kb.row("5 минут") - kb.row("10 минут") - kb.row("15 минут") - kb.row("20 минут") - bot.send_message(user.chat_id, "Я не понимаю такой команды, используй кнопки.", reply_markup=kb) + bot.send_message( + user.chat_id, + "Я не понимаю такой команды, используй кнопки.", + reply_markup=notify_keyboard() + ) return mongo.users_collection.update_one({"chat_id": user.chat_id}, {"$set": {"notify_minutes": user.notify_minutes}}) if user.notify_minutes is not None: text = f"Принято! Буду уведомлять тебя за {text}." else: text = f"Принято! Я не уведомлять тебя." - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Пары сегодня") - kb.row("Уведомления") - kb.row("Сброс настроек") - bot.send_message(user.chat_id, text, reply_markup=kb) + bot.send_message(user.chat_id, text, reply_markup=main_keyboard()) self.set_state(user, "ready") def handle_state_reset(self, message: Message, user: User): if message.text == "Да": mongo.users_collection.delete_one({"hse_id": user.hse_id}) - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Начать заново") - bot.send_message(user.chat_id, "Настройки сброшены, ждем твоего возвращения", reply_markup=kb) + bot.send_message(user.chat_id, "Настройки сброшены, ждем твоего возвращения", reply_markup=again_keyboard()) elif message.text == "Нет": - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Пары сегодня") - kb.row("Уведомления") - kb.row("Сброс настроек") - bot.send_message(user.chat_id, "Возращаюсь к прежнему режиму", reply_markup=kb) + bot.send_message(user.chat_id, "Возращаюсь к прежнему режиму", reply_markup=main_keyboard()) self.set_state(user, "ready") else: - kb = telebot.types.ReplyKeyboardMarkup(True, False) - kb.row("Да") - kb.row("Нет") bot.send_message(user.chat_id, "Я не понимаю, используй кнопки", - reply_markup=kb) + reply_markup=yes_no_keyboard()) + + def _check_time_correct(self, line: str) -> bool: + if len(line) != 5: + return False + if line.count(':') != 1: + return False + hours, minutes = line.split(':') + try: + hours = int(hours) + minutes = int(minutes) + except: + return False + if hours < 0 or hours > 23 or minutes < 0 or minutes > 59: + return False + return True + + def handle_state_wait_for_daily_notify(self, message: Message, user: User): + text = message.text + if not self._check_time_correct(text): + bot.send_message( + user.chat_id, + "Ты прислал что-то неправильное. Пришли время в формате чч:мм. Например, 09:30", + reply_markup=no_daily_notify() + ) + return + user.notify_daily = message.text + hours, minutes = user.notify_daily.split(':') + cron = croniter.croniter(f"{minutes} {hours} * * *", now()) + next_date = cron.get_next(datetime.datetime) + next_date = next_date.replace(tzinfo=pytz.timezone('Europe/Moscow')) + mongo.users_collection.update_one({"chat_id": user.chat_id}, {"$set": { + "notify_daily": message.text, + "next_notify_time": next_date + }}) + bot.send_message( + user.chat_id, + f"Принято! Буду каждый день уведомлять тебя в {message.text}", + reply_markup=main_keyboard() + ) + self.set_state(user, "ready") answer = Answer() diff --git a/helpers/keyboards.py b/helpers/keyboards.py new file mode 100644 index 0000000..b945eb2 --- /dev/null +++ b/helpers/keyboards.py @@ -0,0 +1,46 @@ +import telebot + + +def main_keyboard(): + kb = telebot.types.ReplyKeyboardMarkup(True, False) + kb.row("Пары сегодня") + kb.row("Уведомления о парах") + kb.row("Ежедневные уведомления") + kb.row("Сброс настроек") + return kb + + +def notify_keyboard(): + kb = telebot.types.ReplyKeyboardMarkup(True, False) + kb.row("Не уведомлять") + kb.row("5 минут") + kb.row("10 минут") + kb.row("15 минут") + kb.row("20 минут") + return kb + + +def yes_no_keyboard(): + kb = telebot.types.ReplyKeyboardMarkup(True, False) + kb.row("Да") + kb.row("Нет") + return kb + + +def again_keyboard(): + kb = telebot.types.ReplyKeyboardMarkup(True, False) + kb.row("Начать заново") + return kb + + +def groups_keyboard(data): + kb = telebot.types.ReplyKeyboardMarkup(True, False) + for entity in data: + kb.row(entity['description']) + return kb + + +def no_daily_notify(): + kb = telebot.types.ReplyKeyboardMarkup(True, False) + kb.row("Не уведомлять") + return kb diff --git a/helpers/models.py b/helpers/models.py index 0419ea0..956f42b 100644 --- a/helpers/models.py +++ b/helpers/models.py @@ -1,3 +1,4 @@ +import datetime from dataclasses import dataclass from typing import Optional @@ -8,6 +9,8 @@ from marshmallow import EXCLUDE @dataclass class User: chat_id: int + next_notify_time: datetime.datetime + notify_daily: Optional[str] = "09:00" name: Optional[str] = None group: Optional[str] = None hse_id: Optional[int] = None diff --git a/helpers/mongo.py b/helpers/mongo.py index 2aec3cb..bf25ace 100644 --- a/helpers/mongo.py +++ b/helpers/mongo.py @@ -5,6 +5,7 @@ from functools import cached_property import pymongo import settings +from helpers import now from helpers.models import UserSchema, User @@ -50,8 +51,7 @@ class Mongo: return self["lessons"] def get_today_lessons(self, user: User): - zone = zoneinfo.ZoneInfo("Europe/Moscow") - today = datetime.datetime.now(zone) + today = now() tomorrow = today + datetime.timedelta(days=1) tomorrow = datetime.datetime(year=tomorrow.year, month=tomorrow.month, day=tomorrow.day) lessons = [] diff --git a/settings.py b/settings.py index ac6f28f..bef9bc6 100644 --- a/settings.py +++ b/settings.py @@ -1,5 +1,5 @@ import os - +import zoneinfo MONGO_USER = os.getenv("MONGO_USER", "mongo") MONGO_PASSWORD = os.getenv("MONGO_PASSWORD", "password") @@ -7,3 +7,4 @@ MONGO_HOST = os.getenv("MONGO_HOST", "localhost") DEBUG = os.getenv("DEBUG", "true") == "true" RUZ_API = "https://ruz.hse.ru/api/" +MOSCOW_TIMEZONE = zoneinfo.ZoneInfo("Europe/Moscow")