diff --git a/.gitignore b/.gitignore index 566e654..50eb5fa 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ broadcast broadcast/* Pipfile.lock /broadcast/* + +.python-version diff --git a/Pipfile.lock b/Pipfile.lock index 3a8cd14..1622573 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "b1796abc6e82f99b5ed7d9051067fdae96749df5353cbef419e3032a091dc355" + "sha256": "1688bc3e1dfa8255a850bc8ba04feb0140fb2da72bcfc30442a1d087ad429e75" }, "pipfile-spec": 6, "requires": { - "python_version": "3.9" + "python_version": "3.10" }, "sources": [ { @@ -403,6 +403,7 @@ }, "pillow": { "hashes": [ + "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33", "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b", "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e", "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35", @@ -436,10 +437,16 @@ "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a", "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e", "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f", + "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848", "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57", + "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f", + "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c", "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9", "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5", + "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9", "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d", + "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0", + "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1", "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e", "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815", "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0", @@ -689,10 +696,10 @@ }, "python-dotenv": { "hashes": [ - "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5", - "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045" + "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49", + "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a" ], - "version": "==0.21.0" + "version": "==0.21.1" }, "pytz": { "hashes": [ diff --git a/zmanim_bot/handlers/__init__.py b/zmanim_bot/handlers/__init__.py index 072c2a6..14627a4 100644 --- a/zmanim_bot/handlers/__init__.py +++ b/zmanim_bot/handlers/__init__.py @@ -6,9 +6,9 @@ from zmanim_bot.helpers import CallbackPrefixes, LOCATION_PATTERN from zmanim_bot.misc import dp from zmanim_bot.states import FeedbackState, ConverterGregorianDateState, ConverterJewishDateState, \ - LocationNameState, ZmanimGregorianDateState + LocationNameState, ZmanimGregorianDateState, CreateNotificationState from . import admin, converter, errors, festivals, forms, main, menus, settings, \ - incorrect_text_handler, reset_handler, payments, geolocation + incorrect_text_handler, reset_handler, payments, geolocation, notifications from ..texts.single import buttons __all__ = ['register_handlers'] @@ -67,6 +67,7 @@ def register_handlers(): dp.register_message_handler(menus.handle_settings_menu, commands=['settings']) dp.register_message_handler(menus.handle_settings_menu, text=buttons.mm_settings) dp.register_message_handler(menus.handle_donate, text=buttons.mm_donate) + dp.register_message_handler(menus.handle_notifications, text='Notifications') # todo # festivals dp.register_message_handler(festivals.handle_fast, text=buttons.FASTS) @@ -121,5 +122,12 @@ def register_handlers(): dp.register_message_handler(payments.on_success_payment, content_types=[ContentType.SUCCESSFUL_PAYMENT]) dp.register_message_handler(payments.on_success_payment, content_types=[ContentType]) + # Notifications + dp.register_callback_query_handler(notifications.init_notification, text_startswith=CallbackPrefixes.init_notification_setup) + dp.register_callback_query_handler(notifications.init_zmanim_notification, text_startswith=CallbackPrefixes.select_notification_zmanim) + dp.register_message_handler(notifications.set_offset, state=CreateNotificationState.waiting_for_offset_state) + dp.register_message_handler(notifications.set_message, state=CreateNotificationState.waiting_for_message_text_state) + dp.register_message_handler(notifications.set_name, state=CreateNotificationState.waiting_for_name_state) + # unknown messages (SHOULD BE LAST!) dp.register_message_handler(incorrect_text_handler.handle_incorrect_text) diff --git a/zmanim_bot/handlers/menus.py b/zmanim_bot/handlers/menus.py index 4302622..abcd53c 100644 --- a/zmanim_bot/handlers/menus.py +++ b/zmanim_bot/handlers/menus.py @@ -41,3 +41,10 @@ async def handle_settings_menu(msg: Message): @track('Donate button') async def handle_donate(msg: Message): await msg.reply(messages.donate_init, reply_markup=DONATE_KB) + + +@chat_action('text') +@track('.') # todo +async def handle_notifications(msg: Message): + kb = menus.get_notifications_welcome_menu(False) + await msg.reply('notifications management', reply_markup=kb) # todo diff --git a/zmanim_bot/handlers/notifications.py b/zmanim_bot/handlers/notifications.py new file mode 100644 index 0000000..3ce123c --- /dev/null +++ b/zmanim_bot/handlers/notifications.py @@ -0,0 +1,70 @@ +from aiogram import Bot +from aiogram.dispatcher import FSMContext +from aiogram.types import Message, CallbackQuery + +from zmanim_bot.helpers import CallbackPrefixes, FeatureType, ZmanimType +from zmanim_bot.service import notifications_service +from zmanim_bot.states import CreateNotificationState + + +async def handle_new_notifications_menu(msg: Message): + resp = 'select notification type:' # todo + kb = '' + + +async def init_notification(call: CallbackQuery): + feature_type = call.data.split(CallbackPrefixes.init_notification_setup)[1] + resp, kb = notifications_service.process_init_notification(feature_type) + await Bot.get_current().send_message(call.message.chat.id, resp, reply_markup=kb) + await call.answer() + + +async def init_zmanim_notification(call: CallbackQuery, state: FSMContext): + # todo hide zmanim buttons + zman_type = call.data.split(CallbackPrefixes.select_notification_zmanim)[1] + + await CreateNotificationState.waiting_for_offset_state.set() + await state.update_data( + { + 'feature': FeatureType.zmanim, + 'zman_type': ZmanimType(zman_type).name # todo get enum value! + } + ) + + resp = 'add offset to notification (send 0 for no ofset)' # todo translate; +/- hint + await Bot.get_current().send_message(call.message.chat.id, resp) + + +async def set_offset(msg: Message, state: FSMContext): + try: + offset = notifications_service.process_offset(msg.text) + except ValueError: + return await Bot.get_current().send_message(msg.from_user.id, 'incorrect offset') # todo translate + + await state.update_data({'offset': offset}) + + resp = 'write a message you want to get' # todo translate + await msg.reply(resp) + await CreateNotificationState.next() + + +async def set_message(msg: Message, state: FSMContext): + await state.update_data({'message': msg.text}) + + resp = 'write a name for your notification' # todo translate + await msg.reply(resp) + await CreateNotificationState.next() + + +async def set_name(msg: Message, state: FSMContext): + data = await state.get_data() + data['name'] = msg.text + + await notifications_service.create_notification(data) + + resp = f'notification {msg.text} saved, manage it in ssetings' # todo translate + await msg.reply(resp) + await state.finish() + + +# todo fix state-based routing diff --git a/zmanim_bot/helpers.py b/zmanim_bot/helpers.py index 1b2609a..7dabf44 100644 --- a/zmanim_bot/helpers.py +++ b/zmanim_bot/helpers.py @@ -1,4 +1,5 @@ from datetime import date +from enum import Enum from typing import Tuple from zmanim_bot.exceptions import IncorrectGregorianDateException, IncorrectLocationException @@ -40,6 +41,39 @@ class CallbackPrefixes: update_fast = 'uf:' update_yom_tov = 'uy:' + init_notification_setup = 'a:' + select_notification_zmanim = 'b:' + + +class FeatureType: + zmanim = '0' + shabbat = '1' + rosh_chodesh = '2' + holiday = '3' + yom_tov = '4' + fast = '5' + daf_yomi = '6' + + +class ZmanimType(str, Enum): + alos = 'a' + misheyakir_10_2 = 'b' + sunrise = 'c' + sof_zman_shema_ma = 'd' + sof_zman_shema_gra = 'e' + sof_zman_tefila_ma = 'f' + sof_zman_tefila_gra = 'g' + chatzos = 'h' + mincha_gedola = 'i' + mincha_ketana = 'j' + plag_mincha = 'k' + sunset = 'l' + tzeis_5_95_degrees = 'm' + tzeis_8_5_degrees = 'n' + tzeis_42_minutes = 'o' + tzeis_72_minutes = 'p' + chatzot_laila = 'q' + def parse_coordinates(coordinates: str) -> Tuple[float, float]: try: diff --git a/zmanim_bot/integrations/zmanim_api_client.py b/zmanim_bot/integrations/zmanim_api_client.py index 2d3744a..23a9f2d 100644 --- a/zmanim_bot/integrations/zmanim_api_client.py +++ b/zmanim_bot/integrations/zmanim_api_client.py @@ -26,7 +26,7 @@ async def get_zmanim(location: Tuple[float, float], zmanim_settings: dict, date_ if date_: params['date'] = date_ - async with bot.session.post(url, params=params, json=zmanim_settings) as resp: + async with (await bot.get_session()).post(url, params=params, json=zmanim_settings) as resp: raw_resp = await resp.json() return Zmanim(**raw_resp) @@ -47,7 +47,7 @@ async def get_shabbat( if date_: params['date'] = date_ - async with bot.session.get(url, params=params) as resp: + async with (await bot.get_session()).get(url, params=params) as resp: raw_resp = await resp.json() return Shabbat(**raw_resp) @@ -56,7 +56,7 @@ async def get_daf_yomi(date_=None) -> DafYomi: url = config.ZMANIM_API_URL.format('daf_yomi') params = None if not date_ else {'date': date_} - async with bot.session.get(url, params=params) as resp: + async with (await bot.get_session()).get(url, params=params) as resp: raw_resp = await resp.json() return DafYomi(**raw_resp) @@ -65,7 +65,7 @@ async def get_rosh_chodesh(date_=None) -> RoshChodesh: url = config.ZMANIM_API_URL.format('rosh_chodesh') params = None if not date_ else {'date': date_} - async with bot.session.get(url, params=params) as resp: + async with (await bot.get_session()).get(url, params=params) as resp: raw_resp = await resp.json() return RoshChodesh(**raw_resp) @@ -85,7 +85,7 @@ async def get_generic_yomtov( 'havdala': havdala_opinion } - async with bot.session.get(url, params=params) as resp: + async with (await bot.get_session()).get(url, params=params) as resp: raw_resp = await resp.json() return YomTov(**raw_resp) @@ -99,7 +99,7 @@ async def get_generic_fast(name: str, location: Tuple[float, float], havdala_opi 'havdala': havdala_opinion } - async with bot.session.get(url, params=params) as resp: + async with (await bot.get_session()).get(url, params=params) as resp: raw_resp = await resp.json() return Fast(**raw_resp) @@ -108,7 +108,7 @@ async def get_generic_holiday(name: str) -> Holiday: url = config.ZMANIM_API_URL.format('holiday') params = {'holiday_name': name} - async with bot.session.get(url, params=params) as resp: + async with (await bot.get_session()).get(url, params=params) as resp: raw_resp = await resp.json() return Holiday(**raw_resp) @@ -121,7 +121,7 @@ async def get_israel_holidays() -> IsraelHolidays: for name in ['yom_hashoah', 'yom_hazikaron', 'yom_haatzmaut', 'yom_yerushalaim']: params = {'holiday_name': name} - async with bot.session.get(url, params=params) as resp: + async with (await bot.get_session()).get(url, params=params) as resp: raw_resp = await resp.json() result.append((name, date.fromisoformat(raw_resp['date']))) diff --git a/zmanim_bot/keyboards/menus.py b/zmanim_bot/keyboards/menus.py index 3dec0b3..cc5f428 100644 --- a/zmanim_bot/keyboards/menus.py +++ b/zmanim_bot/keyboards/menus.py @@ -15,7 +15,7 @@ def get_main_menu() -> ReplyKeyboardMarkup: kb = ReplyKeyboardMarkup(resize_keyboard=True) kb.add(buttons.mm_zmanim.value, buttons.mm_shabbat.value, buttons.mm_holidays.value) kb.add(buttons.mm_rh.value, buttons.mm_daf_yomi.value, buttons.mm_fasts.value) - kb.add(buttons.mm_zmanim_by_date.value, buttons.mm_converter.value) + kb.add(buttons.mm_zmanim_by_date.value, 'Notifications', buttons.mm_converter.value) # todo translate kb.add(buttons.hm_report.value, buttons.mm_donate.value, buttons.mm_settings.value) if i18n_.is_rtl(): @@ -113,3 +113,11 @@ def get_report_keyboard() -> ReplyKeyboardMarkup: kb = ReplyKeyboardMarkup(resize_keyboard=True) kb.row(buttons.cancel.value, buttons.done.value) return kb + + +def get_notifications_welcome_menu(nothing_to_manage: bool) -> ReplyKeyboardMarkup: # todo translate + kb = ReplyKeyboardMarkup(resize_keyboard=True) + row = ['Create new notification'] + not nothing_to_manage and row.append('Manage my notifications') + kb.row(*row) + return kb diff --git a/zmanim_bot/keyboards/notifications.py b/zmanim_bot/keyboards/notifications.py new file mode 100644 index 0000000..a5028ee --- /dev/null +++ b/zmanim_bot/keyboards/notifications.py @@ -0,0 +1,22 @@ +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton + +from zmanim_bot.helpers import CallbackPrefixes, ZmanimType + + +def add_notification_button_to_kb(kb: InlineKeyboardMarkup, feature_type: str) -> None: + # todo translate + kb.row(InlineKeyboardButton( + text='🔔 Create notification', + callback_data=f'{CallbackPrefixes.init_notification_setup}{feature_type}' + )) + + +zmanim_notifications_kb = InlineKeyboardMarkup() + +for zman_type in ZmanimType: + zmanim_notifications_kb.row( + InlineKeyboardButton( + text=f'{zman_type.name}', + callback_data=f'{CallbackPrefixes.select_notification_zmanim}{zman_type.value}' + ) + ) diff --git a/zmanim_bot/notificator/__init__.py b/zmanim_bot/notificator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zmanim_bot/notificator/events.py b/zmanim_bot/notificator/events.py new file mode 100644 index 0000000..e69de29 diff --git a/zmanim_bot/notificator/trigger.py b/zmanim_bot/notificator/trigger.py new file mode 100644 index 0000000..b46faed --- /dev/null +++ b/zmanim_bot/notificator/trigger.py @@ -0,0 +1,32 @@ +from enum import Enum + + +class TriggerType(Enum): + gregorian_date = 'gregorian_date' + hebrew_date = 'hebrew_date' + zman = 'zman' + + +class ZmanType(Enum): + alos = 'alos' + misheyakir_10_2 = 'misheyakir_10_2' + sunrise = 'sunrise' + sof_zman_shema_ma = 'sof_zman_shema_ma' + sof_zman_shema_gra = 'sof_zman_shema_gra' + sof_zman_tefila_ma = 'sof_zman_tefila_ma' + sof_zman_tefila_gra = 'sof_zman_tefila_gra' + chatzos = 'chatzos' + mincha_gedola = 'mincha_gedola' + mincha_ketana = 'mincha_ketana' + plag_mincha = 'plag_mincha' + sunset = 'sunset' + tzeis_5_95_degrees = 'tzeis_5_95_degrees' + tzeis_8_5_degrees = 'tzeis_8_5_degrees' + tzeis_42_minutes = 'tzeis_42_minutes' + tzeis_72_minutes = 'tzeis_72_minutes' + chatzot_laila = 'chatzot_laila' + + + + + diff --git a/zmanim_bot/repository/_storage.py b/zmanim_bot/repository/_storage.py index a5bd8df..227aca2 100644 --- a/zmanim_bot/repository/_storage.py +++ b/zmanim_bot/repository/_storage.py @@ -5,14 +5,16 @@ from aiogram import types from zmanim_bot.config import config -from zmanim_bot.exceptions import (ActiveLocationException, - MaxLocationLimitException, - NoLanguageException, NoLocationException, - NonUniqueLocationException, - NonUniqueLocationNameException) +from zmanim_bot.exceptions import ( + ActiveLocationException, + MaxLocationLimitException, + NoLanguageException, NoLocationException, + NonUniqueLocationException, + NonUniqueLocationNameException +) from zmanim_bot.integrations.geo_client import get_location_name from zmanim_bot.misc import db_engine -from .models import Location, User, UserInfo, ZmanimSettings +from .models import Location, User, UserInfo, ZmanimSettings, Event __all__ = [ '_get_or_create_user', @@ -33,6 +35,8 @@ 'set_processor_type', 'get_omer_flag', 'set_omer_flag', + '_create_event', + '_get_user_by_id', ] MAX_LOCATION_NAME_SIZE = 27 @@ -83,6 +87,14 @@ async def _get_or_create_user(tg_user: types.User) -> User: return user +async def _get_user_by_id(user_id: int) -> User: + user = await db_engine.find_one(User, User.user_id == user_id) + + if not user: + raise ValueError(f'User {user_id} not found!') + return user + + async def get_lang(tg_user: types.User) -> Optional[str]: user = await _get_or_create_user(tg_user) lang = user.language @@ -239,3 +251,8 @@ async def set_omer_flag(tg_user: types.User, omer_flag: bool, omer_time: Optiona user.omer.notification_time = omer_time await db_engine.save(user) + + +async def _create_event(event: Event): + await db_engine.save(event) + diff --git a/zmanim_bot/repository/bot_repository.py b/zmanim_bot/repository/bot_repository.py index c0dda3a..b8436bc 100644 --- a/zmanim_bot/repository/bot_repository.py +++ b/zmanim_bot/repository/bot_repository.py @@ -3,12 +3,13 @@ from aiogram.types import User from ._storage import * -from .models import Location +from .models import Location, Event from .models import User as BotUser __all__ = [ 'Location', 'get_or_create_user', + 'get_user_by_id', 'get_or_set_zmanim', 'get_or_set_cl', 'get_or_set_lang', @@ -19,6 +20,7 @@ 'delete_location', 'get_or_set_processor_type', 'get_or_set_omer_flag', + 'create_event', ] @@ -27,6 +29,10 @@ async def get_or_create_user() -> BotUser: return await _get_or_create_user(user) +async def get_user_by_id(user_id: int) -> BotUser: + return await _get_user_by_id(user_id) + + async def get_or_set_lang(lang: str = None) -> Optional[str]: user = User.get_current() return await get_lang(user) if not lang else await set_lang(user, lang) @@ -82,3 +88,8 @@ async def get_or_set_omer_flag( omer_time = zmanim and zmanim.tzeis_8_5_degrees.isoformat() return await set_omer_flag(user, omer_flag, omer_time) return await get_omer_flag(user) + + +async def create_event(event: Event): + await _create_event(event) + diff --git a/zmanim_bot/repository/models.py b/zmanim_bot/repository/models.py index 50b8be1..45a0bee 100644 --- a/zmanim_bot/repository/models.py +++ b/zmanim_bot/repository/models.py @@ -1,15 +1,23 @@ -from datetime import datetime as dt +from __future__ import annotations + +from abc import abstractmethod +from datetime import datetime as dt, timedelta from typing import List, Optional, Tuple from odmantic import EmbeddedModel, Field, Model +from zmanim.hebrew_calendar.jewish_date import JewishDate from zmanim_bot.config import config from zmanim_bot.exceptions import NoLocationException, UnknownProcessorException +from zmanim_bot.integrations.zmanim_api_client import get_zmanim from zmanim_bot.processors import PROCESSORS from zmanim_bot.processors.base import BaseProcessor +from zmanim_bot.repository import bot_repository HAVDALA_OPINIONS = ['tzeis_5_95_degrees', 'tzeis_8_5_degrees', 'tzeis_42_minutes', 'tzeis_72_minutes'] +# todo refactor: split repositories! + class UserInfo(EmbeddedModel): first_name: Optional[str] = None @@ -99,3 +107,141 @@ def get_processor(self, location: Optional[Location] = None) -> BaseProcessor: except KeyError: raise UnknownProcessorException() + +class Event(Model): + owner_id: int + name: str + message: str + trigger: GregorianDateTrigger | JewishDateTrigger | ZmanTrigger + + class Config: + collection = 'events' + + +class EventTrigger(EmbeddedModel): + type: str + + @abstractmethod + def calculate_next_event_dt( + self, from_point: dt | None = None, + user_id: int | None = None + ) -> dt: + raise NotImplemented() + + +class GregorianDateTrigger(EventTrigger): + """ + A trigger that executes on each gregorian date every year + """ + # todo user's timezone? + + month: int + day: int + hour: int + minute: int + + def calculate_next_event_dt(self, from_point: dt | None = None, _=None) -> dt: + if not from_point: + from_point = dt.now() + + next_event = dt( + year=from_point.year, + month=self.month, + day=self.day, + hour=self.hour, + minute=self.minute + ) + if next_event < from_point: + next_event = dt( + year=from_point.year + 1, + month=self.month, + day=self.day, # todo: days in month less then in previous year? + hour=self.hour, + minute=self.minute + ) + return next_event + + +class JewishDateTrigger(EventTrigger): + """ + A trigger that executes on each jewish date every year + """ + # todo user's timezone? + + j_month: int + j_day: int + hour: int + minute: int + + def calculate_next_event_dt(self, from_point: dt | None = None, _=None) -> dt: + if not from_point: + from_point = dt.now() + + j_from_point = JewishDate.from_date(from_point.date()) + + next_event_j_date = JewishDate( + j_from_point.jewish_year, + self.j_month, + self.j_day + ) + if next_event_j_date < j_from_point: + next_event_j_date = JewishDate( + j_from_point.jewish_year + 1, # todo: adar ii? + self.j_month, # todo: days in month less then in previous year? + self.j_day + ) + + next_event_date = next_event_j_date.gregorian_date + next_event = dt( + year=next_event_date.year, + month=next_event_date.month, + day=next_event_date.day, + hour=self.hour, + minute=self.minute + ) + return next_event + + +class ZmanTrigger(EventTrigger): + """ + A trigger that executes each day at selected zman + """ + zman_type: str + type: str = 'ZmanTrigger' + offset: int # todo missing implementation + + async def __get_zman(self, date_: str, user_id: int) -> dt: + user = await bot_repository.get_user_by_id(user_id) + zmanim = await get_zmanim( + (user.location.lat, user.location.lng), + {self.zman_type: True}, + date_ + ) + next_event = getattr(zmanim, self.zman_type) + return next_event + + async def calculate_next_event_dt( + self, + from_point: dt | None = None, + user_id: int | None = None + ) -> dt: + if not user_id: + raise ValueError('No user_id was provided!') + if not from_point: + from_point = dt.now() + + next_event = await self.__get_zman(from_point.date().isoformat(), user_id) + + if next_event < from_point.replace(tzinfo=next_event.tzinfo): + from_point += timedelta(days=1) + next_event = await self.__get_zman(from_point.date().isoformat(), user_id) + + # todo asur be-melacha? + return next_event + + +Event.update_forward_refs() + + +# todo: candle light trigger +# todo: significant day trigger diff --git a/zmanim_bot/service/notifications_service.py b/zmanim_bot/service/notifications_service.py new file mode 100644 index 0000000..4a32fd0 --- /dev/null +++ b/zmanim_bot/service/notifications_service.py @@ -0,0 +1,57 @@ +import re + +from aiogram.types import InlineKeyboardMarkup + +from zmanim_bot.helpers import FeatureType +from zmanim_bot.keyboards import notifications as kb_set +from zmanim_bot.repository import bot_repository +from zmanim_bot.repository.models import Event, ZmanTrigger + + +def process_init_notification(feature_type: str) -> tuple[str, InlineKeyboardMarkup]: + match feature_type: + case FeatureType.zmanim: + kb = kb_set.zmanim_notifications_kb + case _: + raise ValueError('Unsupported notification type!') # todo better handle + + resp = 'select zman for notification' # todo translate + return resp, kb + + +def process_offset(user_inout: str) -> int: + offset_pattern = r'^(\+|-)?(\d+)$' + + if re_match := re.match(offset_pattern, user_inout): + offset = int(re_match.groups()[1]) + + if re_match.groups()[0] == '-': + offset *= -1 + else: + raise ValueError + + return offset + + +async def create_notification(data: dict): + user = await bot_repository.get_or_create_user() + feature_type: str = data['feature'] + + match feature_type: + case FeatureType.zmanim: + trigger = ZmanTrigger( + zman_type=data['zman_type'], + offset=data['offset'] + ) + case _: + raise ValueError('Unsupported notification type!') # todo better handle + + event = Event( + owner_id=user.user_id, + name=data['name'], + message=data['message'], + trigger=trigger + ) + await bot_repository.create_event(event) + + diff --git a/zmanim_bot/service/zmanim_service.py b/zmanim_bot/service/zmanim_service.py index 609ed89..50a121d 100644 --- a/zmanim_bot/service/zmanim_service.py +++ b/zmanim_bot/service/zmanim_service.py @@ -3,7 +3,8 @@ from aiogram.dispatcher import FSMContext from aiogram.types import CallbackQuery -from zmanim_bot.helpers import CallbackPrefixes +import zmanim_bot.keyboards.notifications +from zmanim_bot.helpers import CallbackPrefixes, FeatureType from zmanim_bot.integrations import zmanim_api_client from zmanim_bot.integrations.zmanim_models import Zmanim from zmanim_bot.keyboards import inline @@ -43,6 +44,7 @@ async def send_zmanim(*, state: FSMContext, date: str = None, call: CallbackQuer CallbackPrefixes.update_zmanim, date_=date ) + zmanim_bot.keyboards.notifications.add_notification_button_to_kb(kb, FeatureType.zmanim) await user.get_processor(location=location).send_zmanim(data, kb) if call: await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb) diff --git a/zmanim_bot/states.py b/zmanim_bot/states.py index a92d81f..20f4a4c 100644 --- a/zmanim_bot/states.py +++ b/zmanim_bot/states.py @@ -20,3 +20,9 @@ class FeedbackState(StatesGroup): class LocationNameState(StatesGroup): waiting_for_location_name_state = State() + + +class CreateNotificationState(StatesGroup): + waiting_for_offset_state = State() + waiting_for_message_text_state = State() + waiting_for_name_state = State() diff --git a/zmanim_bot/texts/single/buttons.py b/zmanim_bot/texts/single/buttons.py index 1eceb01..0dc95e4 100644 --- a/zmanim_bot/texts/single/buttons.py +++ b/zmanim_bot/texts/single/buttons.py @@ -47,6 +47,19 @@ 'text': sm_format_text_option, } +notification_types = { + 'jefish_date': '', + 'gragorian_date': '', + 'day_of_week': '', + 'zman': '', + 'candle_lighting': '', + 'havdala': '', + 'fast_start': '', + 'fast_end': '', + 'omer_count': '', + 'rosh_chodesh': '' +} + settings_enabled = _('enabled') settings_disabled = _('disabled')