├── broadcast
├── __init__.py
├── data.py
└── broadcast.py
├── zmanim_bot
├── __init__.py
├── admin
│ ├── __init__.py
│ ├── states.py
│ └── report_management.py
├── repository
│ ├── __init__.py
│ ├── bot_repository.py
│ ├── models.py
│ └── _storage.py
├── service
│ ├── __init__.py
│ ├── payments_service.py
│ ├── converter_service.py
│ ├── festivals_service.py
│ ├── zmanim_service.py
│ └── settings_service.py
├── handlers
│ ├── utils
│ │ ├── __init__.py
│ │ ├── warnings.py
│ │ └── redirects.py
│ ├── incorrect_text_handler.py
│ ├── reset_handler.py
│ ├── payments.py
│ ├── converter.py
│ ├── menus.py
│ ├── festivals.py
│ ├── admin.py
│ ├── main.py
│ ├── errors.py
│ ├── settings.py
│ ├── forms.py
│ ├── geolocation.py
│ └── __init__.py
├── integrations
│ ├── __init__.py
│ ├── open_topo_data_client.py
│ ├── geo_client.py
│ ├── zmanim_api_client.py
│ └── zmanim_models.py
├── processors
│ ├── text
│ │ ├── __init__.py
│ │ ├── text_processor.py
│ │ └── composer.py
│ ├── image
│ │ ├── __init__.py
│ │ ├── res
│ │ │ ├── fonts
│ │ │ │ ├── gothic.TTF
│ │ │ │ ├── fontello.ttf
│ │ │ │ ├── gothic-bold.TTF
│ │ │ │ ├── gothic_old.TTF
│ │ │ │ ├── gothic-bold_old.TTF
│ │ │ │ └── VarelaRound-Regular.ttf
│ │ │ └── backgrounds
│ │ │ │ ├── fast.png
│ │ │ │ ├── pesah.png
│ │ │ │ ├── purim.png
│ │ │ │ ├── chanuka.png
│ │ │ │ ├── daf_yomi.png
│ │ │ │ ├── shabbos.png
│ │ │ │ ├── shavuot.png
│ │ │ │ ├── succos.png
│ │ │ │ ├── zmanim.png
│ │ │ │ ├── lagbaomer.png
│ │ │ │ ├── tubishvat.png
│ │ │ │ ├── yom_kippur.png
│ │ │ │ ├── zmanim_bot.ai
│ │ │ │ ├── rosh_hashana.png
│ │ │ │ ├── rosh_hodesh.png
│ │ │ │ ├── israel_holidays.png
│ │ │ │ ├── shmini_atzeret.png
│ │ │ │ └── shabbos_attention.png
│ │ ├── image_processor.py
│ │ └── renderer.py
│ ├── __init__.py
│ ├── text_utils.py
│ └── base.py
├── texts
│ ├── plural
│ │ ├── __init__.py
│ │ └── units.py
│ ├── __init__.py
│ ├── single
│ │ ├── __init__.py
│ │ ├── helpers.py
│ │ ├── zmanim.py
│ │ ├── headers.py
│ │ ├── buttons.py
│ │ ├── messages.py
│ │ └── names.py
│ ├── translators.py
│ └── commands.py
├── keyboards
│ ├── __init__.py
│ ├── menus.py
│ └── inline.py
├── res
│ └── on_sucess_donate.jpg
├── middlewares
│ ├── __init__.py
│ ├── sentry_context_middleware.py
│ ├── chat_type_middleware.py
│ ├── black_list_middleware.py
│ └── i18n.py
├── states.py
├── misc.py
├── utils.py
├── main.py
├── exceptions.py
├── helpers.py
└── config.py
├── .dockerignore
├── locales
├── he
│ └── LC_MESSAGES
│ │ └── zmanim_bot.mo
└── ru
│ └── LC_MESSAGES
│ └── zmanim_bot.mo
├── Dockerfile
├── .gitignore
├── .github
└── workflows
│ ├── ci_beta.yaml
│ └── ci_prod.yaml
└── pyproject.toml
/broadcast/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/admin/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/repository/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/service/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/integrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/text/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/plural/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .env
2 | Pipfile.lock
3 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/__init__.py:
--------------------------------------------------------------------------------
1 | from . import plural, single
2 |
--------------------------------------------------------------------------------
/zmanim_bot/keyboards/__init__.py:
--------------------------------------------------------------------------------
1 | from . import inline, menus
2 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/single/__init__.py:
--------------------------------------------------------------------------------
1 | from . import buttons, headers, helpers, messages, names, zmanim
2 |
--------------------------------------------------------------------------------
/locales/he/LC_MESSAGES/zmanim_bot.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/locales/he/LC_MESSAGES/zmanim_bot.mo
--------------------------------------------------------------------------------
/locales/ru/LC_MESSAGES/zmanim_bot.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/locales/ru/LC_MESSAGES/zmanim_bot.mo
--------------------------------------------------------------------------------
/zmanim_bot/res/on_sucess_donate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/res/on_sucess_donate.jpg
--------------------------------------------------------------------------------
/zmanim_bot/texts/translators.py:
--------------------------------------------------------------------------------
1 | fake_gettext = lambda word: word
2 | fake_gettext_plural = lambda word_s, word_p: (word_s, word_p)
3 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/fonts/gothic.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/fonts/gothic.TTF
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/fonts/fontello.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/fonts/fontello.ttf
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/fast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/fast.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/pesah.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/pesah.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/purim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/purim.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/fonts/gothic-bold.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/fonts/gothic-bold.TTF
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/fonts/gothic_old.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/fonts/gothic_old.TTF
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/chanuka.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/chanuka.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/daf_yomi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/daf_yomi.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/shabbos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/shabbos.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/shavuot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/shavuot.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/succos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/succos.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/zmanim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/zmanim.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/lagbaomer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/lagbaomer.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/tubishvat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/tubishvat.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/yom_kippur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/yom_kippur.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/zmanim_bot.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/zmanim_bot.ai
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/fonts/gothic-bold_old.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/fonts/gothic-bold_old.TTF
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/rosh_hashana.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/rosh_hashana.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/rosh_hodesh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/rosh_hodesh.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/fonts/VarelaRound-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/fonts/VarelaRound-Regular.ttf
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/israel_holidays.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/israel_holidays.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/shmini_atzeret.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/shmini_atzeret.png
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/res/backgrounds/shabbos_attention.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benyaming/zmanim_bot/HEAD/zmanim_bot/processors/image/res/backgrounds/shabbos_attention.png
--------------------------------------------------------------------------------
/zmanim_bot/admin/states.py:
--------------------------------------------------------------------------------
1 | from aiogram.dispatcher.filters.state import State, StatesGroup
2 |
3 |
4 | class AdminReportResponse(StatesGroup):
5 | waiting_for_response_text = State()
6 | waiting_for_payload = State()
7 |
--------------------------------------------------------------------------------
/broadcast/data.py:
--------------------------------------------------------------------------------
1 | ru_message = '''тестовая рассылка'''
2 |
3 | en_message = '''broadcast test'''
4 |
5 | ru_button_text = 'Поддержка zmanim_bot'
6 | en_button_text = 'Donate to zmanim_bot'
7 |
8 | link = 'https://telegra.ph/Donate-to-Zmanim-bot-03-31'
9 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/__init__.py:
--------------------------------------------------------------------------------
1 | from zmanim_bot.processors.base import BaseProcessor
2 |
3 | from .image.image_processor import ImageProcessor
4 | from .text.text_processor import TextProcessor
5 |
6 | PROCESSORS = {
7 | 'image': ImageProcessor,
8 | 'text': TextProcessor
9 | }
10 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/plural/units.py:
--------------------------------------------------------------------------------
1 | from ..translators import fake_gettext_plural as __
2 |
3 | # Time units
4 | tu_year = __('year', 'years')
5 | tu_month = __('month', 'months')
6 | tu_day = __('day', 'days')
7 | tu_hour = __('hour', 'hours')
8 | tu_minute = __('minute', 'minutes')
9 | tu_part = __('part', 'parts')
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.10-slim
2 |
3 | RUN apt update
4 | RUN apt install -y libraqm-dev
5 |
6 | RUN pip install pdm
7 | WORKDIR /home/app
8 | COPY . .
9 | WORKDIR /home/app/zmanim_bot
10 | RUN pdm install
11 | ENV PYTHONPATH=/home/app
12 | ENV DOCKER_MODE=true
13 | EXPOSE 8000
14 | CMD ["pdm", "run", "python", "main.py"]
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | \.idea/
3 |
4 | zmanim/__pycache__/
5 |
6 | zmanim/res/__pycache__/
7 |
8 | zmanim/res/image/__pycache__/
9 |
10 | zmanim/res/image/tmp\.py
11 |
12 | zmanim/res/localizations/__pycache__/
13 |
14 | __pycache__/
15 |
16 | \.env
17 | /broadcast/
18 | /broadcast/**
19 | broadcast
20 | broadcast/*
21 | Pipfile.lock
22 | /broadcast/*
23 | .pdm-python
24 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/incorrect_text_handler.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import Message
2 | from aiogram_metrics import track
3 |
4 | from zmanim_bot.texts.single.messages import incorrect_text
5 | from zmanim_bot.utils import chat_action
6 |
7 |
8 | @chat_action('text')
9 | @track('Incorrect text')
10 | async def handle_incorrect_text(msg: Message):
11 | await msg.reply(incorrect_text)
12 |
13 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/single/helpers.py:
--------------------------------------------------------------------------------
1 | from zmanim_bot.middlewares.i18n import lazy_gettext as _
2 |
3 | and_word = _('and')
4 |
5 | cl_error_warning = _('It is impossible to compute Zmanim for\nyour location. Most likely it is because of\n'
6 | 'Midnight Sun or polar night.')
7 | cl_late_warning = _('Notice! it is recommended to clarify time\nof taking Shabbat in your community.')
8 |
9 | cl_offset = _('before sunset')
10 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/commands.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import BotCommand
2 |
3 |
4 | commands = [
5 | BotCommand('start', 'Start/Начать'),
6 | # BotCommand('help', 'Help/Помощь'),
7 | BotCommand('language', 'Language/Язык'),
8 | BotCommand('location', 'Location/Локация'),
9 | BotCommand('settings', 'Settings/Настройки'),
10 | BotCommand('report', 'Report/Сообщить о проблеме'),
11 | BotCommand('donate', 'Donate/Поддержать')
12 | ]
13 |
--------------------------------------------------------------------------------
/zmanim_bot/middlewares/__init__.py:
--------------------------------------------------------------------------------
1 | from zmanim_bot.misc import dp
2 | from .black_list_middleware import BlackListMiddleware
3 | from .chat_type_middleware import ChatTypeMiddleware
4 | from .i18n import i18n_
5 | from .sentry_context_middleware import SentryContextMiddleware
6 |
7 |
8 | def setup_middlewares():
9 | dp.middleware.setup(ChatTypeMiddleware())
10 | dp.middleware.setup(BlackListMiddleware())
11 | dp.middleware.setup(SentryContextMiddleware())
12 | dp.middleware.setup(i18n_)
13 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/reset_handler.py:
--------------------------------------------------------------------------------
1 | from aiogram.dispatcher import FSMContext
2 |
3 | from zmanim_bot.handlers.utils.redirects import redirect_to_main_menu
4 | from zmanim_bot.utils import chat_action
5 |
6 |
7 | @chat_action('text')
8 | async def handle_start(_, state: FSMContext):
9 | await state.finish()
10 | await redirect_to_main_menu()
11 |
12 |
13 | @chat_action('text')
14 | async def handle_back(_, state: FSMContext):
15 | await state.finish()
16 | await redirect_to_main_menu()
17 |
--------------------------------------------------------------------------------
/zmanim_bot/middlewares/sentry_context_middleware.py:
--------------------------------------------------------------------------------
1 | import sentry_sdk
2 | from aiogram.dispatcher.middlewares import BaseMiddleware
3 | from aiogram.types import Update
4 |
5 |
6 | class SentryContextMiddleware(BaseMiddleware):
7 |
8 | @staticmethod
9 | async def on_pre_process_update(update: Update, _):
10 | if (not update.message) and (not update.callback_query):
11 | return
12 |
13 | sentry_sdk.set_user({
14 | 'id': (update.message or update.callback_query).from_user.id,
15 | 'update': update.to_python()
16 | })
17 |
--------------------------------------------------------------------------------
/zmanim_bot/states.py:
--------------------------------------------------------------------------------
1 | from aiogram.dispatcher.filters.state import State, StatesGroup
2 |
3 |
4 | class ConverterGregorianDateState(StatesGroup):
5 | waiting_for_gregorian_date = State()
6 |
7 |
8 | class ConverterJewishDateState(StatesGroup):
9 | waiting_for_jewish_date = State()
10 |
11 |
12 | class ZmanimGregorianDateState(StatesGroup):
13 | waiting_for_gregorian_date = State()
14 |
15 |
16 | class FeedbackState(StatesGroup):
17 | waiting_for_feedback_text = State()
18 | waiting_for_payload = State()
19 |
20 |
21 | class LocationNameState(StatesGroup):
22 | waiting_for_location_name_state = State()
23 |
--------------------------------------------------------------------------------
/zmanim_bot/integrations/open_topo_data_client.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from aiogram import Bot
4 |
5 | from zmanim_bot.config import config
6 |
7 | OPEN_TOPO_DATA_URL = 'https://api.opentopodata.org/v1/'
8 |
9 | async def get_elevation(lat: float, lng: float) -> int:
10 | url = f'{OPEN_TOPO_DATA_URL}{config.OPEN_TOPO_DATA_DB}'
11 | params = {
12 | 'locations': f'{lat},{lng}'
13 | }
14 | session = await Bot.get_current().get_session()
15 | resp = await session.get(url, params=params)
16 | data = await resp.json()
17 |
18 | try:
19 | elevation = int(data['results'][0]['elevation'])
20 | except Exception as e:
21 | logging.exception(e)
22 | elevation = 0
23 |
24 | return elevation
25 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/utils/warnings.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import User
2 |
3 | import zmanim_bot.keyboards.menus
4 | from zmanim_bot.misc import bot
5 | from zmanim_bot.texts.single import messages
6 |
7 | __all__ = [
8 | 'incorrect_greg_date_warning',
9 | 'incorrect_jew_date_warning'
10 | ]
11 |
12 |
13 | async def incorrect_greg_date_warning():
14 | user_id = User.get_current().id
15 | kb = zmanim_bot.keyboards.menus.get_cancel_keyboard()
16 | await bot.send_message(user_id, messages.incorrect_greg_date, reply_markup=kb)
17 |
18 |
19 | async def incorrect_jew_date_warning():
20 | user_id = User.get_current().id
21 | kb = zmanim_bot.keyboards.menus.get_cancel_keyboard()
22 | await bot.send_message(user_id, messages.incorrect_jew_date, reply_markup=kb)
23 |
--------------------------------------------------------------------------------
/zmanim_bot/misc.py:
--------------------------------------------------------------------------------
1 | import betterlogging as bl
2 | from aiogram import Bot, Dispatcher, types
3 | from aiogram.contrib.fsm_storage.redis import RedisStorage2
4 | from motor.core import AgnosticCollection
5 | from motor.motor_asyncio import AsyncIOMotorClient
6 | from odmantic import AIOEngine
7 |
8 | from .config import config
9 |
10 | bl.basic_colorized_config(level=bl.INFO)
11 | logger = bl.getLogger('zmanim_bot')
12 |
13 | storage = RedisStorage2(host=config.REDIS_HOST, port=config.REDIS_PORT, db=config.REDIS_DB)
14 | bot = Bot(config.BOT_TOKEN, parse_mode=types.ParseMode.HTML)
15 | dp = Dispatcher(bot, storage=storage)
16 | loop = bot.loop
17 |
18 |
19 | motor_client = AsyncIOMotorClient(config.DB_URL)
20 | collection: AgnosticCollection = motor_client[config.DB_NAME][config.DB_COLLECTION_NAME]
21 | db_engine = AIOEngine(motor_client, database=config.DB_NAME)
22 |
23 |
--------------------------------------------------------------------------------
/zmanim_bot/service/payments_service.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime as dt
2 |
3 | from aiogram.types import LabeledPrice, User
4 |
5 | from zmanim_bot.config import config
6 | from zmanim_bot.misc import bot
7 | from zmanim_bot.texts.single import messages
8 |
9 |
10 | async def init_donate(amount: int):
11 | user = User.get_current()
12 | invoice_title = messages.donate_invoice_title.value.format(amount)
13 | price = int(amount) * 100
14 |
15 | await bot.send_invoice(
16 | user.id,
17 | title=invoice_title,
18 | description=messages.donate_invoice_description.value.format(amount),
19 | payload=f'donate:{amount}:usd:{user.id}:{dt.now().isoformat()}',
20 | provider_token=config.PAYMENTS_PROVIDER_TOKEN,
21 | currency='usd',
22 | prices=[LabeledPrice(invoice_title, price)],
23 | provider_data=['qweqweqwe']
24 | )
25 |
--------------------------------------------------------------------------------
/.github/workflows/ci_beta.yaml:
--------------------------------------------------------------------------------
1 | name: Zmanim bot beta CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - beta
7 | # pull_request:
8 | # branches:
9 | # - master
10 |
11 | jobs:
12 | ci:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v1
16 | - name: Docker login
17 | run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
18 | - name: Build
19 | run: docker build -t zmanim-bot -f Dockerfile .
20 | - name: Tags
21 | run: |
22 | docker tag zmanim-bot ${{ secrets.DOCKER_USER }}/zmanim-bot:${{ github.sha }}
23 | docker tag zmanim-bot ${{ secrets.DOCKER_USER }}/zmanim-bot:beta
24 | - name: Push
25 | run: |
26 | docker push ${{ secrets.DOCKER_USER }}/zmanim-bot:${{ github.sha }}
27 | docker push ${{ secrets.DOCKER_USER }}/zmanim-bot:beta
28 |
--------------------------------------------------------------------------------
/.github/workflows/ci_prod.yaml:
--------------------------------------------------------------------------------
1 | name: Zmanim bot prod CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | # pull_request:
8 | # branches:
9 | # - master
10 |
11 | jobs:
12 | ci:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v1
16 | - name: Docker login
17 | run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
18 | - name: Build
19 | run: docker build -t zmanim-bot -f Dockerfile .
20 | - name: Tags
21 | run: |
22 | docker tag zmanim-bot ${{ secrets.DOCKER_USER }}/zmanim-bot:${{ github.sha }}
23 | docker tag zmanim-bot ${{ secrets.DOCKER_USER }}/zmanim-bot:latest
24 | - name: Push
25 | run: |
26 | docker push ${{ secrets.DOCKER_USER }}/zmanim-bot:${{ github.sha }}
27 | docker push ${{ secrets.DOCKER_USER }}/zmanim-bot:latest
28 |
--------------------------------------------------------------------------------
/zmanim_bot/middlewares/chat_type_middleware.py:
--------------------------------------------------------------------------------
1 | from aiogram.dispatcher.middlewares import BaseMiddleware
2 | from aiogram.types import Message, ChatMemberUpdated
3 |
4 | from zmanim_bot.exceptions import UnsupportedChatTypeException
5 |
6 | error = 'This bot supports only private chats! Please, remove the bot from the group!'
7 |
8 |
9 | class ChatTypeMiddleware(BaseMiddleware):
10 |
11 | @staticmethod
12 | async def on_pre_process_my_chat_member(update: ChatMemberUpdated, *_):
13 | if update.new_chat_member.status in ('member', 'restricted'):
14 | await update.bot.send_message(update.chat.id, error)
15 | raise UnsupportedChatTypeException()
16 |
17 | @staticmethod
18 | async def on_process_message(msg: Message, *_):
19 | if msg.chat.type in ('group', 'supergroup') and not msg.left_chat_member:
20 | await msg.reply(error)
21 | raise UnsupportedChatTypeException()
22 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/payments.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import CallbackQuery, PreCheckoutQuery, Message, InputFile
2 | from aiogram_metrics import track
3 |
4 | from zmanim_bot.helpers import CallbackPrefixes
5 | from zmanim_bot.misc import bot
6 | from zmanim_bot.service import payments_service
7 | from zmanim_bot.texts.single import messages
8 | from zmanim_bot.utils import chat_action
9 |
10 |
11 | @chat_action('text')
12 | @track('Init donate')
13 | async def handle_donate(call: CallbackQuery):
14 | amount = int(call.data.split(CallbackPrefixes.donate)[1])
15 | await call.answer()
16 | await payments_service.init_donate(amount)
17 |
18 |
19 | @track('Pre-checkout')
20 | async def handle_pre_checkout(query: PreCheckoutQuery):
21 | await bot.answer_pre_checkout_query(query.id, ok=True)
22 |
23 |
24 | async def on_success_payment(msg: Message):
25 | await bot.send_photo(msg.from_user.id, InputFile('./res/on_sucess_donate.jpg'), caption=messages.donate_thanks)
26 |
--------------------------------------------------------------------------------
/zmanim_bot/middlewares/black_list_middleware.py:
--------------------------------------------------------------------------------
1 | from aiogram import types
2 | from aiogram.dispatcher.middlewares import BaseMiddleware
3 |
4 | from zmanim_bot.exceptions import AccessDeniedException
5 |
6 |
7 | class BlackListMiddleware(BaseMiddleware):
8 |
9 | async def trigger(self, action, args):
10 | if action not in ('pre_process_message', 'pre_process_callback_query'):
11 | return
12 |
13 | match args[0]:
14 | case types.Message() as msg:
15 | chat_type = msg.chat.type
16 | case types.CallbackQuery() as call:
17 | chat_type = call.message.chat
18 | case _:
19 | return
20 |
21 | if chat_type != 'private':
22 | return
23 |
24 | from zmanim_bot.repository.bot_repository import get_or_create_user
25 |
26 | user = await get_or_create_user()
27 | if user.meta.is_banned_by_admin:
28 | raise AccessDeniedException()
29 |
30 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/single/zmanim.py:
--------------------------------------------------------------------------------
1 | from zmanim_bot.middlewares.i18n import lazy_gettext as _
2 |
3 | sunrise = _('Sunrise')
4 | alos = _('Alot ha-shakhar')
5 | sof_zman_tefila_gra = _('Sof zman tfila [AGRO]')
6 | sof_zman_tefila_ma = _('Sof zman tfila [MA]')
7 | misheyakir_10_2 = _('Misheyakir (zman tallit and tfilin)')
8 | sof_zman_shema_gra = _('Sof zman Shemah [AGRO]')
9 | sof_zman_shema_ma = _('Sof zman Shemah [MA]')
10 | chatzos = _('Chatzot')
11 | mincha_ketana = _('Mincha ktanah')
12 | mincha_gedola = _('Mincha gdolah')
13 | plag_mincha = _('Plag mincha')
14 | sunset = _('Shkia (sunset)')
15 | tzeis_8_5_degrees = _('Tzeit ha-kochavim [8.5 degrees]')
16 | tzeis_72_minutes = _('Tzeit ha-kochavim [72 minutes]')
17 | tzeis_42_minutes = _('Tzeit ha-kochavim [42 minutes]')
18 | tzeis_5_95_degrees = _('Tzeit ha-kochavim [5.95 degrees]')
19 | chatzot_laila = _('Chatzot Laila (midnight)')
20 | astronomical_hour_ma = _('Astronomical hour [MA]')
21 | astronomical_hour_gra = _('Astronomical hour [AGRO]')
22 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/converter.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import Message
2 | from aiogram_metrics import track
3 |
4 | from zmanim_bot.service import converter_service
5 | from zmanim_bot.states import ConverterGregorianDateState
6 | from zmanim_bot.utils import chat_action
7 |
8 |
9 | @chat_action('text')
10 | # @track('Entry to converter_api')
11 | async def handle_converter_entry(msg: Message):
12 | resp, kb = converter_service.get_converter_entry_menu()
13 | await msg.reply(resp, reply_markup=kb)
14 |
15 |
16 | @chat_action('text')
17 | @track('Converter - gregorian -> jewish')
18 | async def start_greg_to_jew_converter(msg: Message):
19 | await ConverterGregorianDateState().waiting_for_gregorian_date.set()
20 | resp, kb = await converter_service.init_greg_to_jew()
21 | await msg.reply(resp, reply_markup=kb)
22 |
23 |
24 | @chat_action('text')
25 | @track('Converter - jewish -> gregorian')
26 | async def start_jew_to_greg_converter(msg: Message):
27 | resp, kb = await converter_service.init_jew_to_greg()
28 | await msg.reply(resp, reply_markup=kb)
29 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 |
2 | [project]
3 | name = ""
4 | version = ""
5 | description = ""
6 | authors = [
7 | {name = "Benyamin Ginzburg", email = "benyomin.94@gmail.com"},
8 | ]
9 | dependencies = [
10 | "pillow",
11 | "aiogram==2.24",
12 | "zmanim",
13 | "pydantic[dotenv]==1.10.9",
14 | "redis==4.4.1",
15 | "betterlogging",
16 | "odmantic==0.9.2",
17 | "motor",
18 | "sentry-sdk==1.13.0",
19 | "aiogram-metrics==1.0.4",
20 | "setuptools",
21 | "urllib3>=2.2.1",
22 | ]
23 | requires-python = "==3.10.7"
24 | license = {text = "-"}
25 |
26 | [tool.pdm.scripts]
27 | babel-extract = """pybabel extract
28 | #zmanim_bot/texts/plural/units.py
29 | zmanim_bot/texts/single/buttons.py
30 | zmanim_bot/texts/single/headers.py
31 | zmanim_bot/texts/single/helpers.py
32 | zmanim_bot/texts/single/messages.py
33 | zmanim_bot/texts/single/names.py
34 | zmanim_bot/texts/single/zmanim.py
35 | -o locales/zmanim_bot.pot -k __:1,2 --add-comments=NOTE"""
36 | babel-init = "pybabel init -i locales/zmanim_bot.pot -d locales -D zmanim_bot -l "
37 | babel-compile = "pybabel compile -d locales -D zmanim_bot"
38 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/single/headers.py:
--------------------------------------------------------------------------------
1 | from zmanim_bot.middlewares.i18n import lazy_gettext as _
2 |
3 | date = _('Date')
4 |
5 | daf_masehet = _('Masechet')
6 | daf_page = _('Sheet')
7 |
8 | rh_duration = _('Number of days')
9 | rh_month = _('Мonth')
10 | rh_molad = _('Molad')
11 |
12 | parsha = _('Parshat ha-shavuah')
13 | cl = _('Candle Lighting')
14 | havdala = _('Havdala')
15 |
16 | fast_start = _('Fast begins')
17 | fast_end = _('Fast ends (tzeit ha-kochavim)')
18 | fast_chatzot = _('Chatzot')
19 | fast_moved = _('Attention! Fast has been moved!')
20 | fast_not_moved = _('Fast hasn’t been moved.')
21 | fast_end_5_95_dgr = _('5.95 degrees, derabanan')
22 | fast_end_8_5_dgr = _('8.5 degrees, lehumra')
23 | fast_end_42_min = _('42 minutes')
24 |
25 | hoshana_raba = _('Hoshana Rabbah')
26 |
27 | israel_holidays = {
28 | 'yom_hashoah': _('Yom ha-Shoah'),
29 | 'yom_hazikaron': _('Yom ha-Zikaron'),
30 | 'yom_haatzmaut': _('Yom ha-Atzmaut'),
31 | 'yom_yerushalaim': _('Yom Yerushalaim'),
32 | }
33 |
34 | pesach_end_eating_chametz = _('Eating chametz until')
35 | pesach_end_burning_chametz = _('Burning chametz until')
36 |
--------------------------------------------------------------------------------
/zmanim_bot/utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import inspect
3 |
4 | from aiogram.types import ChatActions
5 | from pymongo import IndexModel
6 |
7 | from zmanim_bot.misc import collection
8 | from zmanim_bot.repository.bot_repository import get_or_set_processor_type
9 |
10 |
11 | def chat_action(action: str = None):
12 | def decorator(func):
13 |
14 | @functools.wraps(func)
15 | async def wrapper(*args, **kwargs):
16 | chat_actions = {
17 | 'image': ChatActions.upload_photo,
18 | 'text': ChatActions.typing
19 | }
20 | processor_type = action or await get_or_set_processor_type()
21 | action_func = chat_actions.get(action or processor_type, ChatActions.typing)
22 | await action_func()
23 |
24 | spec = inspect.getfullargspec(func)
25 | kwargs = {k: v for k, v in kwargs.items() if k in spec.args}
26 |
27 | return await func(*args, **kwargs)
28 |
29 | return wrapper
30 | return decorator
31 |
32 |
33 | async def ensure_mongo_index():
34 | index = IndexModel('user_id', unique=True)
35 | await collection.create_indexes([index])
36 |
--------------------------------------------------------------------------------
/zmanim_bot/main.py:
--------------------------------------------------------------------------------
1 | import aiogram_metrics
2 | import sentry_sdk
3 | from aiogram.utils.executor import start_polling, start_webhook
4 | from sentry_sdk.integrations.aiohttp import AioHttpIntegration
5 |
6 | from zmanim_bot.config import config
7 | from zmanim_bot.handlers import register_handlers
8 | from zmanim_bot.middlewares import setup_middlewares
9 | from zmanim_bot.misc import dp, logger, bot
10 | from zmanim_bot.texts.commands import commands
11 | from zmanim_bot.utils import ensure_mongo_index
12 |
13 | sentry_sdk.init(dsn=config.SENTRY_KEY, integrations=[AioHttpIntegration()])
14 |
15 |
16 | async def on_start(_):
17 | setup_middlewares()
18 | register_handlers()
19 |
20 | await bot.set_my_commands(commands)
21 |
22 | await ensure_mongo_index()
23 |
24 | if config.METRICS_DSN:
25 | await aiogram_metrics.register(config.METRICS_DSN, config.METRICS_TABLE_NAME)
26 |
27 | logger.info('Starting zmanim bot...')
28 |
29 |
30 | async def on_close(_):
31 | await aiogram_metrics.close()
32 |
33 |
34 | if __name__ == '__main__':
35 | if config.IS_PROD:
36 | start_webhook(dp, config.WEBHOOK_PATH, on_startup=on_start)
37 | else:
38 | start_polling(dp, on_startup=on_start, skip_updates=True)
39 |
--------------------------------------------------------------------------------
/zmanim_bot/exceptions.py:
--------------------------------------------------------------------------------
1 | class ZmanimBotBaseException(Exception):
2 | ...
3 |
4 |
5 | class NoLanguageException(ZmanimBotBaseException):
6 | ...
7 |
8 |
9 | class NoLocationException(ZmanimBotBaseException):
10 | ...
11 |
12 |
13 | class IncorrectLocationException(ZmanimBotBaseException):
14 | ...
15 |
16 |
17 | class IncorrectTextException(ZmanimBotBaseException):
18 | ...
19 |
20 |
21 | class IncorrectGregorianDateException(ZmanimBotBaseException):
22 | ...
23 |
24 |
25 | class IncorrectJewishDateException(ZmanimBotBaseException):
26 | ...
27 |
28 |
29 | class NonUniqueLocationException(ZmanimBotBaseException):
30 | ...
31 |
32 |
33 | class NonUniqueLocationNameException(ZmanimBotBaseException):
34 | ...
35 |
36 |
37 | class MaxLocationLimitException(ZmanimBotBaseException):
38 | ...
39 |
40 |
41 | class ActiveLocationException(ZmanimBotBaseException):
42 | ...
43 |
44 |
45 | class PolarCoordinatesException(ZmanimBotBaseException):
46 | ...
47 |
48 |
49 | class UnknownProcessorException(ZmanimBotBaseException):
50 | ...
51 |
52 |
53 | class AccessDeniedException(ZmanimBotBaseException):
54 | ...
55 |
56 |
57 | class UnsupportedChatTypeException(ZmanimBotBaseException):
58 | ...
59 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/menus.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import Message
2 | from aiogram_metrics import track
3 |
4 | from zmanim_bot.keyboards import menus
5 | from zmanim_bot.keyboards.inline import DONATE_KB
6 | from zmanim_bot.texts.single import messages
7 | from zmanim_bot.utils import chat_action
8 |
9 |
10 | @chat_action('text')
11 | @track('Holidays menu')
12 | async def handle_holidays_menu(msg: Message):
13 | kb = menus.get_holidays_menu()
14 | await msg.reply(messages.select, reply_markup=kb)
15 |
16 |
17 | @chat_action('text')
18 | # @track('More holidays menu')
19 | async def handle_more_holidays_menu(msg: Message):
20 | kb = menus.get_more_holidays_menu()
21 | await msg.reply(messages.select, reply_markup=kb)
22 |
23 |
24 | @chat_action('text')
25 | # @track('Fasts menu')
26 | async def handle_fasts_menu(msg: Message):
27 | kb = menus.get_fast_menu()
28 | await msg.reply(messages.select, reply_markup=kb)
29 |
30 |
31 | @chat_action('text')
32 | # @track('Settings menu')
33 | async def handle_settings_menu(msg: Message):
34 | kb = menus.get_settings_menu()
35 | await msg.reply(messages.init_settings, reply_markup=kb)
36 |
37 |
38 | @chat_action('text')
39 | @track('Donate button')
40 | async def handle_donate(msg: Message):
41 | await msg.reply(messages.donate_init, reply_markup=DONATE_KB)
42 |
--------------------------------------------------------------------------------
/zmanim_bot/middlewares/i18n.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import Any, Tuple
3 |
4 | from aiogram import types
5 | from aiogram.contrib.middlewares.i18n import I18nMiddleware
6 | from aiogram.types import Message
7 |
8 | from zmanim_bot.config import config
9 |
10 | LOCALES_DIR = Path(__file__).parent.parent.parent / 'locales'
11 |
12 |
13 | class I18N(I18nMiddleware):
14 | async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
15 | if isinstance(args[0], Message) and args[0].chat.type == 'channel':
16 | return ''
17 |
18 | if len(args) > 0 and isinstance(args[0], Message) and args[0].text in config.LANGUAGE_LIST:
19 | return args[0].text
20 |
21 | match args[0]:
22 | case types.Message() as msg if msg.chat.type != 'private':
23 | return ''
24 | case types.CallbackQuery() as call if call.message.chat.type != 'private':
25 | return ''
26 | case _:
27 | from zmanim_bot.repository.bot_repository import get_or_set_lang
28 | return await get_or_set_lang()
29 |
30 | def is_rtl(self) -> bool:
31 | return self.ctx_locale.get() == 'he'
32 |
33 |
34 | i18n_ = I18N(config.I18N_DOMAIN, LOCALES_DIR)
35 | gettext = i18n_.gettext
36 | lazy_gettext = i18n_.lazy_gettext
37 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/utils/redirects.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import User
2 |
3 | from zmanim_bot.keyboards import menus, inline
4 | from zmanim_bot.misc import bot
5 | from zmanim_bot.texts.single import messages
6 |
7 | __all__ = [
8 | 'redirect_to_main_menu',
9 | 'redirect_to_settings_menu',
10 | 'redirect_to_request_location',
11 | 'redirect_to_request_language',
12 | ]
13 |
14 |
15 | async def redirect_to_main_menu(text: str = None):
16 | user_id = User.get_current().id
17 | kb = menus.get_main_menu()
18 | await bot.send_message(user_id, text or messages.init_main_menu, reply_markup=kb)
19 |
20 |
21 | async def redirect_to_settings_menu(text: str = None):
22 | user_id = User.get_current().id
23 | kb = menus.get_settings_menu()
24 | await bot.send_message(user_id, text or messages.init_main_menu, reply_markup=kb)
25 |
26 |
27 | async def redirect_to_request_location():
28 | user_id = User.get_current().id
29 | kb = inline.LOCATION_SEARCH_KB
30 | await bot.send_message(user_id, messages.request_location_on_init, reply_markup=kb)
31 |
32 |
33 | async def redirect_to_request_language():
34 | user_id = User.get_current().id
35 | kb = menus.get_lang_menu()
36 | resp = messages.request_language.value
37 | if resp == 'request language':
38 | resp = 'Select your language:'
39 | await bot.send_message(user_id, resp, reply_markup=kb)
40 |
--------------------------------------------------------------------------------
/zmanim_bot/helpers.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 | from typing import Tuple
3 |
4 | from zmanim_bot.exceptions import IncorrectGregorianDateException, IncorrectLocationException
5 |
6 | LOCATION_PATTERN = r'^-?\d{1,2}\.{1}\d+, {0,1}-?\d{1,3}\.{1}\d+$'
7 | LANGUAGE_SHORTCUTS = {
8 | 'English': 'en',
9 | 'Русский': 'ru'
10 | }
11 | CALL_ANSWER_OK = '✅'
12 |
13 | CL_OFFET_OPTIONS = [10, 15, 18, 20, 22, 30, 40]
14 | HAVDALA_OPINION_OPTIONS = [
15 | 'tzeis_5_95_degrees',
16 | 'tzeis_8_5_degrees',
17 | 'tzeis_42_minutes',
18 | 'tzeis_72_minutes',
19 | ]
20 |
21 |
22 | class CallbackPrefixes:
23 | cl = 'cl:'
24 | zmanim = 'zmanim_api:'
25 | havdala = 'havdala:'
26 | report = 'report:'
27 | zmanim_by_date = 'zbd:'
28 | omer = 'omer:'
29 | format = 'format:'
30 | location_activate = 'loc_a:'
31 | location_rename = 'loc_r:'
32 | location_delete = 'loc_d:'
33 | location_namage = 'loc_m:'
34 | location_add = 'loc_add:'
35 | location_menu_back = 'loc_back'
36 | donate = 'donate:'
37 |
38 | update_zmanim = 'uz:'
39 | update_shabbat = 'us:'
40 | update_fast = 'uf:'
41 | update_yom_tov = 'uy:'
42 |
43 |
44 | def parse_coordinates(coordinates: str) -> Tuple[float, float]:
45 | try:
46 | lat, lng = map(float, coordinates.split(','))
47 | except ValueError:
48 | raise IncorrectLocationException('Incorrect location format!')
49 |
50 | if lng > 180.0 or lng < -180.0 or lat > 90.0 or lat < -90.0:
51 | raise IncorrectLocationException('Incorrect location value!')
52 |
53 | return lat, lng
54 |
55 |
56 | def check_date(date_: str):
57 | try:
58 | date.fromisoformat(date_)
59 | except ValueError:
60 | raise IncorrectGregorianDateException
61 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/festivals.py:
--------------------------------------------------------------------------------
1 | from aiogram.dispatcher import FSMContext
2 | from aiogram.types import Message, CallbackQuery
3 | from aiogram_metrics import track
4 |
5 | from zmanim_bot.helpers import CallbackPrefixes
6 | from zmanim_bot.service import festivals_service
7 | from zmanim_bot.utils import chat_action
8 |
9 |
10 | @chat_action()
11 | async def handle_fast(msg: Message, state: FSMContext):
12 | await state.update_data({'current_fast': msg.text})
13 | await festivals_service.get_generic_fast(msg.text)
14 |
15 |
16 | async def handle_fast_update(call: CallbackQuery, state: FSMContext):
17 | coordinates = call.data.split(CallbackPrefixes.update_fast)[1]
18 | lat, lng = map(float, coordinates.split(','))
19 | await state.update_data({'current_location': [lat, lng]})
20 |
21 | current_fast_name = (await state.get_data()).get('current_fast')
22 | if not current_fast_name:
23 | return await call.answer('Current fast is not set')
24 |
25 | await festivals_service.update_generic_fast(current_fast_name, lat, lng)
26 | await call.answer()
27 |
28 |
29 | @chat_action()
30 | async def handle_yom_tov(msg: Message, state: FSMContext):
31 | await state.update_data({'current_yom_tov': msg.text})
32 | await festivals_service.get_generic_yomtov(msg.text)
33 |
34 |
35 | async def handle_yom_tov_update(call: CallbackQuery, state: FSMContext):
36 | coordinates = call.data.split(CallbackPrefixes.update_yom_tov)[1]
37 | lat, lng = map(float, coordinates.split(','))
38 | await state.update_data({'current_location': [lat, lng]})
39 |
40 | current_yom_tov_name = (await state.get_data()).get('current_yom_tov')
41 | if not current_yom_tov_name:
42 | return await call.answer('Current yom tov is not set')
43 |
44 | await festivals_service.update_generic_yom_tov(current_yom_tov_name, lat, lng)
45 | await call.answer()
46 |
47 |
48 | @chat_action()
49 | @track('Holiday')
50 | async def handle_holiday(msg: Message):
51 | await festivals_service.get_generic_holiday(msg.text)
52 |
--------------------------------------------------------------------------------
/zmanim_bot/integrations/geo_client.py:
--------------------------------------------------------------------------------
1 | from hashlib import md5
2 | from typing import List
3 |
4 | from aiogram.types import InlineQueryResultLocation
5 |
6 | from zmanim_bot.config import config
7 | from zmanim_bot.misc import bot
8 |
9 |
10 | async def get_location_name(lat: float, lng: float, locality: str, *, no_trim: bool = False) -> str:
11 | params = {
12 | 'latitude': lat,
13 | 'longitude': lng,
14 | 'localityLanguage': locality
15 | }
16 | async with (await bot.get_session()).get(config.GEO_API_URL, params=params) as resp:
17 | raw_resp: dict = await resp.json()
18 | city = raw_resp.get('city')
19 | locality = raw_resp.get('locality')
20 |
21 | if city and locality and city != locality:
22 | name = f'{city}, {locality}'
23 | if not no_trim:
24 | name = name[:30]
25 |
26 | elif city or locality:
27 | name = (city or locality)
28 | if not no_trim:
29 | name = name[:30]
30 |
31 | else:
32 | name = f'{lat:.3f}, {lng:.3f}'
33 | return name
34 |
35 |
36 | async def find_places_by_query(query: str, language: str) -> List[InlineQueryResultLocation]:
37 | url = f'https://api.mapbox.com/geocoding/v5/mapbox.places/{query}.json?'
38 | params = {
39 | 'access_token': config.MAPBOX_API_KEY,
40 | 'language': language,
41 | 'types': 'place,locality,neighborhood,address' # ,poi
42 | }
43 | results = []
44 |
45 | async with (await bot.get_session()).get(url, params=params) as resp:
46 | raw_resp: dict = await resp.json()
47 |
48 | for feature in raw_resp.get('features', []):
49 | lng, lat = feature.get('center', (None, None))
50 | title: str = feature.get('place_name')
51 |
52 | if not all((lat, lng, title)):
53 | continue
54 |
55 | place_id = md5(title.encode()).hexdigest()
56 | results.append(InlineQueryResultLocation(id=place_id, latitude=lat, longitude=lng, title=title))
57 |
58 | return results
59 |
--------------------------------------------------------------------------------
/zmanim_bot/config.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseSettings, Field, validator
2 |
3 |
4 | class Config(BaseSettings):
5 |
6 | class Config:
7 | env_file = '.env'
8 | env_file_encoding = 'utf-8'
9 |
10 | I18N_DOMAIN: str = Field('zmanim_bot')
11 | BOT_TOKEN: str = Field(env='BOT_TOKEN')
12 |
13 | IS_PROD: bool = Field(False, env='IS_PROD')
14 | WEBHOOK_PATH: str = Field('/zmanim_bot', env='WEBHOOK_PATH')
15 |
16 | LANGUAGE_LIST: list[str] = Field(['English', 'Русский', 'עברית'])
17 | LANGUAGE_SHORTCUTS: dict[str, str] = Field({
18 | 'English': 'en',
19 | 'Русский': 'ru',
20 | 'עברית': 'he'
21 | })
22 |
23 | DB_URL: str = Field('localhost', env='DB_URL')
24 | DB_NAME: str = Field(env='DB_NAME')
25 | DB_COLLECTION_NAME: str = Field(env='DB_COLLECTION_NAME')
26 |
27 | REDIS_HOST: str = Field(env='REDIS_HOST')
28 | REDIS_PORT: int = Field(env='REDIS_PORT')
29 | REDIS_DB: int = Field(env='REDIS_DB')
30 |
31 | ZMANIM_API_URL: str = Field(env='ZMANIM_API_URL')
32 | GEO_API_URL: str = Field(env='GEO_API_URL')
33 | MAPBOX_API_KEY: str = Field(env='MAPBOX_API_KEY')
34 |
35 | REPORT_ADMIN_LIST: list[int] = Field(env='REPORT_ADMIN_LIST')
36 |
37 | LOCATION_NUMBER_LIMIT: int = Field(5, env='LOCATION_NUMBER_LIMIT')
38 | SENTRY_KEY: str | None = Field(env='SENTRY_PUBLIC_KEY')
39 |
40 | METRICS_DSN: str | None = Field(env='METRICS_DSN')
41 | METRICS_TABLE_NAME: str | None = Field(env='METRICS_TABLE_NAME')
42 |
43 | PAYMENTS_PROVIDER_TOKEN: str = Field(env='PAYMENTS_PROVIDER_TOKEN')
44 | DONATE_OPTIONS: list[int] = Field([2, 5, 10, 25, 50])
45 |
46 | OPEN_TOPO_DATA_DB: str = Field(..., env='OPEN_TOPO_DATA_DB')
47 |
48 | @validator('REPORT_ADMIN_LIST', pre=True)
49 | def parse_list(cls, report_admin_list):
50 | if isinstance(report_admin_list, int):
51 | return [report_admin_list]
52 | if isinstance(report_admin_list, str):
53 | return [int(i.strip()) for i in report_admin_list.split(',')]
54 | return report_admin_list
55 |
56 |
57 | config = Config()
58 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/admin.py:
--------------------------------------------------------------------------------
1 | from asyncio.tasks import create_task
2 |
3 | from aiogram.dispatcher import FSMContext
4 | from aiogram.types import CallbackQuery, ContentType, Message
5 |
6 | from zmanim_bot.admin.report_management import send_response_to_user
7 | from zmanim_bot.admin.states import AdminReportResponse
8 | from zmanim_bot.handlers.utils.redirects import redirect_to_main_menu
9 | from zmanim_bot.keyboards.menus import get_cancel_keyboard, get_report_keyboard
10 | from zmanim_bot.misc import bot
11 | from zmanim_bot.texts.single import messages
12 |
13 |
14 | async def handle_report(call: CallbackQuery, state: FSMContext):
15 | _, msg_id, user_id = call.data.split(':')
16 | report_data = {
17 | 'message_id': msg_id,
18 | 'user_id': user_id,
19 | 'media_ids': []
20 | }
21 |
22 | await AdminReportResponse().waiting_for_response_text.set()
23 | await state.set_data(report_data)
24 |
25 | resp = 'Write your response:'
26 | kb = get_cancel_keyboard()
27 |
28 | await bot.send_message(call.from_user.id, resp, reply_markup=kb)
29 | await call.answer()
30 |
31 |
32 | async def handle_report_response(msg: Message, state: FSMContext):
33 | response_data = await state.get_data()
34 | response_data['response'] = msg.text
35 | await state.set_data(response_data)
36 | await AdminReportResponse.next()
37 |
38 | resp = 'Attach media (scrinshots or video)'
39 | kb = get_report_keyboard()
40 | await msg.reply(resp, reply_markup=kb)
41 |
42 |
43 | async def handle_done_report(_, state: FSMContext):
44 | response = await state.get_data()
45 | await state.finish()
46 | await redirect_to_main_menu('Succesfully sent')
47 | create_task(send_response_to_user(response))
48 |
49 |
50 | async def handle_report_payload(msg: Message, state: FSMContext):
51 | if msg.content_type not in (ContentType.PHOTO, ContentType.VIDEO):
52 | return await msg.reply(messages.reports_incorrect_media_type)
53 |
54 | response = await state.get_data()
55 |
56 | if msg.photo:
57 | response['media_ids'].append((msg.photo[-1].file_id, 'photo'))
58 | if msg.video:
59 | response['media_ids'].append((msg.video.file_id, 'video'))
60 |
61 | await state.set_data(response)
62 | await msg.reply(messages.reports_media_received)
63 |
64 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/single/buttons.py:
--------------------------------------------------------------------------------
1 | from zmanim_bot.middlewares.i18n import lazy_gettext as _
2 |
3 | # Service
4 | cancel = _('Cancel')
5 | back = _('Back')
6 | done = _('Done')
7 |
8 | # location
9 | geobutton = _('Send location')
10 | search_location = _('Find location by name')
11 | manage_locations = _('Manage saved locations')
12 | add_location = _('Add new location')
13 |
14 |
15 | # Main menu
16 | mm_zmanim = _('Zmanim')
17 | mm_shabbat = _('Shabbat')
18 | mm_holidays = _('Holidays')
19 | mm_daf_yomi = _('Daf yomi')
20 | mm_rh = _('Rosh chodesh')
21 | mm_fasts = _('Fast days')
22 | mm_zmanim_by_date = _('Zmanim by the date')
23 | mm_converter = _('Date converter')
24 | mm_help = _('Help')
25 | mm_settings = _('Settings')
26 | mm_donate = _('Donate')
27 |
28 |
29 | # Help menu
30 | hm_faq = _('F.A.Q.')
31 | hm_report = _('Report a problem')
32 |
33 |
34 | # Settings menu
35 | sm_zmanim = _('Select zmanim')
36 | sm_candle = _('Candle lighting')
37 | sm_havdala = _('Havdala')
38 | sm_lang = _('Language')
39 | sm_location = _('Location')
40 | sm_omer = _('Omer count')
41 | sm_format = _('Format')
42 | sm_format_text_option = _('Text')
43 | sm_format_image_option = _('Picture')
44 |
45 | processor_types = {
46 | 'image': sm_format_image_option,
47 | 'text': sm_format_text_option,
48 | }
49 |
50 | settings_enabled = _('enabled')
51 | settings_disabled = _('disabled')
52 |
53 | # Holidays
54 | hom_more = _('More...')
55 | hom_main = _('Main holidays')
56 |
57 | hom_rosh_hashana = _('Rosh ha-Shanah')
58 | hom_yom_kippur = _('Yom Kippur')
59 | hom_succot = _('Succot')
60 | hom_shmini_atzeret = _('Shmini Atzeres')
61 | hom_chanukah = _('Chanukah')
62 | hom_purim = _('Purim')
63 | hom_pesach = _('Pesach')
64 | hom_shavuot = _('Shavuot')
65 | hom_tu_bishvat = _('Tu bi-Shvat')
66 | hom_lag_baomer = _('Lag ba-Omer')
67 | hom_israel = _('Israel holidays')
68 |
69 |
70 | # Fasts
71 | fm_gedaliah = _('Fast of Gedaliah')
72 | fm_tevet = _('10th of Tevet')
73 | fm_esther = _('Fast of Ester')
74 | fm_tammuz = _('17th of Tammuz')
75 | fm_av = _('9th of Av')
76 |
77 | YOMTOVS = [hom_rosh_hashana, hom_yom_kippur, hom_succot, hom_shmini_atzeret, hom_pesach, hom_shavuot]
78 | HOLIDAYS = [hom_chanukah, hom_purim, hom_tu_bishvat, hom_lag_baomer, hom_israel]
79 | FASTS = [fm_gedaliah, fm_tevet, fm_esther, fm_tammuz, fm_av]
80 |
81 |
82 | # converter
83 | conv_greg_to_jew = _('Gregorian ➡ Jewish')
84 | conv_jew_to_greg = _('Jewish ➡ Gregorian')
85 |
86 | # NOTE prefix for button "Zmanim for {date}"
87 | zmanim_for_date_prefix = _('Zmanim for')
88 |
--------------------------------------------------------------------------------
/zmanim_bot/service/converter_service.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 | from typing import Tuple
3 |
4 | from aiogram.types import InlineKeyboardMarkup, ReplyKeyboardMarkup
5 | from zmanim.hebrew_calendar.jewish_calendar import JewishCalendar
6 |
7 | from zmanim_bot import keyboards
8 | from zmanim_bot.exceptions import (IncorrectGregorianDateException, IncorrectJewishDateException)
9 | from zmanim_bot.keyboards.inline import get_zmanim_by_date_buttons
10 | from zmanim_bot.states import (ConverterGregorianDateState, ConverterJewishDateState)
11 | from zmanim_bot.texts.single import messages
12 | from zmanim_bot.texts.single.names import (JEWISH_MONTHS_GENITIVE, MONTH_NAMES_GENETIVE, WEEKDAYS)
13 |
14 |
15 | def get_converter_entry_menu() -> Tuple[str, ReplyKeyboardMarkup]:
16 | kb = keyboards.menus.get_converter_menu()
17 | return messages.init_converter, kb
18 |
19 |
20 | async def init_greg_to_jew() -> Tuple[str, ReplyKeyboardMarkup]:
21 | await ConverterGregorianDateState().waiting_for_gregorian_date.set()
22 | kb = keyboards.menus.get_cancel_keyboard()
23 | return messages.greg_date_request, kb
24 |
25 |
26 | async def init_jew_to_greg() -> Tuple[str, ReplyKeyboardMarkup]:
27 | await ConverterJewishDateState().waiting_for_jewish_date.set()
28 | kb = keyboards.menus.get_cancel_keyboard()
29 | return messages.jew_date_request, kb
30 |
31 |
32 | def convert_heb_to_greg(date_: str) -> Tuple[str, InlineKeyboardMarkup]:
33 | try:
34 | year, month, day = map(int, date_.split('-'))
35 | except ValueError:
36 | raise IncorrectJewishDateException
37 |
38 | try:
39 | calendar = JewishCalendar(year, month, day)
40 | except ValueError:
41 | raise IncorrectJewishDateException
42 | gr_date = calendar.gregorian_date
43 | resp = f'{gr_date.day} {MONTH_NAMES_GENETIVE[gr_date.month]} {gr_date.year}, {WEEKDAYS[gr_date.weekday()]}'
44 |
45 | kb = get_zmanim_by_date_buttons([gr_date])
46 | return resp, kb
47 |
48 |
49 | def convert_greg_to_heb(date_: str) -> Tuple[str, InlineKeyboardMarkup]:
50 | try:
51 | pydate = date.fromisoformat(date_)
52 | except ValueError:
53 | raise IncorrectGregorianDateException
54 |
55 | calendar = JewishCalendar.from_date(pydate)
56 | jewish_date = f'{calendar.jewish_day} {JEWISH_MONTHS_GENITIVE[calendar.jewish_month_name()]} {calendar.jewish_year},' \
57 | f' {WEEKDAYS[calendar.gregorian_date.weekday()]}'
58 | kb = get_zmanim_by_date_buttons([pydate])
59 | return jewish_date, kb
60 |
61 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/main.py:
--------------------------------------------------------------------------------
1 | from aiogram.dispatcher import FSMContext
2 | from aiogram.types import CallbackQuery, Message, ChatActions
3 | from aiogram_metrics import track
4 |
5 | from zmanim_bot.helpers import CallbackPrefixes
6 | from zmanim_bot.keyboards.menus import get_cancel_keyboard
7 | from zmanim_bot.repository.bot_repository import get_or_set_processor_type
8 | from zmanim_bot.service import zmanim_service
9 | from zmanim_bot.texts.single import messages
10 | from zmanim_bot.utils import chat_action
11 |
12 |
13 | @track('Zmanim')
14 | async def handle_zmanim(_, state: FSMContext):
15 | chat_actions = { # TODO refactor `@chat_action` to work with `@track`
16 | 'image': ChatActions.upload_photo,
17 | 'text': ChatActions.typing
18 | }
19 | processor_type = await get_or_set_processor_type()
20 | await chat_actions.get(processor_type, 'text')()
21 |
22 | await zmanim_service.send_zmanim(state=state)
23 |
24 |
25 | @track('Zmanim geo-variant')
26 | async def handle_update_zmanim(call: CallbackQuery):
27 | await call.answer()
28 | coordinates = call.data.split(CallbackPrefixes.update_zmanim)[1]
29 |
30 | if ':' in coordinates:
31 | coordinates, date = coordinates.split(':')
32 | else:
33 | date = None
34 |
35 | lat, lng = map(float, coordinates.split(','))
36 | await zmanim_service.update_zmanim(lat, lng, date)
37 |
38 |
39 | @chat_action()
40 | async def handle_zmanim_by_date_callback(call: CallbackQuery, state: FSMContext):
41 | await zmanim_service.send_zmanim(call=call, state=state)
42 | await call.answer()
43 |
44 |
45 | @chat_action()
46 | @track('Zmanim by date')
47 | async def handle_zmanim_by_date(msg: Message):
48 | await zmanim_service.init_zmanim_by_date()
49 | await msg.reply(messages.greg_date_request, reply_markup=get_cancel_keyboard())
50 |
51 |
52 | @chat_action()
53 | @track('Shabbat')
54 | async def handle_shabbat(_):
55 | await zmanim_service.get_shabbat()
56 |
57 |
58 | @track('Shabbat geo-variant')
59 | async def handle_update_shabbat(call: CallbackQuery, state: FSMContext):
60 | coordinates = call.data.split(CallbackPrefixes.update_shabbat)[1]
61 | lat, lng = map(float, coordinates.split(','))
62 | await zmanim_service.update_shabbat(lat, lng, state)
63 | await call.answer()
64 |
65 |
66 | @chat_action()
67 | @track('Daf yomi')
68 | async def handle_daf_yomi(_):
69 | await zmanim_service.get_daf_yomi()
70 |
71 |
72 | @chat_action()
73 | @track('Rosh chodesh')
74 | async def handle_rosh_chodesh(_):
75 | await zmanim_service.get_rosh_chodesh()
76 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/text_utils.py:
--------------------------------------------------------------------------------
1 | from datetime import date, time, datetime as dt
2 | from typing import List, Union
3 |
4 | from zmanim.hebrew_calendar.jewish_date import JewishDate
5 |
6 | from zmanim_bot.integrations.zmanim_models import AsurBeMelachaDay
7 | from zmanim_bot.texts.single import names
8 | from zmanim_bot.texts.single.names import JEWISH_MONTHS_GENITIVE
9 |
10 |
11 | def humanize_date(date_range: List[Union[date, AsurBeMelachaDay]],
12 | weekday_on_new_line: bool = False) -> str:
13 | """
14 | Use this function for humanize date or date range
15 | Examples:
16 | 2020-01-01 -> 1 January 2020, Wednesday
17 | 2020-01-01, 2020-01-08 -> 1—7 January 2020, Wednesday-Tuesday
18 | 2020-01-31, 2020-02-1 -> 31 January—1 February 2020, Friday-Saturday
19 | 2019-12-25, 2020-01-03 -> 25 December 2019—3 January 2020, Wednesday-Friday
20 | """
21 | weekday_sep = '\n' if weekday_on_new_line else ' '
22 | months = names.MONTH_NAMES_GENETIVE
23 | weekdays = names.WEEKDAYS
24 |
25 | d1 = date_range[0]
26 | d2 = date_range[1] if (len(date_range) > 1) and (date_range[0] != date_range[1]) else None
27 |
28 | if isinstance(d1, AsurBeMelachaDay):
29 | d1 = d1.date
30 | if isinstance(d2, AsurBeMelachaDay):
31 | d2 = d2.date
32 |
33 | if not d2:
34 | d = d1
35 | resp = f'{d.day} {months[d.month]} {d.year},{weekday_sep}{weekdays[d.weekday()]}'
36 |
37 | elif d1.year != d2.year:
38 | resp = f'{d1.day} {months[d1.month]} {d1.year} — ' \
39 | f'{d2.day} {months[d2.month]} {d2.year}, ' \
40 | f'{weekdays[d1.weekday()]}-{weekdays[d2.weekday()]}'
41 |
42 | elif d1.month != d2.month:
43 | resp = f'{d1.day} {months[d1.month]} — {d2.day} {months[d2.month]} {d1.year},{weekday_sep}' \
44 | f'{weekdays[d1.weekday()]}-{weekdays[d2.weekday()]}'
45 |
46 | else:
47 | resp = f'{d1.day}-{d2.day} {months[d1.month]} {d1.year},{weekday_sep}' \
48 | f'{weekdays[d1.weekday()]}-{weekdays[d2.weekday()]}'
49 |
50 | return resp
51 |
52 |
53 | def humanize_time(time_or_dt: Union[time, dt]) -> str:
54 | """ Use this function for convert time to hh:mm """
55 | if isinstance(time_or_dt, time):
56 | return time_or_dt.isoformat(timespec='minutes')
57 | if isinstance(time_or_dt, dt):
58 | return time_or_dt.time().isoformat(timespec='minutes')
59 |
60 |
61 | def parse_jewish_date(date_str: str) -> str:
62 | year, month, day = date_str.split('-')
63 | return f'{day} {JEWISH_MONTHS_GENITIVE.get(list(JewishDate.MONTHS)[int(month) - 1].name)} {year}'
64 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/text/text_processor.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 |
3 | from aiogram import Bot
4 | from aiogram.types import InlineKeyboardMarkup, User, CallbackQuery, Message
5 |
6 | from zmanim_bot.integrations.zmanim_models import DafYomi, RoshChodesh, IsraelHolidays, Holiday, \
7 | Fast, YomTov, Shabbat, Zmanim
8 | from zmanim_bot.keyboards import inline
9 | from zmanim_bot.processors import BaseProcessor
10 | from zmanim_bot.processors.text import composer
11 |
12 |
13 | class TextProcessor(BaseProcessor[str]):
14 |
15 | _type = 'text'
16 |
17 | async def _send(self, text: str, *kb_list: InlineKeyboardMarkup):
18 | bot = Bot.get_current()
19 | user = User.get_current()
20 | call = CallbackQuery.get_current()
21 | message = Message.get_current()
22 | kb_list = [kb for kb in kb_list if kb] # clean from None
23 |
24 | if call:
25 | reply_to = call.message.message_id
26 | elif message:
27 | reply_to = message and message.message_id
28 | else:
29 | reply_to = None
30 |
31 | if len(kb_list) == 0:
32 | kb = None
33 | elif len(kb_list) == 1:
34 | kb = kb_list[0]
35 | else:
36 | kb = inline.merge_inline_keyboards(kb_list[0], kb_list[1])
37 |
38 | await bot.send_message(user.id, text, reply_markup=kb, reply_to_message_id=reply_to)
39 |
40 | async def _update(self, text: str, *kb_list: InlineKeyboardMarkup):
41 | bot = Bot.get_current()
42 | user = User.get_current()
43 | call = CallbackQuery.get_current()
44 | kb_list = [kb for kb in kb_list if kb] # clean from None
45 |
46 | if len(kb_list) == 0:
47 | kb = None
48 | elif len(kb_list) == 1:
49 | kb = kb_list[0]
50 | else:
51 | kb = inline.merge_inline_keyboards(kb_list[0], kb_list[1])
52 |
53 | await bot.edit_message_text(text, user.id, call.message.message_id, reply_markup=kb)
54 |
55 | def _get_zmanim(self, data: Zmanim) -> str:
56 | return composer.compose_zmanim(data, self._location_name)
57 |
58 | def _get_shabbat(self, data: Shabbat) -> Tuple[str, InlineKeyboardMarkup]:
59 | return composer.compose_shabbat(data, self._location_name)
60 |
61 | def _get_yom_tov(self, data: YomTov) -> Tuple[str, InlineKeyboardMarkup]:
62 | return composer.compose_yom_tov(data, self._location_name)
63 |
64 | def _get_fast(self, data: Fast) -> Tuple[str, InlineKeyboardMarkup]:
65 | return composer.compose_fast(data, self._location_name)
66 |
67 | def _get_holiday(self, data: Holiday) -> str:
68 | return composer.compose_holiday(data)
69 |
70 | def _get_israel_holidays(self, data: IsraelHolidays) -> str:
71 | return composer.compose_israel_holidays(data)
72 |
73 | def _get_rosh_chodesh(self, data: RoshChodesh) -> str:
74 | return composer.compose_rosh_chodesh(data)
75 |
76 | def _get_daf_yomi(self, data: DafYomi) -> str:
77 | return composer.compose_daf_yomi(data)
78 |
--------------------------------------------------------------------------------
/zmanim_bot/repository/bot_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Any, List, Optional, Tuple
2 |
3 | from aiogram.types import User
4 |
5 | from ._storage import *
6 | from .models import Location
7 | from .models import User as BotUser
8 |
9 | __all__ = [
10 | 'Location',
11 | 'get_or_create_user',
12 | 'get_or_set_zmanim',
13 | 'get_or_set_cl',
14 | 'get_or_set_lang',
15 | 'get_or_set_havdala',
16 | 'get_or_set_location',
17 | 'set_location_name',
18 | 'activate_location',
19 | 'delete_location',
20 | 'get_or_set_processor_type',
21 | 'get_or_set_omer_flag',
22 | ]
23 |
24 |
25 | async def get_or_create_user() -> BotUser:
26 | user = User.get_current()
27 | return await _get_or_create_user(user)
28 |
29 |
30 | async def get_or_set_lang(lang: str = None) -> Optional[str]:
31 | user = User.get_current()
32 | return await get_lang(user) if not lang else await set_lang(user, lang)
33 |
34 |
35 | async def get_or_set_location(location: Tuple[float, float] = None) -> Location:
36 | user = User.get_current()
37 | return await get_location(user) if not location else await set_location(user, location)
38 |
39 |
40 | async def set_location_name(new_name: str, old_name: str) -> List[Location]:
41 | user = User.get_current()
42 | return await do_set_location_name(user, new_name=new_name, old_name=old_name)
43 |
44 |
45 | async def activate_location(name: str) -> List[Location]:
46 | user = User.get_current()
47 | return await do_activate_location(user, name)
48 |
49 |
50 | async def delete_location(name: str) -> List[Location]:
51 | user = User.get_current()
52 | return await do_delete_location(user, name)
53 |
54 |
55 | async def get_or_set_cl(cl: int = None) -> Optional[int]:
56 | user = User.get_current()
57 | return await get_cl_offset(user) if not cl else await set_cl(user, cl)
58 |
59 |
60 | async def get_or_set_zmanim(zmanim: dict = None) -> Optional[dict]:
61 | user = User.get_current()
62 | return await get_zmanim(user) if not zmanim else await set_zmanim(user, zmanim)
63 |
64 |
65 | async def get_or_set_havdala(havdala: str = None) -> Optional[str]:
66 | user = User.get_current()
67 | return await get_havdala(user) if not havdala else await set_havdala(user, havdala)
68 |
69 |
70 | async def get_or_set_processor_type(processor_type: Optional[str] = None) -> Optional[str]:
71 | user = User.get_current()
72 | return await get_processor_type(user) if not processor_type \
73 | else await set_processor_type(user, processor_type)
74 |
75 |
76 | async def get_or_set_omer_flag(
77 | omer_flag: Optional[bool] = None,
78 | zmanim: Optional[Any] = None # todo circullar import problem, refactor needed
79 | ) -> Optional[bool]:
80 | user = User.get_current()
81 | if omer_flag is not None:
82 | omer_time = None
83 | if omer_flag:
84 | omer_time = zmanim and zmanim.tzeis_8_5_degrees.isoformat()
85 |
86 | return await set_omer_flag(user, omer_flag, omer_time)
87 | return await get_omer_flag(user)
88 |
--------------------------------------------------------------------------------
/broadcast/broadcast.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from typing import Tuple
4 |
5 | from aiogram import Bot, Dispatcher, types
6 | from aiogram.types import InlineKeyboardMarkup
7 | from aiogram.utils import exceptions, executor
8 | from pymongo import MongoClient
9 |
10 | import data
11 |
12 | logging.basicConfig(level=logging.INFO)
13 | log = logging.getLogger('broadcast')
14 |
15 | bot = Bot(token=config.BOT_TOKEN, parse_mode=types.ParseMode.HTML)
16 | dp = Dispatcher(bot)
17 |
18 | texts = {
19 | 'ru': data.ru_message,
20 | 'en': data.en_message
21 | }
22 | button_texts = {
23 | 'ru': data.ru_button_text,
24 | 'en': data.en_button_text
25 | }
26 |
27 |
28 | def get_users() -> Tuple[int, str]:
29 | client = MongoClient(config.DB_URL)
30 | collection = client[config.DB_NAME][config.DB_COLLECTION_NAME]
31 | docs = list(collection.find())
32 | users = map(lambda doc: (doc['user_id'], doc['language']), docs)
33 | yield from users
34 |
35 |
36 | async def send_message(
37 | user_id: int,
38 | text: str,
39 | kb: InlineKeyboardMarkup = None,
40 | disable_notification: bool = False
41 | ) -> bool:
42 | try:
43 | await bot.send_message(user_id, text, disable_notification=disable_notification, reply_markup=None)
44 | except exceptions.BotBlocked:
45 | log.error(f"Target [ID:{user_id}]: blocked by user")
46 | except exceptions.ChatNotFound:
47 | log.error(f"Target [ID:{user_id}]: invalid user ID")
48 | except exceptions.RetryAfter as e:
49 | log.error(f"Target [ID:{user_id}]: Flood limit is exceeded. Sleep {e.timeout} seconds.")
50 | await asyncio.sleep(e.timeout)
51 | return await send_message(user_id, text) # Recursive call
52 | except exceptions.UserDeactivated:
53 | log.error(f"Target [ID:{user_id}]: user is deactivated")
54 | except exceptions.TelegramAPIError:
55 | log.exception(f"Target [ID:{user_id}]: failed")
56 | else:
57 | log.info(f"Target [ID:{user_id}]: success")
58 | return True
59 | return False
60 |
61 |
62 | async def broadcaster() -> int:
63 | """
64 | Simple broadcaster
65 | :return: Count of messages
66 | """
67 | count = 0
68 | try:
69 | for user_id, lang in get_users():
70 | text = texts.get(lang)
71 | if text is None:
72 | log.error(f'There is no known language for user {user_id}')
73 | continue
74 |
75 | # kb = InlineKeyboardMarkup()
76 | # kb.row(InlineKeyboardButton(text=button_donate_texts[lang], url=data.donate_link))
77 | # kb.row(InlineKeyboardButton(text=button_channel_texts[lang], url=data.channel_link))
78 | if await send_message(user_id, text, kb=...):
79 | count += 1
80 | await asyncio.sleep(.05) # 20 messages per second (Limit: 30 messages per second)
81 | finally:
82 | log.info(f"{count} messages successful sent.")
83 |
84 | return count
85 |
86 |
87 | if __name__ == '__main__':
88 | # Execute broadcaster
89 | executor.start(dp, broadcaster())
90 |
--------------------------------------------------------------------------------
/zmanim_bot/admin/report_management.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Union
3 |
4 | from aiogram import types
5 |
6 | from zmanim_bot.repository.bot_repository import get_or_set_location
7 | from ..config import config
8 | from ..exceptions import NoLocationException
9 | from ..helpers import CallbackPrefixes
10 | from ..misc import bot
11 |
12 |
13 | async def _compose_report_text(report: dict) -> str:
14 | try:
15 | location = await get_or_set_location()
16 | except NoLocationException:
17 | location = None
18 |
19 | report_data = {
20 | 'text': report['message'],
21 | 'meta': {
22 | 'user_id': types.User.get_current().id,
23 | 'user_location': location
24 | }
25 | }
26 | report_text = json.dumps(report_data, indent=2, ensure_ascii=False)
27 | text = f'NEW REPORT!\n\n{report_text}'
28 | return text
29 |
30 |
31 | async def _compose_media(report: dict) -> Union[types.MediaGroup, types.InputMediaPhoto]:
32 | media_ids = report['media_ids']
33 |
34 | media_photos = []
35 | for file_id, file_type in media_ids:
36 | media_types = {
37 | 'photo': types.InputMediaPhoto,
38 | 'video': types.InputMediaVideo
39 | }
40 | file = await bot.get_file(file_id)
41 | url = bot.get_file_url(file.file_path)
42 | media_photos.append(media_types[file_type](types.InputFile.from_url(url)))
43 |
44 | if len(media_photos) == 1:
45 | return media_photos[0]
46 | return types.MediaGroup(media_photos)
47 |
48 |
49 | async def send_report_to_admins(report: dict):
50 | button_data = f'{CallbackPrefixes.report}{report["message_id"]}:{report["user_id"]}'
51 | button = types.InlineKeyboardMarkup().row(
52 | types.InlineKeyboardButton('Reply to report', callback_data=button_data)
53 | )
54 | report_text = await _compose_report_text(report)
55 | report_media = await _compose_media(report) if report['media_ids'] else None
56 |
57 | for admin_id in config.REPORT_ADMIN_LIST:
58 | if report_media:
59 | if isinstance(report_media, types.MediaGroup):
60 | msg = await bot.send_media_group(admin_id, report_media)
61 | else:
62 | msg = await bot.send_photo(admin_id, report_media.file)
63 |
64 | resp_id = msg[0].message_id if isinstance(msg, list) else msg.message_id
65 | await bot.send_message(admin_id, report_text, reply_markup=button, reply_to_message_id=resp_id)
66 | else:
67 | await bot.send_message(admin_id, report_text, reply_markup=button)
68 |
69 |
70 | async def send_response_to_user(response: dict):
71 | response_text = response['response']
72 | response_media = await _compose_media(response) if response['media_ids'] else None
73 |
74 | msg = await bot.send_message(
75 | response['user_id'],
76 | response_text,
77 | reply_to_message_id=response['message_id']
78 | )
79 | if response_media:
80 | if isinstance(response_media, types.MediaGroup):
81 | await bot.send_media_group(response['user_id'], response_media, reply_to_message_id=msg.message_id)
82 | else:
83 | await bot.send_photo(response['user_id'], response_media.file, reply_to_message_id=msg.message_id)
84 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/image_processor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from io import BytesIO
4 | from typing import Tuple
5 |
6 | from aiogram import Bot
7 | from aiogram.types import InlineKeyboardMarkup, Message, User, CallbackQuery, InputMedia
8 |
9 | from zmanim_bot.integrations import zmanim_models
10 | from zmanim_bot.keyboards import inline
11 | from zmanim_bot.processors.base import BaseProcessor
12 | from zmanim_bot.processors.image import renderer
13 |
14 |
15 | class ImageProcessor(BaseProcessor[BytesIO]):
16 |
17 | _type = 'image'
18 |
19 | async def _send(self, image: BytesIO, *kb_list: InlineKeyboardMarkup):
20 | bot = Bot.get_current()
21 | user = User.get_current()
22 | call = CallbackQuery.get_current()
23 | message = Message.get_current()
24 | kb_list = [kb for kb in kb_list if kb] # clean from None
25 |
26 | if call:
27 | reply_to = call.message.message_id
28 | elif message:
29 | reply_to = message and message.message_id
30 | else:
31 | reply_to = None
32 |
33 | if len(kb_list) == 0:
34 | kb = None
35 | elif len(kb_list) == 1:
36 | kb = kb_list[0]
37 | else:
38 | kb = inline.merge_inline_keyboards(kb_list[0], kb_list[1])
39 |
40 | await bot.send_photo(user.id, image, reply_markup=kb, reply_to_message_id=reply_to)
41 |
42 | async def _update(self, image: BytesIO, *kb_list: InlineKeyboardMarkup):
43 | bot = Bot.get_current()
44 | user = User.get_current()
45 | call = CallbackQuery.get_current()
46 | kb_list = [kb for kb in kb_list if kb] # clean from None
47 |
48 | media = InputMedia(media=image)
49 |
50 | if len(kb_list) == 0:
51 | kb = None
52 | elif len(kb_list) == 1:
53 | kb = kb_list[0]
54 | else:
55 | kb = inline.merge_inline_keyboards(kb_list[0], kb_list[1])
56 |
57 | await bot.edit_message_media(media, user.id, call.message.message_id, reply_markup=kb)
58 |
59 | def _get_zmanim(self, data: zmanim_models.Zmanim) -> BytesIO:
60 | return renderer.ZmanimImage(data, self._location_name).get_image()
61 |
62 | def _get_shabbat(self, data: zmanim_models.Shabbat) -> Tuple[BytesIO, InlineKeyboardMarkup]:
63 | return renderer.ShabbatImage(data, self._location_name).get_image()
64 |
65 | def _get_yom_tov(self, data: zmanim_models.YomTov) -> Tuple[BytesIO, InlineKeyboardMarkup]:
66 | return renderer.YomTovImage(data, self._location_name).get_image()
67 |
68 | def _get_fast(self, data: zmanim_models.Fast) -> Tuple[BytesIO, InlineKeyboardMarkup]:
69 | return renderer.FastImage(data, self._location_name).get_image()
70 |
71 | def _get_holiday(self, data: zmanim_models.Holiday) -> BytesIO:
72 | return renderer.HolidayImage(data).get_image()
73 |
74 | def _get_israel_holidays(self, data: zmanim_models.IsraelHolidays) -> BytesIO:
75 | return renderer.IsraelHolidaysImage(data).get_image()
76 |
77 | def _get_rosh_chodesh(self, data: zmanim_models.RoshChodesh) -> BytesIO:
78 | return renderer.RoshChodeshImage(data).get_image()
79 |
80 | def _get_daf_yomi(self, data: zmanim_models.DafYomi) -> BytesIO:
81 | return renderer.DafYomImage(data).get_image()
82 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/base.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from abc import ABC, abstractmethod
4 | from typing import Tuple, Generic, TypeVar
5 |
6 | from aiogram.types import InlineKeyboardMarkup
7 |
8 | from zmanim_bot.integrations.zmanim_models import (Zmanim, Shabbat, YomTov, Fast, IsraelHolidays,
9 | RoshChodesh, DafYomi,
10 | Holiday)
11 |
12 | T = TypeVar('T')
13 |
14 |
15 | class BaseProcessor(ABC, Generic[T]):
16 | _location_name: str
17 |
18 | def __init__(self, location_name: str):
19 | self._location_name = location_name
20 |
21 | @abstractmethod
22 | async def _send(self, data: T, *kb: InlineKeyboardMarkup):
23 | pass
24 |
25 | @abstractmethod
26 | async def _update(self, data: T, *kb: InlineKeyboardMarkup):
27 | pass
28 |
29 | async def send_zmanim(self, data: Zmanim, kb: InlineKeyboardMarkup):
30 | await self._send(self._get_zmanim(data), kb)
31 |
32 | async def send_shabbat(self, data: Shabbat, kb: InlineKeyboardMarkup):
33 | await self._send(*self._get_shabbat(data), kb)
34 |
35 | async def send_yom_tov(self, data: YomTov, kb: InlineKeyboardMarkup):
36 | await self._send(*self._get_yom_tov(data), kb)
37 |
38 | async def send_fast(self, data: Fast, kb: InlineKeyboardMarkup):
39 | await self._send(*self._get_fast(data), kb)
40 |
41 | async def send_holiday(self, data: Holiday):
42 | await self._send(self._get_holiday(data))
43 |
44 | async def send_israel_holidays(self, data: IsraelHolidays):
45 | await self._send(self._get_israel_holidays(data))
46 |
47 | async def send_rosh_chodesh(self, data: RoshChodesh):
48 | await self._send(self._get_rosh_chodesh(data))
49 |
50 | async def send_daf_yomi(self, data: DafYomi):
51 | await self._send(self._get_daf_yomi(data))
52 |
53 | async def update_zmanim(self, data: Zmanim, kb: InlineKeyboardMarkup):
54 | await self._update(self._get_zmanim(data), kb)
55 |
56 | async def update_shabbat(self, data: Shabbat, kb: InlineKeyboardMarkup):
57 | await self._update(*self._get_shabbat(data), kb)
58 |
59 | async def update_yom_tov(self, data: YomTov, kb: InlineKeyboardMarkup):
60 | await self._update(*self._get_yom_tov(data), kb)
61 |
62 | async def update_fast(self, data: Fast, kb: InlineKeyboardMarkup):
63 | await self._update(*self._get_fast(data), kb)
64 |
65 | @abstractmethod
66 | def _get_zmanim(self, data: Zmanim) -> T:
67 | pass
68 |
69 | @abstractmethod
70 | def _get_shabbat(self, data: Shabbat) -> Tuple[T, InlineKeyboardMarkup]:
71 | pass
72 |
73 | @abstractmethod
74 | def _get_yom_tov(self, data: YomTov) -> Tuple[T, InlineKeyboardMarkup]:
75 | pass
76 |
77 | @abstractmethod
78 | def _get_fast(self, data: Fast) -> Tuple[T, InlineKeyboardMarkup]:
79 | pass
80 |
81 | @abstractmethod
82 | def _get_holiday(self, data: Holiday) -> T:
83 | pass
84 |
85 | @abstractmethod
86 | def _get_israel_holidays(self, data: IsraelHolidays) -> T:
87 | pass
88 |
89 | @abstractmethod
90 | def _get_rosh_chodesh(self, data: RoshChodesh) -> T:
91 | pass
92 |
93 | @abstractmethod
94 | def _get_daf_yomi(self, data: DafYomi) -> T:
95 | pass
96 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/errors.py:
--------------------------------------------------------------------------------
1 | import aiogram_metrics
2 | import sentry_sdk
3 | from aiogram import Bot
4 | from aiogram.types import User, Message, CallbackQuery
5 | from aiogram.utils.exceptions import CantInitiateConversation
6 |
7 | from zmanim_bot.exceptions import *
8 | from zmanim_bot.handlers.utils.redirects import *
9 | from zmanim_bot.handlers.utils.warnings import *
10 | from zmanim_bot.misc import logger
11 | from zmanim_bot.texts.single import messages
12 | from zmanim_bot.texts.single.helpers import cl_error_warning
13 | from zmanim_bot.texts.single.messages import error_occured
14 |
15 |
16 | async def no_location_exception_handler(*_):
17 | await redirect_to_request_location()
18 | return True
19 |
20 |
21 | async def incorrect_location_exception_handler(*_):
22 | user = User.get_current()
23 | bot = Bot.get_current()
24 | await bot.send_message(user.id, messages.incorrect_locations_received)
25 | return True
26 |
27 |
28 | async def non_unique_location_exception_handler(*_):
29 | user = User.get_current()
30 | bot = Bot.get_current()
31 | await bot.send_message(user.id, messages.location_already_exists)
32 | return True
33 |
34 |
35 | async def non_unique_location_name_exception_handler(*_):
36 | user = User.get_current()
37 | bot = Bot.get_current()
38 | await bot.send_message(user.id, messages.location_name_already_exists)
39 | return True
40 |
41 |
42 | async def location_limit_exception_handler(*_):
43 | user = User.get_current()
44 | bot = Bot.get_current()
45 | await bot.send_message(user.id, messages.too_many_locations_error)
46 | return True
47 |
48 |
49 | async def no_language_exception_handler(*_):
50 | await redirect_to_request_language()
51 |
52 |
53 | async def gregorian_date_exception_handler(*_):
54 | await incorrect_greg_date_warning()
55 | return True
56 |
57 |
58 | async def jewish_date_exception_handler(*_):
59 | await incorrect_jew_date_warning()
60 | return True
61 |
62 |
63 | async def polar_coordinates_exception_handler(*_):
64 | await Message.get_current().reply(cl_error_warning.value.replace('\n', ' '))
65 | return True
66 |
67 |
68 | async def unknown_processor_exception_handler(_, e: UnknownProcessorException):
69 | await Message.get_current().reply(error_occured)
70 | raise e
71 |
72 |
73 | async def access_denied_exception_handler(*_):
74 | user = User.get_current()
75 | msg = Message.get_current()
76 | call = CallbackQuery.get_current()
77 |
78 | try:
79 | if msg:
80 | await msg.reply('Access was denied by admin.')
81 | elif call:
82 | await call.answer('Access was denied by admin.')
83 | else:
84 | await Bot.get_current().send_message(user.id, 'Access was denied by admin.')
85 | except CantInitiateConversation:
86 | pass
87 |
88 | aiogram_metrics.manual_track('Access denied')
89 | return True
90 |
91 |
92 | async def empty_exception_handler(*_):
93 | return True
94 |
95 |
96 | async def main_errors_handler(_, e: Exception):
97 | if isinstance(e, ZmanimBotBaseException):
98 | return True
99 | user = User.get_current()
100 | bot = Bot.get_current()
101 | await bot.send_message(user.id, error_occured)
102 |
103 | logger.exception(e)
104 |
105 | sentry_sdk.capture_exception(e)
106 | return True
107 |
--------------------------------------------------------------------------------
/zmanim_bot/repository/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime as dt
2 | from typing import List, Optional, Tuple
3 |
4 | from odmantic import EmbeddedModel, Field, Model
5 |
6 | from zmanim_bot.config import config
7 | from zmanim_bot.exceptions import NoLocationException, UnknownProcessorException
8 | from zmanim_bot.processors import PROCESSORS
9 | from zmanim_bot.processors.base import BaseProcessor
10 |
11 | HAVDALA_OPINIONS = ['tzeis_5_95_degrees', 'tzeis_8_5_degrees', 'tzeis_42_minutes', 'tzeis_72_minutes']
12 |
13 |
14 | class UserInfo(EmbeddedModel):
15 | first_name: Optional[str] = None
16 | last_name: Optional[str] = None
17 | username: Optional[str] = None
18 |
19 |
20 | class UserMeta(EmbeddedModel):
21 | last_seen_at: dt = Field(default_factory=dt.now)
22 | is_banned_by_admin: bool = False
23 | is_user_blocked_bot: bool = False
24 |
25 |
26 | class Location(EmbeddedModel):
27 | lat: float
28 | lng: float
29 | name: str
30 | is_active: bool
31 | elevation: int | None = 0
32 |
33 | @property
34 | def coordinates(self) -> Tuple[float, float, int]:
35 | elevation = self.elevation if self.elevation >= 0 else 0
36 | return self.lat, self.lng, elevation
37 |
38 |
39 | class OmerSettings(EmbeddedModel):
40 | is_enabled: bool = False
41 | is_sent_today: Optional[bool]
42 | notification_time: Optional[str]
43 |
44 |
45 | class ZmanimSettings(EmbeddedModel):
46 | alos: bool = True
47 | misheyakir_10_2: bool = True
48 | sunrise: bool = True
49 | sof_zman_shema_ma: bool = False
50 | sof_zman_shema_gra: bool = True
51 | sof_zman_tefila_ma: bool = False
52 | sof_zman_tefila_gra: bool = True
53 | chatzos: bool = True
54 | mincha_gedola: bool = True
55 | mincha_ketana: bool = False
56 | plag_mincha: bool = False
57 | sunset: bool = True
58 | tzeis_5_95_degrees: bool = False
59 | tzeis_8_5_degrees: bool = True
60 | tzeis_42_minutes: bool = False
61 | tzeis_72_minutes: bool = False
62 | chatzot_laila: bool = False
63 | astronomical_hour_ma: bool = False
64 | astronomical_hour_gra: bool = False
65 |
66 |
67 | class User(Model):
68 | user_id: int
69 | personal_info: UserInfo = Field(default_factory=UserInfo)
70 |
71 | language: Optional[str] = None
72 | location_list: List[Location] = Field(default_factory=list)
73 | cl_offset: int = 18
74 | havdala_opinion: str = 'tzeis_8_5_degrees'
75 | zmanim_settings: ZmanimSettings = Field(default_factory=ZmanimSettings)
76 | processor_type: str = 'image'
77 | omer: OmerSettings = Field(default_factory=OmerSettings)
78 |
79 | meta: UserMeta = Field(default_factory=UserMeta)
80 |
81 | class Config:
82 | collection = config.DB_COLLECTION_NAME
83 | parse_doc_with_default_factories = True
84 |
85 | @property
86 | def location(self) -> Location:
87 | loc = list(filter(lambda l: l.is_active, self.location_list))
88 | if not loc:
89 | raise NoLocationException
90 | return loc[0]
91 |
92 | def get_location_by_coords(self, lat: float, lng: float) -> Location:
93 | resp = list(filter(lambda loc: loc.lat == lat and loc.lng == lng, self.location_list))
94 | if not resp:
95 | raise NoLocationException
96 | return resp[0]
97 |
98 | def get_processor(self, location: Optional[Location] = None) -> BaseProcessor:
99 | try:
100 | return PROCESSORS[self.processor_type]((location and location.name) or self.location.name)
101 | except KeyError:
102 | raise UnknownProcessorException()
103 |
104 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/settings.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import CallbackQuery, Message
2 | from aiogram.utils.exceptions import MessageNotModified
3 | from aiogram_metrics import track
4 |
5 | from zmanim_bot.handlers.utils.redirects import (redirect_to_main_menu,
6 | redirect_to_request_language)
7 | from zmanim_bot.helpers import (CALL_ANSWER_OK)
8 | from zmanim_bot.misc import bot
9 | from zmanim_bot.service import settings_service
10 | from zmanim_bot.utils import chat_action
11 |
12 |
13 | @chat_action('text')
14 | @track('Candle lighting selection')
15 | async def settings_menu_cl(msg: Message):
16 | resp, kb = await settings_service.get_current_cl()
17 | await msg.reply(resp, reply_markup=kb)
18 |
19 |
20 | async def set_cl(call: CallbackQuery):
21 | await call.answer(CALL_ANSWER_OK)
22 |
23 | kb = await settings_service.set_cl(call.data)
24 | try:
25 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
26 | except MessageNotModified:
27 | pass
28 |
29 |
30 | @chat_action('text')
31 | @track('Havdala selection')
32 | async def settings_menu_havdala(msg: Message):
33 | resp, kb = await settings_service.get_current_havdala()
34 | await msg.reply(resp, reply_markup=kb)
35 |
36 |
37 | async def set_havdala(call: CallbackQuery):
38 | await call.answer(CALL_ANSWER_OK)
39 |
40 | kb = await settings_service.set_havdala(call.data)
41 |
42 | try:
43 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
44 | except MessageNotModified:
45 | pass
46 |
47 |
48 | @track('Zmanim selection')
49 | async def settings_menu_zmanim(msg: Message):
50 | resp, kb = await settings_service.get_current_zmanim()
51 | await msg.reply(resp, reply_markup=kb)
52 |
53 |
54 | async def set_zmanim(call: CallbackQuery):
55 | await call.answer(CALL_ANSWER_OK)
56 |
57 | kb = await settings_service.set_zmanim(call.data)
58 | try:
59 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
60 | except MessageNotModified:
61 | pass
62 |
63 |
64 | @chat_action('text')
65 | @track('Omer Settings')
66 | async def handle_omer_settings(msg: Message):
67 | resp, kb = await settings_service.get_current_omer()
68 | await msg.reply(resp, reply_markup=kb)
69 |
70 |
71 | async def set_omer(call: CallbackQuery):
72 | await call.answer(CALL_ANSWER_OK)
73 |
74 | kb = await settings_service.set_omer(call.data)
75 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
76 |
77 |
78 | @chat_action('text')
79 | @track('Format settings')
80 | async def handle_format_settings(msg: Message):
81 | resp, kb = await settings_service.get_current_format()
82 | await msg.reply(resp, reply_markup=kb)
83 |
84 |
85 | async def set_format(call: CallbackQuery):
86 | await call.answer(CALL_ANSWER_OK)
87 |
88 | kb = await settings_service.set_format(call.data)
89 |
90 | try:
91 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
92 | except MessageNotModified:
93 | pass
94 |
95 |
96 | @chat_action('text')
97 | @track('Language command')
98 | async def handle_language_request(_: Message):
99 | await redirect_to_request_language()
100 |
101 |
102 | @chat_action('text')
103 | @track('Language selected')
104 | async def set_language(msg: Message):
105 | await settings_service.set_language(msg.text)
106 | return await redirect_to_main_menu()
107 |
108 |
109 | @chat_action('text')
110 | @track('Init report')
111 | async def help_menu_report(msg: Message):
112 | resp, kb = await settings_service.init_report()
113 | await msg.reply(resp, reply_markup=kb)
114 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/forms.py:
--------------------------------------------------------------------------------
1 | from asyncio import create_task
2 |
3 | from aiogram.dispatcher import FSMContext
4 | from aiogram.types import ContentType, Message
5 |
6 | from zmanim_bot.admin.report_management import send_report_to_admins
7 | from zmanim_bot.handlers.utils.redirects import (redirect_to_main_menu, redirect_to_settings_menu)
8 | from zmanim_bot.helpers import check_date
9 | from zmanim_bot.keyboards.menus import get_report_keyboard
10 | from zmanim_bot.misc import bot
11 | from zmanim_bot.service import converter_service, settings_service, zmanim_service
12 | from zmanim_bot.states import FeedbackState
13 | from zmanim_bot.texts.single import messages, buttons
14 | from zmanim_bot.utils import chat_action
15 |
16 |
17 | # REPORTS
18 |
19 | @chat_action('text')
20 | async def handle_report(msg: Message, state: FSMContext):
21 | report = {
22 | 'message': msg.text,
23 | 'message_id': msg.message_id,
24 | 'user_id': msg.from_user.id,
25 | 'media_ids': []
26 | }
27 | await state.set_data(report)
28 | await FeedbackState.next()
29 |
30 | kb = get_report_keyboard()
31 | await bot.send_message(msg.chat.id, messages.reports_text_received, reply_markup=kb)
32 |
33 |
34 | @chat_action('text')
35 | async def handle_done_report(_, state: FSMContext):
36 | report = await state.get_data()
37 | await state.finish()
38 | await redirect_to_main_menu(messages.reports_created)
39 | create_task(send_report_to_admins(report))
40 |
41 |
42 | @chat_action('text')
43 | async def handle_report_payload(msg: Message, state: FSMContext):
44 | if msg.content_type != ContentType.PHOTO:
45 | return await msg.reply(messages.reports_incorrect_media_type)
46 |
47 | report = await state.get_data()
48 | report['media_ids'].append((msg.photo[-1].file_id, 'photo'))
49 | await state.set_data(report)
50 |
51 | await msg.reply(messages.reports_media_received)
52 |
53 |
54 | # CONVERTER #
55 |
56 | @chat_action('text')
57 | async def handle_converter_gregorian_date(msg: Message, state: FSMContext):
58 | resp, kb = converter_service.convert_greg_to_heb(msg.text)
59 | await state.finish()
60 | await msg.reply(resp, reply_markup=kb)
61 | await redirect_to_main_menu()
62 |
63 |
64 | @chat_action('text')
65 | async def handle_converter_jewish_date(msg: Message, state: FSMContext):
66 | resp, kb = converter_service.convert_heb_to_greg(msg.text)
67 | await state.finish()
68 | await msg.reply(resp, reply_markup=kb)
69 | await redirect_to_main_menu()
70 |
71 |
72 | # ZMANIM #
73 |
74 | @chat_action('text')
75 | async def handle_zmanim_gregorian_date(msg: Message, state: FSMContext):
76 | check_date(msg.text)
77 | await zmanim_service.send_zmanim(date=msg.text, state=state)
78 | await state.finish()
79 | await redirect_to_main_menu()
80 |
81 |
82 | # LOCATIONS #
83 |
84 | @chat_action('text')
85 | async def handle_location_name(msg: Message, state: FSMContext):
86 | state_data = await state.get_data()
87 |
88 | old_name = state_data.get('location_name')
89 | redirect_target = state_data.get('redirect_target', 'main')
90 | redirect_message = state_data.get('redirect_message')
91 | origin_message_id = state_data.get('origin_message_id')
92 | targets = {
93 | 'main': redirect_to_main_menu,
94 | 'settings': redirect_to_settings_menu
95 | }
96 | redirect = targets[redirect_target]
97 |
98 | if msg.text == buttons.done.value:
99 | await state.finish()
100 | return await redirect(redirect_message)
101 |
102 | location_kb = await settings_service.update_location_name(new_name=msg.text, old_name=old_name)
103 |
104 | if origin_message_id:
105 | await bot.edit_message_reply_markup(msg.from_user.id, origin_message_id, reply_markup=location_kb)
106 |
107 | await state.finish()
108 | await redirect(redirect_message)
109 |
--------------------------------------------------------------------------------
/zmanim_bot/service/festivals_service.py:
--------------------------------------------------------------------------------
1 | from aiogram_metrics import track
2 |
3 | from zmanim_bot.helpers import CallbackPrefixes
4 | from zmanim_bot.integrations import zmanim_api_client
5 | from zmanim_bot.keyboards import inline
6 | from zmanim_bot.repository import bot_repository
7 | from zmanim_bot.texts.single import buttons
8 |
9 |
10 | def _get_festival_name(input_str: str) -> str:
11 | festival_shortcuts = {
12 | buttons.hom_rosh_hashana.value: 'rosh_hashana',
13 | buttons.hom_yom_kippur.value: 'yom_kippur',
14 | buttons.hom_succot.value: 'succot',
15 | buttons.hom_shmini_atzeret.value: 'shmini_atzeres',
16 | buttons.hom_chanukah.value: 'chanukah',
17 | buttons.hom_purim.value: 'purim',
18 | buttons.hom_pesach.value: 'pesach',
19 | buttons.hom_shavuot.value: 'shavuot',
20 | buttons.hom_tu_bishvat.value: 'tu_bi_shvat',
21 | buttons.hom_lag_baomer.value: 'lag_baomer',
22 | buttons.fm_gedaliah.value: 'fast_gedalia',
23 | buttons.fm_tevet.value: 'fast_10_teves',
24 | buttons.fm_esther.value: 'fast_esther',
25 | buttons.fm_tammuz.value: 'fast_17_tammuz',
26 | buttons.fm_av.value: 'fast_9_av',
27 | }
28 | return festival_shortcuts[input_str]
29 |
30 |
31 | @track('Fast')
32 | async def get_generic_fast(fast_name: str):
33 | user = await bot_repository.get_or_create_user()
34 | data = await zmanim_api_client.get_generic_fast(
35 | name=_get_festival_name(fast_name),
36 | location=user.location.coordinates,
37 | havdala_opinion=user.havdala_opinion
38 | )
39 | kb = inline.get_location_variants_menu(user.location_list, user.location, CallbackPrefixes.update_fast)
40 | await user.get_processor().send_fast(data, kb)
41 |
42 |
43 | @track('Fast geo-variant')
44 | async def update_generic_fast(fast_name: str, lat: float, lng: float):
45 | user = await bot_repository.get_or_create_user()
46 | location = user.get_location_by_coords(lat, lng)
47 | data = await zmanim_api_client.get_generic_fast(
48 | name=_get_festival_name(fast_name),
49 | location=location.coordinates,
50 | havdala_opinion=user.havdala_opinion
51 | )
52 | kb = inline.get_location_variants_menu(user.location_list, location, CallbackPrefixes.update_fast)
53 | await user.get_processor(location).update_fast(data, kb)
54 |
55 |
56 | @track('Yom tov')
57 | async def get_generic_yomtov(yomtov_name: str):
58 | user = await bot_repository.get_or_create_user()
59 | data = await zmanim_api_client.get_generic_yomtov(
60 | name=_get_festival_name(yomtov_name),
61 | location=user.location.coordinates,
62 | cl_offset=user.cl_offset,
63 | havdala_opinion=user.havdala_opinion
64 | )
65 | kb = inline.get_location_variants_menu(user.location_list, user.location, CallbackPrefixes.update_yom_tov)
66 | await user.get_processor().send_yom_tov(data, kb)
67 |
68 |
69 | @track('Yom tov geo-variant')
70 | async def update_generic_yom_tov(yom_tov_name: str, lat: float, lng: float):
71 | user = await bot_repository.get_or_create_user()
72 | location = user.get_location_by_coords(lat, lng)
73 | data = await zmanim_api_client.get_generic_yomtov(
74 | name=_get_festival_name(yom_tov_name),
75 | location=location.coordinates,
76 | cl_offset=user.cl_offset,
77 | havdala_opinion=user.havdala_opinion
78 | )
79 | kb = inline.get_location_variants_menu(user.location_list, location, CallbackPrefixes.update_yom_tov)
80 | await user.get_processor(location).update_yom_tov(data, kb)
81 |
82 |
83 | async def get_generic_holiday(holiday_name: str):
84 | user = await bot_repository.get_or_create_user()
85 | if holiday_name == buttons.hom_israel:
86 | data = await zmanim_api_client.get_israel_holidays()
87 | await user.get_processor().send_israel_holidays(data)
88 | else:
89 | data = await zmanim_api_client.get_generic_holiday(_get_festival_name(holiday_name))
90 | await user.get_processor().send_holiday(data)
91 |
--------------------------------------------------------------------------------
/zmanim_bot/keyboards/menus.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
2 |
3 | from zmanim_bot.config import config
4 | from zmanim_bot.middlewares.i18n import i18n_
5 | from zmanim_bot.texts.single import buttons
6 |
7 |
8 | def get_lang_menu() -> ReplyKeyboardMarkup:
9 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
10 | kb.add(*config.LANGUAGE_LIST)
11 | return kb
12 |
13 |
14 | def get_main_menu() -> ReplyKeyboardMarkup:
15 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
16 | kb.add(buttons.mm_zmanim.value, buttons.mm_shabbat.value, buttons.mm_holidays.value)
17 | kb.add(buttons.mm_rh.value, buttons.mm_daf_yomi.value, buttons.mm_fasts.value)
18 | kb.add(buttons.mm_zmanim_by_date.value, buttons.mm_converter.value)
19 | kb.add(buttons.hm_report.value, buttons.mm_donate.value, buttons.mm_settings.value)
20 |
21 | if i18n_.is_rtl():
22 | for row in kb.keyboard:
23 | row.reverse()
24 | return kb
25 |
26 |
27 | # def get_help_menu() -> ReplyKeyboardMarkup:
28 | # kb = ReplyKeyboardMarkup(resize_keyboard=True)
29 | # kb.row(buttons.hm_faq.value, buttons.hm_report.value)
30 | # kb.row(buttons.back.value)
31 | # return kb
32 |
33 |
34 | def get_settings_menu() -> ReplyKeyboardMarkup:
35 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
36 | kb.row(buttons.sm_zmanim.value, buttons.sm_candle.value, buttons.sm_havdala.value)
37 | kb.row(buttons.sm_format.value, buttons.sm_lang.value, buttons.sm_omer.value)
38 | kb.row(buttons.sm_location.value, buttons.back.value)
39 |
40 | if i18n_.is_rtl():
41 | for row in kb.keyboard:
42 | row.reverse()
43 |
44 | return kb
45 |
46 |
47 | def get_holidays_menu() -> ReplyKeyboardMarkup:
48 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
49 | kb.row(buttons.hom_rosh_hashana.value, buttons.hom_yom_kippur.value, buttons.hom_succot.value)
50 | kb.row(buttons.hom_shmini_atzeret.value, buttons.hom_chanukah.value, buttons.hom_purim.value)
51 | kb.row(buttons.hom_pesach.value, buttons.hom_shavuot.value, buttons.hom_more.value)
52 | kb.row(buttons.back.value)
53 |
54 | if i18n_.is_rtl():
55 | for row in kb.keyboard:
56 | row.reverse()
57 |
58 | return kb
59 |
60 |
61 | def get_more_holidays_menu() -> ReplyKeyboardMarkup:
62 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
63 | kb.row(buttons.hom_tu_bishvat.value, buttons.hom_lag_baomer.value, buttons.hom_israel.value)
64 | kb.row(buttons.mm_holidays.value, buttons.back.value)
65 |
66 | if i18n_.is_rtl():
67 | for row in kb.keyboard:
68 | row.reverse()
69 |
70 | return kb
71 |
72 |
73 | def get_fast_menu() -> ReplyKeyboardMarkup:
74 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
75 | kb.row(buttons.fm_gedaliah.value, buttons.fm_tevet.value, buttons.fm_esther.value)
76 | kb.row(buttons.fm_tammuz.value, buttons.fm_av.value)
77 | kb.row(buttons.back.value)
78 |
79 | if i18n_.is_rtl():
80 | for row in kb.keyboard:
81 | row.reverse()
82 |
83 | return kb
84 |
85 |
86 | def get_converter_menu() -> ReplyKeyboardMarkup:
87 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
88 | kb.row(buttons.conv_greg_to_jew.value, buttons.conv_jew_to_greg.value)
89 | kb.row(buttons.back.value)
90 | return kb
91 |
92 |
93 | def get_geobutton(with_back: bool = False) -> ReplyKeyboardMarkup:
94 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
95 | geobutton = KeyboardButton(text=buttons.geobutton.value, request_location=True)
96 | kb.row(geobutton) if not with_back else kb.row(buttons.back.value, geobutton)
97 | return kb
98 |
99 |
100 | def get_cancel_keyboard() -> ReplyKeyboardMarkup:
101 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
102 | kb.row(buttons.cancel.value)
103 | return kb
104 |
105 |
106 | def get_done_keyboard() -> ReplyKeyboardMarkup:
107 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
108 | kb.row(buttons.done.value)
109 | return kb
110 |
111 |
112 | def get_report_keyboard() -> ReplyKeyboardMarkup:
113 | kb = ReplyKeyboardMarkup(resize_keyboard=True)
114 | kb.row(buttons.cancel.value, buttons.done.value)
115 | return kb
116 |
--------------------------------------------------------------------------------
/zmanim_bot/service/zmanim_service.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from aiogram.dispatcher import FSMContext
4 | from aiogram.types import CallbackQuery
5 |
6 | from zmanim_bot.helpers import CallbackPrefixes
7 | from zmanim_bot.integrations import zmanim_api_client
8 | from zmanim_bot.integrations.zmanim_models import Zmanim
9 | from zmanim_bot.keyboards import inline
10 | from zmanim_bot.misc import bot
11 | from zmanim_bot.repository import bot_repository
12 | from zmanim_bot.states import ZmanimGregorianDateState
13 |
14 |
15 | async def get_zmanim(must_have_8_5: bool = False) -> Zmanim:
16 | user = await bot_repository.get_or_create_user()
17 | user_zmanim_settings = user.zmanim_settings
18 |
19 | if must_have_8_5:
20 | user_zmanim_settings.tzeis_8_5_degrees = True
21 |
22 | data = await zmanim_api_client.get_zmanim(
23 | user.location.coordinates,
24 | user_zmanim_settings.dict()
25 | )
26 | return data
27 |
28 |
29 | async def send_zmanim(*, state: FSMContext, date: str = None, call: CallbackQuery = None):
30 | if call:
31 | date = call.data.split(CallbackPrefixes.zmanim_by_date)[1]
32 | user = await bot_repository.get_or_create_user()
33 |
34 | state_data = await state.get_data()
35 | if state_data.get('current_location'):
36 | location = user.get_location_by_coords(*state_data['current_location'])
37 | else:
38 | location = user.location
39 |
40 | data = await zmanim_api_client.get_zmanim(
41 | location.coordinates,
42 | user.zmanim_settings.dict(),
43 | date_=date
44 | )
45 | kb = inline.get_location_variants_menu(
46 | user.location_list,
47 | location,
48 | CallbackPrefixes.update_zmanim,
49 | date_=date
50 | )
51 | await user.get_processor(location=location).send_zmanim(data, kb)
52 | if call:
53 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
54 |
55 |
56 | async def update_zmanim(lat: float, lng: float, date: Optional[str]):
57 | user = await bot_repository.get_or_create_user()
58 | location = user.get_location_by_coords(lat, lng)
59 |
60 | data = await zmanim_api_client.get_zmanim(
61 | location.coordinates,
62 | user.zmanim_settings.dict(),
63 | date_=date
64 | )
65 | kb = inline.get_location_variants_menu(
66 | user.location_list,
67 | location,
68 | CallbackPrefixes.update_zmanim,
69 | date_=date
70 | )
71 | await user.get_processor(location).update_zmanim(data, kb)
72 |
73 |
74 | async def init_zmanim_by_date():
75 | await ZmanimGregorianDateState().waiting_for_gregorian_date.set()
76 |
77 |
78 | async def get_shabbat():
79 | user = await bot_repository.get_or_create_user()
80 | data = await zmanim_api_client.get_shabbat(
81 | location=user.location.coordinates,
82 | cl_offset=user.cl_offset,
83 | havdala_opinion=user.havdala_opinion
84 | )
85 | kb = inline.get_location_variants_menu(user.location_list, user.location, CallbackPrefixes.update_shabbat)
86 | await user.get_processor().send_shabbat(data, kb)
87 |
88 |
89 | async def update_shabbat(lat: float, lng: float, state: FSMContext):
90 | user = await bot_repository.get_or_create_user()
91 | location = user.get_location_by_coords(lat, lng)
92 | await state.update_data({'current_location': [lat, lng]})
93 |
94 | data = await zmanim_api_client.get_shabbat(
95 | location=location.coordinates,
96 | cl_offset=user.cl_offset,
97 | havdala_opinion=user.havdala_opinion
98 | )
99 | kb = inline.get_location_variants_menu(user.location_list, location, CallbackPrefixes.update_shabbat)
100 | await user.get_processor(location).update_shabbat(data, kb)
101 |
102 |
103 | async def get_daf_yomi():
104 | user = await bot_repository.get_or_create_user()
105 | data = await zmanim_api_client.get_daf_yomi()
106 | await user.get_processor().send_daf_yomi(data)
107 |
108 |
109 | async def get_rosh_chodesh():
110 | user = await bot_repository.get_or_create_user()
111 | data = await zmanim_api_client.get_rosh_chodesh()
112 | await user.get_processor().send_rosh_chodesh(data)
113 |
--------------------------------------------------------------------------------
/zmanim_bot/integrations/zmanim_api_client.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 | from typing import Optional, Tuple
3 |
4 | from zmanim_bot.config import config
5 | from zmanim_bot.integrations.zmanim_models import *
6 | from zmanim_bot.misc import bot
7 |
8 | __all__ = [
9 | 'get_zmanim',
10 | 'get_shabbat',
11 | 'get_daf_yomi',
12 | 'get_rosh_chodesh',
13 | 'get_generic_yomtov',
14 | 'get_generic_holiday',
15 | 'get_generic_fast',
16 | 'get_israel_holidays'
17 | ]
18 |
19 |
20 | async def get_zmanim(location: Tuple[float, float, int], zmanim_settings: dict, date_: str = None) -> Zmanim:
21 | url = config.ZMANIM_API_URL.format('zmanim')
22 | params = {
23 | 'lat': str(location[0]),
24 | 'lng': str(location[1]),
25 | 'elevation': str(location[2]) if location[2] else '0'
26 | }
27 | if date_:
28 | params['date'] = date_
29 |
30 | async with (await bot.get_session()).post(url, params=params, json=zmanim_settings) as resp:
31 | raw_resp = await resp.json()
32 | return Zmanim(**raw_resp)
33 |
34 |
35 | async def get_shabbat(
36 | location: Tuple[float, float, int],
37 | cl_offset: int,
38 | havdala_opinion: str,
39 | date_: str = None
40 | ) -> Shabbat:
41 | url = config.ZMANIM_API_URL.format('shabbat')
42 | params = {
43 | 'lat': str(location[0]),
44 | 'lng': str(location[1]),
45 | 'elevation': str(location[2]) if location[2] else '0',
46 | 'cl_offset': str(cl_offset),
47 | 'havdala': havdala_opinion
48 | }
49 | if date_:
50 | params['date'] = date_
51 |
52 | async with (await bot.get_session()).get(url, params=params) as resp:
53 | raw_resp = await resp.json()
54 | return Shabbat(**raw_resp)
55 |
56 |
57 | async def get_daf_yomi(date_=None) -> DafYomi:
58 | url = config.ZMANIM_API_URL.format('daf_yomi')
59 | params = None if not date_ else {'date': date_}
60 |
61 | async with (await bot.get_session()).get(url, params=params) as resp:
62 | raw_resp = await resp.json()
63 | return DafYomi(**raw_resp)
64 |
65 |
66 | async def get_rosh_chodesh(date_=None) -> RoshChodesh:
67 | url = config.ZMANIM_API_URL.format('rosh_chodesh')
68 | params = None if not date_ else {'date': date_}
69 |
70 | async with (await bot.get_session()).get(url, params=params) as resp:
71 | raw_resp = await resp.json()
72 | return RoshChodesh(**raw_resp)
73 |
74 |
75 | async def get_generic_yomtov(
76 | name: str,
77 | location: Tuple[float, float, int],
78 | cl_offset: int,
79 | havdala_opinion: str
80 | ) -> YomTov:
81 | url = config.ZMANIM_API_URL.format('yom_tov')
82 | params = {
83 | 'lat': str(location[0]),
84 | 'lng': str(location[1]),
85 | 'elevation': str(location[2]) if location[2] else '0',
86 | 'yomtov_name': name,
87 | 'cl': str(cl_offset),
88 | 'havdala': havdala_opinion
89 | }
90 |
91 | async with (await bot.get_session()).get(url, params=params) as resp:
92 | raw_resp = await resp.json()
93 | return YomTov(**raw_resp)
94 |
95 |
96 | async def get_generic_fast(name: str, location: Tuple[float, float, int], havdala_opinion: str) -> Fast:
97 | url = config.ZMANIM_API_URL.format('fast')
98 | params = {
99 | 'lat': str(location[0]),
100 | 'lng': str(location[1]),
101 | 'elevation': str(location[2]) if location[2] else '0',
102 | 'fast_name': name,
103 | 'havdala': havdala_opinion
104 | }
105 |
106 | async with (await bot.get_session()).get(url, params=params) as resp:
107 | raw_resp = await resp.json()
108 | return Fast(**raw_resp)
109 |
110 |
111 | async def get_generic_holiday(name: str) -> Holiday:
112 | url = config.ZMANIM_API_URL.format('holiday')
113 | params = {'holiday_name': name}
114 |
115 | async with (await bot.get_session()).get(url, params=params) as resp:
116 | raw_resp = await resp.json()
117 | return Holiday(**raw_resp)
118 |
119 |
120 | async def get_israel_holidays() -> IsraelHolidays:
121 | url = config.ZMANIM_API_URL.format('holiday')
122 | result = []
123 | settings: Optional[SimpleSettings] = None
124 |
125 | for name in ['yom_hashoah', 'yom_hazikaron', 'yom_haatzmaut', 'yom_yerushalaim']:
126 | params = {'holiday_name': name}
127 |
128 | async with (await bot.get_session()).get(url, params=params) as resp:
129 | raw_resp = await resp.json()
130 | result.append((name, date.fromisoformat(raw_resp['date'])))
131 |
132 | if not settings:
133 | settings = raw_resp['settings']
134 |
135 | return IsraelHolidays(settings=settings, holiday_list=result)
136 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/single/messages.py:
--------------------------------------------------------------------------------
1 | from zmanim_bot.middlewares.i18n import lazy_gettext as _
2 |
3 | init_bot = _('Welcome to zmanim bot!')
4 | init_main_menu = _('You are in the main menu:')
5 | init_help = _('How can I help you?')
6 | init_settings = _('You are in the settings:')
7 | init_report = _('Please, describe your problem:')
8 | init_converter = _('Choose the way of conversion:')
9 | select = _('Choose:')
10 |
11 | settings_cl = _('How long before shkiya do you light candles?')
12 | settings_zmanim = _('Select zmanim that you want to receive:')
13 | settings_havdala = _('Select your opinion for havdala:')
14 | settings_omer = _('Here you can enable or disable notifications about the Omer count.')
15 | settings_location = _('Here you can activate, edit or send new location;\nIf you want to add new location, just send it right now.\nNotice! if you change the name of a location, it will point to the same location. To add another location, use the "Add Location" button.')
16 | settings_format = _('Select an output type for the bot\'s responces:')
17 |
18 | request_language = _('Select your language:')
19 |
20 | request_location_on_init = _('For this feature, the bot should know your location. If you want to find it by name, press the button below.\nAlso, you can pass your location as an attachment or as a pair of coordinates, like 55.5, 32.2.')
21 | request_location = _('If you want to find location by name, press the button below.\nAlso, you can pass your location as an attachment or as a pair of coordinates, like 55.5, 32.2.')
22 | location_menu_init = _('Here you can manage your saved locations or add some more.')
23 | incorrect_locations_received = _('You sent incorrect coordinates. Please check and re-send them!')
24 | location_already_exists = _('This location already exists in your saved locations!')
25 | location_name_already_exists = _('This location name already exists in your saved locations!')
26 | too_many_locations_error = _('You saved too many locations, please remove some of them before saving new one!')
27 | custom_location_name_request = _('Automatic location name: {}.\n'
28 | 'You can write custom name for the location or press "Done" button.')
29 | location_deleted = _('Location successfully deleted!')
30 | location_renamed = _('Location successfully renamed!')
31 | location_saved = _('Location successfully saved!')
32 | unable_to_delete_active_location = _('It is unable to delete active location!')
33 | location_new_name_request = _('Please, write here new location name for {}:')
34 | location_activated = _('{} selected!')
35 |
36 | incorrect_text = _('"I’m not aware of such command 🤖\nWould you choose something from the menu:')
37 |
38 |
39 | greg_date_request = _('Please enter Gregorian date in following format: YYYY-MM-DD')
40 | incorrect_greg_date = _('Incorrect/Illegal date format. Take a look at the correct one: '
41 | 'YYYY-MM-DD')
42 | jew_date_request = _('Please enter Jewish date in following format: \nYYYY-MM-DD\n'
43 | '1 Nisan\n'
44 | '2 Iyar\n'
45 | '3 Sivan\n'
46 | '4 Tammuz\n'
47 | '5 Av\n'
48 | '6 Elul\n'
49 | '7 Tishrei\n'
50 | '8 Cheshvan\n'
51 | '9 Kislev\n'
52 | '10 Tevet\n'
53 | '11 Shvat\n'
54 | '12 Adar/Adar\n'
55 | '13 Adar II')
56 | incorrect_jew_date = _('Incorrect/Illegal date format. Take a look at the correct one: \nYYYY-MM-DD\n'
57 | '1 Nisan\n'
58 | '2 Iyar\n'
59 | '3 Sivan\n'
60 | '4 Tammuz\n'
61 | '5 Av\n'
62 | '6 Elul\n'
63 | '7 Tishrei\n'
64 | '8 Cheshvan\n'
65 | '9 Kislev\n'
66 | '10 Tevet\n'
67 | '11 Shvat\n'
68 | '12 Adar/Adar\n'
69 | '13 Adar II')
70 |
71 |
72 | reports_text_received = _('Your message has been saved for sending. You can attach screenshots or '
73 | 'videos of your choice. Once done, click the Done button.')
74 | reports_media_received = _('File added successfully. Add another one or click Done.')
75 | reports_incorrect_media_type = _('Unsupported message format. You can attach screenshots or videos '
76 | 'by sending a photo, picture or video.')
77 | reports_created = _('Your message has been sent to the Bot\'s developer. He will fix the problem soon '
78 | 'and you will receive a notification.\n'
79 | 'Thanks for your feedback!')
80 |
81 | error_occured = _('Looks like an error occured... The Bot\'s developer already working on it...')
82 |
83 | donate_init = _('Select donate amount in $:')
84 | donate_invoice_title = _('${} donate')
85 | donate_invoice_description = _('Support zmanim bot with one-time ${} payment.')
86 | donate_thanks = _('Thank you so mach! Your support motivates the developer to spend more time on the bot development!')
87 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/geolocation.py:
--------------------------------------------------------------------------------
1 | from hashlib import md5
2 | from typing import List
3 |
4 | from aiogram.dispatcher import FSMContext
5 | from aiogram.types import InlineQuery, InlineQueryResultLocation, Message, CallbackQuery
6 | from aiogram.utils.exceptions import MessageNotModified
7 | from aiogram_metrics import track
8 |
9 | from zmanim_bot.helpers import CallbackPrefixes, parse_coordinates
10 | from zmanim_bot.integrations.geo_client import get_location_name, find_places_by_query
11 | from zmanim_bot.keyboards import inline
12 | from zmanim_bot.keyboards.menus import get_cancel_keyboard
13 | from zmanim_bot.misc import bot
14 | from zmanim_bot.repository.bot_repository import get_or_create_user
15 | from zmanim_bot.repository.models import User
16 | from zmanim_bot.service import settings_service
17 | from zmanim_bot.states import LocationNameState
18 | from zmanim_bot.texts.single import messages
19 | from zmanim_bot.utils import chat_action
20 |
21 |
22 | async def get_locations_by_location(lat: float, lng: float, user: User) -> List[InlineQueryResultLocation]:
23 | location_name = await get_location_name(lat, lng, user.language, no_trim=True)
24 | result_id = md5(f'{lat}, {lng}'.encode()).hexdigest()
25 | return [InlineQueryResultLocation(id=result_id, latitude=lat, longitude=lng, title=location_name)]
26 |
27 |
28 | async def handle_inline_location_query(query: InlineQuery):
29 | user = await get_or_create_user()
30 |
31 | if len(query.query) < 3:
32 | if not query.location:
33 | return # too short query to search
34 | results = await get_locations_by_location(query.location.latitude, query.location.longitude, user)
35 | else:
36 | results = await find_places_by_query(query.query, user.language)
37 |
38 | await bot.answer_inline_query(query.id, results)
39 |
40 |
41 | @chat_action('text')
42 | # @track('Location menu init')
43 | async def location_settings(msg: Message):
44 | kb = inline.get_location_management_kb()
45 | resp = messages.location_menu_init
46 | await msg.reply(resp, reply_markup=kb)
47 |
48 |
49 | # @track('Location menu back')
50 | async def back_to_location_settings(call: CallbackQuery):
51 | await call.answer()
52 | kb = inline.get_location_management_kb()
53 | resp = messages.location_menu_init
54 | await bot.edit_message_text(resp, call.from_user.id, call.message.message_id)
55 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
56 |
57 |
58 | @track('Location menu -> add new location')
59 | async def add_new_location(call: CallbackQuery):
60 | await call.answer()
61 | await bot.edit_message_text(messages.request_location, call.from_user.id, call.message.message_id)
62 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=inline.LOCATION_SEARCH_KB)
63 |
64 |
65 | @track('Location menu -> Manage saved locations')
66 | async def manage_saved_locations(call: CallbackQuery):
67 | await call.answer()
68 | resp, kb = await settings_service.get_locations_menu()
69 | await bot.edit_message_text(resp, call.from_user.id, call.message.message_id)
70 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
71 |
72 |
73 | async def handle_activate_location(call: CallbackQuery):
74 | location_name = call.data.split(CallbackPrefixes.location_activate)[1]
75 | alert, kb = await settings_service.activate_location(location_name)
76 | await call.answer(alert)
77 |
78 | try:
79 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
80 | except MessageNotModified:
81 | pass
82 |
83 |
84 | async def init_location_rename(call: CallbackQuery, state: FSMContext):
85 | await LocationNameState.waiting_for_location_name_state.set()
86 | location_name = call.data.split(CallbackPrefixes.location_rename)[1]
87 |
88 | state_data = {
89 | 'location_name': location_name,
90 | 'redirect_target': 'settings',
91 | 'redirect_message': messages.location_renamed.value,
92 | 'origin_message_id': call.message.message_id
93 | }
94 |
95 | await state.set_data(state_data)
96 | await call.answer()
97 |
98 | resp = messages.location_new_name_request.value.format(location_name)
99 | kb = get_cancel_keyboard()
100 | await bot.send_message(call.from_user.id, resp, reply_markup=kb)
101 |
102 |
103 | async def handle_delete_location(call: CallbackQuery):
104 | location_name = call.data.split(CallbackPrefixes.location_delete)[1]
105 | alert_text, kb = await settings_service.delete_location(location_name)
106 | await call.answer(alert_text, show_alert=True)
107 |
108 | if kb:
109 | await bot.edit_message_reply_markup(call.from_user.id, call.message.message_id, reply_markup=kb)
110 |
111 |
112 | @track('Location regexp')
113 | async def handle_location(msg: Message, state: FSMContext):
114 | if msg.location:
115 | lat = msg.location.latitude
116 | lng = msg.location.longitude
117 | else:
118 | lat, lng = parse_coordinates(msg.text)
119 |
120 | resp, kb, location_name = await settings_service.save_location(lat, lng)
121 | await LocationNameState().waiting_for_location_name_state.set()
122 | await state.set_data({
123 | 'location_name': location_name,
124 | 'redirect_message': messages.location_saved.value
125 | })
126 | await msg.reply(resp, reply_markup=kb)
127 |
--------------------------------------------------------------------------------
/zmanim_bot/integrations/zmanim_models.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from datetime import date, datetime, time, timedelta
4 | from typing import List, Optional, Tuple
5 |
6 | from pydantic import BaseModel
7 |
8 | __all__ = [
9 | 'Settings',
10 | 'SimpleSettings',
11 | 'AsurBeMelachaDay',
12 | 'DafYomi',
13 | 'RoshChodesh',
14 | 'Shabbat',
15 | 'YomTov',
16 | 'Holiday',
17 | 'Fast',
18 | 'Zmanim',
19 | 'IsraelHolidays'
20 | ]
21 |
22 |
23 | LEHUMRA_MINUS_MINUTE_NAMES = [
24 | 'sof_zman_shema_ma', 'sof_zman_shema_gra', 'sof_zman_tefila_ma',
25 | 'sof_zman_tefila_gra', 'sunset', 'fast_start'
26 | ]
27 | LEHUMRA_TO_MIN = False
28 | LEHUMRA_TO_MAX = True
29 |
30 |
31 | def round_time_lehumra(dt: datetime, lehumra_to_max: bool) -> datetime:
32 | delta = timedelta(minutes=1 if lehumra_to_max else -1)
33 | return dt + delta
34 |
35 |
36 | class BaseModelWithZmanimLehumra(BaseModel):
37 | is_second_day: bool = False
38 |
39 | def apply_zmanim_lehumra(self):
40 | for name, value in self.dict(exclude={'settings'}).items():
41 | if isinstance(value, datetime):
42 | if \
43 | name in LEHUMRA_MINUS_MINUTE_NAMES or (
44 | name == 'candle_lighting' and not self.is_second_day and value.weekday() != 5
45 | or (self.is_second_day and value.weekday() == 4)
46 | ):
47 | lehumra = LEHUMRA_TO_MIN
48 | else:
49 | lehumra = LEHUMRA_TO_MAX
50 | value_lehumra = round_time_lehumra(value, lehumra)
51 | setattr(self, name, value_lehumra)
52 |
53 | def __init__(self, **kwargs):
54 | super().__init__(**kwargs)
55 | self.apply_zmanim_lehumra()
56 |
57 |
58 | class SimpleSettings(BaseModel):
59 | date_: Optional[date] = None
60 | jewish_date: Optional[str] = None
61 | holiday_name: Optional[str] = None
62 |
63 | class Config:
64 | fields = {'date_': 'date'}
65 |
66 |
67 | class Settings(SimpleSettings):
68 | cl_offset: Optional[int] = None
69 | havdala_opinion: Optional[str] = None
70 | coordinates: Optional[Tuple[float, float]] = None
71 | elevation: Optional[int] = None
72 | fast_name: Optional[str] = None
73 | yomtov_name: Optional[str] = None
74 |
75 |
76 | class Zmanim(BaseModelWithZmanimLehumra):
77 | settings: Settings
78 | alos: Optional[datetime] = None
79 | misheyakir_10_2: Optional[datetime] = None
80 | sunrise: Optional[datetime] = None
81 | sof_zman_shema_ma: Optional[datetime] = None
82 | sof_zman_shema_gra: Optional[datetime] = None
83 | sof_zman_tefila_ma: Optional[datetime] = None
84 | sof_zman_tefila_gra: Optional[datetime] = None
85 | chatzos: Optional[datetime] = None
86 | mincha_gedola: Optional[datetime] = None
87 | mincha_ketana: Optional[datetime] = None
88 | plag_mincha: Optional[datetime] = None
89 | sunset: Optional[datetime] = None
90 | tzeis_5_95_degrees: Optional[datetime] = None
91 | tzeis_8_5_degrees: Optional[datetime] = None
92 | tzeis_42_minutes: Optional[datetime] = None
93 | tzeis_72_minutes: Optional[datetime] = None
94 | chatzot_laila: Optional[datetime] = None
95 | astronomical_hour_ma: Optional[time] = None
96 | astronomical_hour_gra: Optional[time] = None
97 |
98 |
99 | class AsurBeMelachaDay(BaseModelWithZmanimLehumra):
100 | date: Optional[date] = None
101 | candle_lighting: Optional[datetime] = None
102 | havdala: Optional[datetime] = None
103 |
104 |
105 | class SecondAsurBeMelachaDay(AsurBeMelachaDay):
106 | is_second_day = True
107 |
108 |
109 | class Shabbat(AsurBeMelachaDay):
110 | settings: Settings
111 | torah_part: str = None
112 | late_cl_warning: bool = False
113 |
114 |
115 | class RoshChodesh(BaseModel):
116 | settings: SimpleSettings
117 | month_name: str
118 | days: List[date]
119 | duration: int
120 | molad: Tuple[datetime, int]
121 |
122 | class Config:
123 | json_encoders = {
124 | datetime: lambda d: d.isoformat(timespec='minutes')
125 | }
126 |
127 |
128 | class DafYomi(BaseModel):
129 | settings: SimpleSettings
130 | masehet: str
131 | daf: int
132 |
133 |
134 | class Holiday(BaseModel):
135 | settings: SimpleSettings
136 | date: date
137 |
138 |
139 | class IsraelHolidays(BaseModel):
140 | settings: SimpleSettings
141 | holiday_list: List[Tuple[str, date]]
142 |
143 |
144 | class YomTov(BaseModel):
145 | settings: Settings
146 |
147 | pesach_eating_chanetz_till: Optional[datetime] = None
148 | pesach_burning_chanetz_till: Optional[datetime] = None
149 |
150 | pre_shabbat: Optional[AsurBeMelachaDay] = None
151 | day_1: AsurBeMelachaDay
152 | day_2: Optional[SecondAsurBeMelachaDay] = None
153 | post_shabbat: Optional[SecondAsurBeMelachaDay] = None
154 | hoshana_rabba: Optional[date] = None
155 |
156 | pesach_part_2_day_1: Optional[AsurBeMelachaDay] = None
157 | pesach_part_2_day_2: Optional[SecondAsurBeMelachaDay] = None
158 | pesach_part_2_post_shabat: Optional[AsurBeMelachaDay] = None
159 |
160 |
161 | class Fast(BaseModelWithZmanimLehumra):
162 | settings: Settings
163 | moved_fast: Optional[bool] = False
164 | fast_start: Optional[datetime] = None
165 | chatzot: Optional[datetime] = None
166 | havdala_5_95_dgr: Optional[datetime] = None
167 | havdala_8_5_dgr: Optional[datetime] = None
168 | havdala_42_min: Optional[datetime] = None
169 |
--------------------------------------------------------------------------------
/zmanim_bot/service/settings_service.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Tuple
2 |
3 | from aiogram.types import InlineKeyboardMarkup, ReplyKeyboardMarkup
4 |
5 | from zmanim_bot import keyboards, texts
6 | from zmanim_bot.config import config
7 | from zmanim_bot.exceptions import ActiveLocationException
8 | from zmanim_bot.helpers import CallbackPrefixes
9 | from zmanim_bot.middlewares.i18n import i18n_
10 | from zmanim_bot.repository import bot_repository
11 | from zmanim_bot.service import zmanim_service
12 | from zmanim_bot.states import FeedbackState
13 | from zmanim_bot.texts.single import messages
14 |
15 |
16 | async def get_current_cl() -> Tuple[str, InlineKeyboardMarkup]:
17 | current_cl = await bot_repository.get_or_set_cl()
18 | kb = keyboards.inline.get_cl_settings_keyboard(current_cl)
19 | return texts.single.messages.settings_cl, kb
20 |
21 |
22 | async def set_cl(call_data: str) -> InlineKeyboardMarkup:
23 | cl = int(call_data.split(CallbackPrefixes.cl)[1])
24 | await bot_repository.get_or_set_cl(cl)
25 |
26 | kb = keyboards.inline.get_cl_settings_keyboard(cl)
27 | return kb
28 |
29 |
30 | async def get_current_havdala() -> Tuple[str, InlineKeyboardMarkup]:
31 | current_havdala = await bot_repository.get_or_set_havdala()
32 | kb = keyboards.inline.get_havdala_settings_keyboard(current_havdala)
33 | return texts.single.messages.settings_havdala, kb
34 |
35 |
36 | async def set_havdala(call_data: str) -> InlineKeyboardMarkup:
37 | havdala = call_data.split(CallbackPrefixes.havdala)[1]
38 | await bot_repository.get_or_set_havdala(havdala)
39 |
40 | kb = keyboards.inline.get_havdala_settings_keyboard(havdala)
41 | return kb
42 |
43 |
44 | async def get_current_zmanim() -> Tuple[str, InlineKeyboardMarkup]:
45 | current_zmanim = await bot_repository.get_or_set_zmanim()
46 | kb = keyboards.inline.get_zmanim_settings_keyboard(current_zmanim)
47 | return texts.single.messages.settings_zmanim, kb
48 |
49 |
50 | async def set_zmanim(call_data: str) -> InlineKeyboardMarkup:
51 | zman_name = call_data.split(CallbackPrefixes.zmanim)[1]
52 | current_zmanim = await bot_repository.get_or_set_zmanim()
53 | current_zmanim[zman_name] = not current_zmanim[zman_name]
54 |
55 | await bot_repository.get_or_set_zmanim(current_zmanim)
56 | kb = keyboards.inline.get_zmanim_settings_keyboard(current_zmanim)
57 | return kb
58 |
59 |
60 | async def get_current_omer() -> Tuple[str, InlineKeyboardMarkup]:
61 | current_omer = await bot_repository.get_or_set_omer_flag()
62 | kb = keyboards.inline.get_omer_kb(current_omer)
63 | return texts.single.messages.settings_omer, kb
64 |
65 |
66 | async def set_omer(call_data: str) -> InlineKeyboardMarkup:
67 | omer_flag = not bool(int(call_data.split(CallbackPrefixes.omer)[1]))
68 | zmanim = omer_flag and await zmanim_service.get_zmanim(must_have_8_5=True)
69 | await bot_repository.get_or_set_omer_flag(omer_flag, zmanim)
70 |
71 | kb = keyboards.inline.get_omer_kb(omer_flag)
72 | return kb
73 |
74 |
75 | async def get_current_format() -> Tuple[str, InlineKeyboardMarkup]:
76 | current_format = await bot_repository.get_or_set_processor_type()
77 | kb = keyboards.inline.get_format_options_kb(current_format)
78 | return texts.single.messages.settings_format, kb
79 |
80 |
81 | async def set_format(call_data: str) -> InlineKeyboardMarkup:
82 | current_format = call_data.split(CallbackPrefixes.format)[1]
83 | await bot_repository.get_or_set_processor_type(current_format)
84 |
85 | kb = keyboards.inline.get_format_options_kb(current_format)
86 | return kb
87 |
88 |
89 | async def set_language(lang: str):
90 | lang = config.LANGUAGE_SHORTCUTS[lang]
91 | await bot_repository.get_or_set_lang(lang)
92 | i18n_.ctx_locale.set(lang)
93 |
94 |
95 | async def get_locations_menu() -> Tuple[str, InlineKeyboardMarkup]:
96 | user = await bot_repository.get_or_create_user()
97 | kb = keyboards.inline.get_location_options_menu(user.location_list)
98 |
99 | return messages.settings_location, kb
100 |
101 |
102 | async def save_location(lat: float, lng: float) -> Tuple[str, ReplyKeyboardMarkup, str]:
103 | location = await bot_repository.get_or_set_location((lat, lng))
104 | kb = keyboards.menus.get_done_keyboard()
105 | resp = messages.custom_location_name_request.value.format(location.name)
106 | return resp, kb, location.name
107 |
108 |
109 | async def activate_location(location_name: str) -> Tuple[str, InlineKeyboardMarkup]:
110 | location_list = await bot_repository.activate_location(location_name)
111 | resp = messages.location_activated.value.format(location_name)
112 | kb = keyboards.inline.get_location_options_menu(location_list)
113 | return resp, kb
114 |
115 |
116 | async def delete_location(location_name: str) -> Tuple[str, Optional[InlineKeyboardMarkup]]:
117 | try:
118 | location_list = await bot_repository.delete_location(location_name)
119 | kb = keyboards.inline.get_location_options_menu(location_list)
120 | msg = messages.location_deleted
121 | except ActiveLocationException:
122 | kb = None
123 | msg = messages.unable_to_delete_active_location
124 |
125 | return msg, kb
126 |
127 |
128 | async def update_location_name(new_name: str, old_name: Optional[str]) -> InlineKeyboardMarkup:
129 | if not old_name:
130 | raise ValueError('There is no old location name!')
131 |
132 | location_list = await bot_repository.set_location_name(new_name=new_name, old_name=old_name)
133 | kb = keyboards.inline.get_location_options_menu(location_list)
134 | return kb
135 |
136 |
137 | async def init_report() -> Tuple[str, ReplyKeyboardMarkup]:
138 | await FeedbackState.waiting_for_feedback_text.set()
139 | kb = keyboards.menus.get_cancel_keyboard()
140 | return texts.single.messages.init_report, kb
141 |
--------------------------------------------------------------------------------
/zmanim_bot/keyboards/inline.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 | from typing import List, Optional, TypeVar
3 |
4 | from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
5 |
6 | from zmanim_bot.config import config
7 | from zmanim_bot.helpers import (CL_OFFET_OPTIONS, HAVDALA_OPINION_OPTIONS, CallbackPrefixes)
8 | from zmanim_bot.processors.text_utils import humanize_date
9 | from zmanim_bot.texts.single import buttons, zmanim
10 | from zmanim_bot.texts.single.buttons import zmanim_for_date_prefix
11 |
12 | Location = TypeVar('Location')
13 |
14 |
15 | BACK_BUTTON = InlineKeyboardButton(text=buttons.back, callback_data=CallbackPrefixes.location_menu_back)
16 |
17 | LOCATION_SEARCH_KB = InlineKeyboardMarkup()
18 | LOCATION_SEARCH_KB.add(InlineKeyboardButton(text=buttons.search_location, switch_inline_query_current_chat=''))
19 | LOCATION_SEARCH_KB.add(BACK_BUTTON)
20 |
21 | DONATE_KB = InlineKeyboardMarkup()
22 | DONATE_KB.row(*[
23 | InlineKeyboardButton(
24 | text=option,
25 | callback_data=f'{CallbackPrefixes.donate}{option}'
26 | ) for option in config.DONATE_OPTIONS
27 | ])
28 |
29 |
30 | def shorten_name(name: str, limit: int) -> str:
31 | if len(name) <= limit:
32 | return name
33 | return f'{name[:limit]}...'
34 |
35 |
36 | def merge_inline_keyboards(kb1: InlineKeyboardMarkup, kb2: InlineKeyboardMarkup) -> InlineKeyboardMarkup:
37 | for row in kb2.inline_keyboard:
38 | kb1.row(*row)
39 | return kb1
40 |
41 |
42 | def get_cl_settings_keyboard(current_value: int) -> InlineKeyboardMarkup:
43 | kb = InlineKeyboardMarkup()
44 | row_1 = []
45 | row_2 = []
46 |
47 | for i, option in enumerate(CL_OFFET_OPTIONS):
48 | button = InlineKeyboardButton(
49 | text=f'{option}' if option != current_value else f'✅ {option}',
50 | callback_data=f'{CallbackPrefixes.cl}{option}'
51 | )
52 | row_1.append(button) if i < 3 else row_2.append(button)
53 |
54 | kb.row(*row_1)
55 | kb.row(*row_2)
56 | return kb
57 |
58 |
59 | def get_havdala_settings_keyboard(current_value: str) -> InlineKeyboardMarkup:
60 | kb = InlineKeyboardMarkup()
61 |
62 | for option in HAVDALA_OPINION_OPTIONS:
63 | option_name = getattr(zmanim, option)
64 | button = InlineKeyboardButton(
65 | text=f'{option_name}' if option != current_value else f'✅ {option_name}',
66 | callback_data=f'{CallbackPrefixes.havdala}{option}'
67 | )
68 | kb.add(button)
69 | return kb
70 |
71 |
72 | def get_zman_button(name: str, status: bool) -> InlineKeyboardButton:
73 | button = InlineKeyboardButton(
74 | text=f'{"✅" if status else "❌"} {getattr(zmanim, name)}',
75 | callback_data=f'{CallbackPrefixes.zmanim}{name}'
76 | )
77 | return button
78 |
79 |
80 | def get_zmanim_settings_keyboard(zmanim_data: dict) -> InlineKeyboardMarkup:
81 | kb = InlineKeyboardMarkup()
82 |
83 | for name, status in zmanim_data.items():
84 | row = [get_zman_button(name, status)]
85 | kb.row(*row)
86 |
87 | return kb
88 |
89 |
90 | def get_omer_kb(status: bool) -> InlineKeyboardMarkup:
91 | kb = InlineKeyboardMarkup()
92 | kb.row(
93 | InlineKeyboardButton(
94 | text=f'✅ {buttons.settings_enabled}' if status
95 | else f'❌ {buttons.settings_disabled}',
96 | callback_data=f'{CallbackPrefixes.omer}{int(status)}'
97 | )
98 | )
99 | return kb
100 |
101 |
102 | def get_zmanim_by_date_buttons(dates: List[date]) -> InlineKeyboardMarkup:
103 | kb = InlineKeyboardMarkup()
104 | for d in dates:
105 | kb.row(InlineKeyboardButton(
106 | text=f'{zmanim_for_date_prefix} {humanize_date([d])}',
107 | callback_data=f'{CallbackPrefixes.zmanim_by_date}{d.isoformat()}'
108 | ))
109 | return kb
110 |
111 |
112 | # todo: refactor imports and fix typing
113 | def get_location_options_menu(location_list: List[Location]) -> InlineKeyboardMarkup:
114 | kb = InlineKeyboardMarkup()
115 |
116 | for location in location_list:
117 | status = '🔘' if location.is_active else '⚪️'
118 | kb.row(InlineKeyboardButton(
119 | text=f'{status} {location.name}',
120 | callback_data=f'{CallbackPrefixes.location_activate}{location.name}'
121 | ))
122 | kb.row(
123 | InlineKeyboardButton(text='✏️', callback_data=f'{CallbackPrefixes.location_rename}{location.name}'),
124 | InlineKeyboardButton(text='❌', callback_data=f'{CallbackPrefixes.location_delete}{location.name}')
125 | )
126 |
127 | kb.add(BACK_BUTTON)
128 | return kb
129 |
130 |
131 | def get_location_management_kb() -> InlineKeyboardMarkup:
132 | kb = InlineKeyboardMarkup()
133 | kb.add(InlineKeyboardButton(
134 | text=buttons.manage_locations, callback_data=CallbackPrefixes.location_namage
135 | ))
136 | kb.add(InlineKeyboardButton(
137 | text=buttons.add_location, callback_data=CallbackPrefixes.location_add
138 | ))
139 | return kb
140 |
141 |
142 | # todo: refactor imports and fix typing
143 | def get_location_variants_menu(
144 | locations: List[Location],
145 | current_loc: Location,
146 | callback_prefix: str,
147 | date_: Optional[date] = None
148 | ) -> Optional[InlineKeyboardMarkup]:
149 | if len(locations) < 2:
150 | return
151 |
152 | kb = InlineKeyboardMarkup()
153 | index = locations.index(current_loc)
154 |
155 | if date_:
156 | date_str = f':{date_}'
157 | else:
158 | date_str = ''
159 |
160 | if len(locations) == 2:
161 | new_index = int(not index)
162 | if current_loc.is_active:
163 | text = f'📍 {locations[new_index].name} ▶️'
164 | else:
165 | text = f'◀️ {locations[new_index].name} 📍'
166 |
167 | kb.row(InlineKeyboardButton(
168 | text=text,
169 | callback_data=f''
170 | f'{callback_prefix}'
171 | f'{locations[new_index].lat},{locations[new_index].lng}'
172 | f'{date_str}'
173 | ))
174 | else:
175 | loc_len = len(locations)
176 | prev_index = ((index - 1) % loc_len + loc_len) % loc_len
177 | next_index = ((index + 1) % loc_len + loc_len) % loc_len
178 |
179 | kb.row(
180 | InlineKeyboardButton(
181 | text=f'◀️ {shorten_name(locations[prev_index].name, 13)} 📍',
182 | callback_data=f''
183 | f'{callback_prefix}'
184 | f'{locations[prev_index].lat},{locations[prev_index].lng}'
185 | f'{date_str}'
186 | ),
187 | InlineKeyboardButton(
188 | text=f'📍 {shorten_name(locations[next_index].name, 13)} ▶️',
189 | callback_data=f''
190 | f'{callback_prefix}'
191 | f'{locations[next_index].lat},{locations[next_index].lng}'
192 | f'{date_str}'
193 | )
194 | )
195 |
196 | return kb
197 |
198 |
199 | def get_format_options_kb(current_type: str) -> InlineKeyboardMarkup:
200 | kb = InlineKeyboardMarkup()
201 | row = []
202 | for processor_type, button_name in buttons.processor_types.items():
203 | button = InlineKeyboardButton(
204 | text=f'{button_name}' if processor_type != current_type else f'✅ {button_name}',
205 | callback_data=f'{CallbackPrefixes.format}{processor_type}'
206 | )
207 | row.append(button)
208 | kb.row(*row)
209 | return kb
210 |
--------------------------------------------------------------------------------
/zmanim_bot/repository/_storage.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime as dt
2 | from typing import List, Optional, Tuple
3 |
4 | import aiogram_metrics
5 | from aiogram import types
6 |
7 | from zmanim_bot.config import config
8 | from zmanim_bot.exceptions import (
9 | ActiveLocationException,
10 | MaxLocationLimitException,
11 | NoLanguageException, NoLocationException,
12 | NonUniqueLocationException,
13 | NonUniqueLocationNameException
14 | )
15 | from zmanim_bot.integrations.geo_client import get_location_name
16 | from zmanim_bot.misc import db_engine
17 | from .models import Location, User, UserInfo, ZmanimSettings
18 |
19 | __all__ = [
20 | '_get_or_create_user',
21 | 'get_cl_offset',
22 | 'get_zmanim',
23 | 'get_havdala',
24 | 'get_lang',
25 | 'get_location',
26 | 'set_zmanim',
27 | 'set_cl',
28 | 'set_havdala',
29 | 'set_lang',
30 | 'set_location',
31 | 'do_set_location_name',
32 | 'do_activate_location',
33 | 'do_delete_location',
34 | 'get_processor_type',
35 | 'set_processor_type',
36 | 'get_omer_flag',
37 | 'set_omer_flag',
38 | ]
39 |
40 | from ..integrations.open_topo_data_client import get_elevation
41 |
42 | MAX_LOCATION_NAME_SIZE = 27
43 |
44 |
45 | def validate_location_coordinates(location: Location, locations: List[Location]):
46 | if len(locations) >= config.LOCATION_NUMBER_LIMIT:
47 | raise MaxLocationLimitException
48 |
49 | for loc in locations:
50 | if loc.lat == location.lat and loc.lng == location.lng:
51 | raise NonUniqueLocationException
52 |
53 |
54 | def validate_location_name(new_name: str, locations: List[Location]):
55 | for loc in locations:
56 | if loc.name == new_name:
57 | raise NonUniqueLocationNameException
58 |
59 |
60 | async def _get_or_create_user(tg_user: types.User) -> User:
61 | user = await db_engine.find_one(User, User.user_id == tg_user.id)
62 |
63 | if not user:
64 | user = User(
65 | user_id=tg_user.id,
66 | personal_info=UserInfo(
67 | first_name=tg_user.first_name,
68 | last_name=tg_user.last_name,
69 | username=tg_user.username
70 | )
71 | )
72 | await db_engine.save(user)
73 | aiogram_metrics.manual_track('New user has joined')
74 |
75 | elif user.personal_info.first_name != tg_user.first_name or \
76 | user.personal_info.last_name != tg_user.last_name or \
77 | user.personal_info.username != tg_user.username:
78 | user.personal_info = UserInfo(
79 | first_name=tg_user.first_name,
80 | last_name=tg_user.last_name,
81 | username=tg_user.username
82 | )
83 |
84 | user.meta.last_seen_at = dt.now()
85 | await db_engine.save(user)
86 |
87 | return user
88 |
89 |
90 | async def get_lang(tg_user: types.User) -> Optional[str]:
91 | user = await _get_or_create_user(tg_user)
92 | lang = user.language
93 | if not lang:
94 | raise NoLanguageException
95 | return lang
96 |
97 |
98 | async def set_lang(tg_user: types.User, lang: str):
99 | user = await _get_or_create_user(tg_user)
100 | user.language = lang
101 | await db_engine.save(user)
102 |
103 |
104 | async def get_location(tg_user: types.User) -> Tuple[float, float]:
105 | user = await _get_or_create_user(tg_user)
106 | location = list(filter(lambda loc: loc.is_active is True, user.location_list))
107 | if not location:
108 | raise NoLocationException
109 |
110 | return location[0].lat, location[0].lng
111 |
112 |
113 | async def set_location(tg_user: types.User, location: Tuple[float, float]) -> Location:
114 | user = await _get_or_create_user(tg_user)
115 | location_name = await get_location_name(location[0], location[1], user.language)
116 | if len(location_name) > MAX_LOCATION_NAME_SIZE:
117 | location_name = f'{location_name[:MAX_LOCATION_NAME_SIZE]}...'
118 |
119 | elevation = await get_elevation(*location)
120 |
121 | location_obj = Location(
122 | lat=location[0],
123 | lng=location[1],
124 | name=location_name,
125 | is_active=True,
126 | elevation=elevation
127 | )
128 | validate_location_coordinates(location_obj, user.location_list)
129 |
130 | for i in range(len(user.location_list)):
131 | user.location_list[i].is_active = False
132 |
133 | user.location_list.append(location_obj)
134 | await db_engine.save(user)
135 | return location_obj
136 |
137 |
138 | async def do_set_location_name(tg_user: types.User, new_name: str, old_name: str) -> List[Location]:
139 | user = await _get_or_create_user(tg_user)
140 | location = list(filter(lambda l: l.name == old_name, user.location_list))
141 |
142 | if len(location) == 0:
143 | raise ValueError('Unknown old location name!')
144 | validate_location_name(new_name, user.location_list)
145 |
146 | if len(new_name) > MAX_LOCATION_NAME_SIZE:
147 | new_name = f'{new_name[:MAX_LOCATION_NAME_SIZE]}...'
148 |
149 | location = location[0]
150 | location.name = new_name
151 |
152 | location_index = user.location_list.index(location)
153 | user.location_list[location_index] = location
154 | await db_engine.save(user)
155 | return user.location_list
156 |
157 |
158 | async def do_activate_location(tg_user: types.User, name: str) -> List[Location]:
159 | user = await _get_or_create_user(tg_user)
160 |
161 | if len(user.location_list) == 1:
162 | return user.location_list
163 |
164 | for location in user.location_list:
165 | location.is_active = False
166 |
167 | location = list(filter(lambda l: l.name == name, user.location_list))
168 | if len(location) == 0:
169 | raise ValueError('Unknown location name!')
170 |
171 | index = user.location_list.index(location[0])
172 | user.location_list[index].is_active = True
173 |
174 | await db_engine.save(user)
175 | return user.location_list
176 |
177 |
178 | async def do_delete_location(tg_user: types.User, name: str) -> List[Location]:
179 | user = await _get_or_create_user(tg_user)
180 | location = list(filter(lambda l: l.name == name, user.location_list))
181 |
182 | if len(location) == 0:
183 | raise ValueError('Unknown location name!')
184 | if location[0].is_active:
185 | raise ActiveLocationException()
186 |
187 | user.location_list.remove(location[0])
188 | await db_engine.save(user)
189 | return user.location_list
190 |
191 |
192 | async def get_cl_offset(tg_user: types.User) -> int:
193 | user = await _get_or_create_user(tg_user)
194 | return user.cl_offset
195 |
196 |
197 | async def set_cl(tg_user: types.User, cl: int):
198 | user = await _get_or_create_user(tg_user)
199 | user.cl_offset = cl
200 | await db_engine.save(user)
201 |
202 |
203 | async def get_havdala(tg_user: types.User) -> str:
204 | user = await _get_or_create_user(tg_user)
205 | return user.havdala_opinion
206 |
207 |
208 | async def set_havdala(tg_user: types.User, havdala: str):
209 | user = await _get_or_create_user(tg_user)
210 | user.havdala_opinion = havdala
211 | await db_engine.save(user)
212 |
213 |
214 | async def get_zmanim(tg_user: types.User) -> dict:
215 | user = await _get_or_create_user(tg_user)
216 | return user.zmanim_settings.dict()
217 |
218 |
219 | async def set_zmanim(tg_user: types.User, zmanim: dict):
220 | zmanim_obj = ZmanimSettings(**zmanim)
221 | user = await _get_or_create_user(tg_user)
222 | user.zmanim_settings = zmanim_obj
223 | await db_engine.save(user)
224 |
225 |
226 | async def get_processor_type(tg_user: types.User) -> str:
227 | user = await _get_or_create_user(tg_user)
228 | return user.processor_type
229 |
230 |
231 | async def set_processor_type(tg_user: types.User, processor_type: str):
232 | user = await _get_or_create_user(tg_user)
233 | user.processor_type = processor_type
234 | await db_engine.save(user)
235 |
236 |
237 | async def get_omer_flag(tg_user: types.User) -> bool:
238 | user = await _get_or_create_user(tg_user)
239 | return user.omer.is_enabled
240 |
241 |
242 | async def set_omer_flag(tg_user: types.User, omer_flag: bool, omer_time: Optional[str] = None):
243 | user = await _get_or_create_user(tg_user)
244 |
245 | user.omer.is_enabled = omer_flag
246 | user.omer.notification_time = omer_time
247 |
248 | await db_engine.save(user)
249 |
--------------------------------------------------------------------------------
/zmanim_bot/texts/single/names.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 |
3 | from babel.support import LazyProxy
4 |
5 | from zmanim_bot.middlewares.i18n import lazy_gettext as _
6 |
7 | MONTH_NAMES_GENETIVE: Dict[int, LazyProxy] = {
8 | # NOTE Genative month name
9 | 1: _('January'),
10 | # NOTE Genative month name
11 | 2: _('February'),
12 | # NOTE Genative month name
13 | 3: _('March'),
14 | # NOTE Genative month name
15 | 4: _('April'),
16 | # NOTE Genative month name
17 | 5: _('May'),
18 | # NOTE Genative month name
19 | 6: _('June'),
20 | # NOTE Genative month name
21 | 7: _('July'),
22 | # NOTE Genative month name
23 | 8: _('August'),
24 | # NOTE Genative month name
25 | 9: _('September'),
26 | # NOTE Genative month name
27 | 10: _('October'),
28 | # NOTE Genative month name
29 | 11: _('November'),
30 | # NOTE Genative month name
31 | 12: _('December'),
32 |
33 | }
34 |
35 | WEEKDAYS: Dict[int, LazyProxy] = {
36 | 0: _('Monday'),
37 | 1: _('Tuesday'),
38 | 2: _('Wednesday'),
39 | 3: _('Thursday'),
40 | 4: _('Friday'),
41 | 5: _('Saturday'),
42 | 6: _('Sunday')
43 | }
44 |
45 | # Titles
46 | title_daf_yomi = _('DAF YOMI')
47 | title_rosh_chodesh = _('ROSH CHODESH')
48 | title_shabbath = _('SHABBAT')
49 | title_zmanim = _('ZMANIM')
50 | FASTS_TITLES = {
51 | 'fast_gedalia': _('FAST OF GEDALIAH'),
52 | 'fast_10_teves': _('10th OF TEVET'),
53 | 'fast_esther': _('FAST OF ESTHER'),
54 | 'fast_17_tammuz': _('17th OF TAMMUZ'),
55 | 'fast_9_av': _('9th OF AV')
56 | }
57 | HOLIDAYS_TITLES = {
58 | 'chanukah': _('CHANUKAH'),
59 | 'tu_bi_shvat': _('TU BI-SHVAT'),
60 | 'purim': _('PURIM'),
61 | 'lag_baomer': _('LAG BA-OMER'),
62 | 'israel_holidays': _('ISRAEL HOLIDAYS')
63 | }
64 | YOMTOVS_TITLES = {
65 | 'rosh_hashana': _('ROSH HA-SHANA'),
66 | 'yom_kippur': _('YOM KIPUR'),
67 | 'succot': _('SUCCOT'),
68 | 'shmini_atzeres': _('SHMINI ATZERET'),
69 | 'pesach': _('PESACH'),
70 | 'shavuot': _('SHAVUOT')
71 | }
72 |
73 | shabbat = _('shabbat')
74 |
75 | TORAH_PARTS = {
76 | 'bereishis': _('Bereshit'),
77 | 'noach': _('Noach'),
78 | 'lech_lecha': _('Lech-Lecha'),
79 | 'vayeira': _('Vayeira'),
80 | 'chayei_sarah': _('Chayei Sarah'),
81 | 'toldos': _('Toledot'),
82 | 'vayeitzei': _('Vayetze'),
83 | 'vayishlach': _('Vayishlach'),
84 | 'vayeishev': _('Vayeshev'),
85 | 'mikeitz': _('Miketz'),
86 | 'vayigash': _('Vayigash'),
87 | 'vayechi': _('Vayechi'),
88 | 'shemos': _('Shemot'),
89 | 'vaeirah': _('Va\'eira'),
90 | 'bo': _('Bo'),
91 | 'beshalach': _('Beshalach'),
92 | 'yisro': _('Yitro'),
93 | 'mishpatim': _('Mishpatim'),
94 | 'terumah': _('Terumah'),
95 | 'tetzaveh': _('Tetzaveh'),
96 | 'ki_sisa': _('Ki Tisa'),
97 | 'vayakheil': _('Vayakhel'),
98 | 'pekudei': _('Pekudei'),
99 | 'vayikra': _('Vayikra'),
100 | 'tzav': _('Tzav'),
101 | 'shemini': _('Shemini'),
102 | 'tazria': _('Tazria'),
103 | 'metzora': _('Metzora'),
104 | 'acharei': _('Acharei Mot'),
105 | 'kedoshim': _('Kedoshim'),
106 | 'emor': _('Emor'),
107 | 'behar': _('Behar'),
108 | 'bechukosai': _('Bechukotai'),
109 | 'bamidbar': _('Bamidbar'),
110 | 'naso': _('Naso'),
111 | 'behaalosecha': _('Behaalotecha'),
112 | 'shelach': _('Shlach'),
113 | 'korach': _('Korach'),
114 | 'chukas': _('Chukat'),
115 | 'balak': _('Balak'),
116 | 'pinchas': _('Pinchas'),
117 | 'matos': _('Matot'),
118 | 'masei': _('Masei'),
119 | 'devarim': _('Devarim'),
120 | 'vaeschanan': _('Va\'etchanan'),
121 | 'eikev': _('Eikev'),
122 | 'reei': _('Re\'eh'),
123 | 'shoftim': _('Shoftim'),
124 | 'ki_seitzei': _('Ki Teitzei'),
125 | 'ki_savo': _('Ki Tavo'),
126 | 'nitzavim': _('Nitzavim'),
127 | 'vayeilech': _('Vayelech'),
128 | 'haazinu': _('Haazinu'),
129 | 'vezos_haberacha': _('V\'Zot HaBerachah'),
130 |
131 | 'vayakheil - pikudei': _('Vayakhel - Pekudei'),
132 | 'tazria - metzora': _('Tazria - Metzora'),
133 | 'acharei - kedoshim': _('Acharei Mot - Kedoshim'),
134 | 'behar - bechukosai': _('Behar - Bechukotai'),
135 | 'chukas - balak': _('Chukat - Balak'),
136 | 'matos - masei': _('Matot - Masei'),
137 | 'nitzavim - vayeilech': _('Nitzavim - Vayelech'),
138 |
139 | 'rosh_hashana': _('Shabbat Rosh ha-Shana'),
140 | 'yom_kippur': _('Shabbat Yom Kippur'),
141 | 'succos': _('Shabat Succot'),
142 | 'chol_hamoed_succos': _('Shabbat Chol ha-moed Succot'),
143 | 'hoshana_rabbah': _('Shabbat Chol ha-moed Succot'),
144 | 'shemini_atzeres': _('Shabbat Shmini Atzeret'),
145 | 'simchas_torah': _('Shabbat Simchat Torah'),
146 | 'pesach': _('Shabbat Pesach'),
147 | 'chol_hamoed_pesach': _('Shabbat Chol ha-moed Pesach'),
148 | 'shavuos': _('Shabbat Shavuot')
149 | }
150 |
151 | GEMARA_BOOKS = {
152 | 'berachos': _('Brachot'),
153 | # NOTE masehet name; It is very important to add space after the month name!!
154 | 'shabbos': _('Shabbat '),
155 | 'eruvin': _('Eruvin'),
156 | 'pesachim': _('Pesachim'),
157 | 'shekalim': _('Shekalim'),
158 | 'yoma': _('Yoma'),
159 | 'sukkah': _('Sukah'),
160 | 'beitzah': _('Beitzah'),
161 | 'rosh_hashanah': _('Rosh Hashana'),
162 | 'taanis': _('Taanit'),
163 | 'megillah': _('Megilah'),
164 | 'moed_katan': _('Moed Katan'),
165 | 'chagigah': _('Chagigah'),
166 | 'yevamos': _('Yevamot'),
167 | 'kesubos': _('Kesuvot'),
168 | 'nedarim': _('Nedarim'),
169 | 'nazir': _('Nazir'),
170 | 'sotah': _('Sotah'),
171 | 'gitin': _('Gitin'),
172 | 'kiddushin': _('Kidushin'),
173 | 'bava_kamma': _('Bava Kama'),
174 | 'bava_metzia': _('Bava Metzia'),
175 | 'bava_basra': _('Bava Batra'),
176 | 'sanhedrin': _('Sanhedrin'),
177 | 'makkos': _('Makot'),
178 | 'shevuos': _('Shevuot'),
179 | 'avodah_zarah': _('Avodah Zarah'),
180 | 'horiyos': _('Horayot'),
181 | 'zevachim': _('Zevachim'),
182 | 'menachos': _('Menachot'),
183 | 'chullin': _('Chulin'),
184 | 'bechoros': _('Bechorot'),
185 | 'arachin': _('Erchin'),
186 | 'temurah': _('Temurah'),
187 | 'kerisos': _('Kerisot'),
188 | 'meilah': _('Meilah'),
189 | 'kinnim': _('Kinnim'),
190 | 'tamid': _('Tamid'),
191 | 'midos': _('Midot'),
192 | 'niddah': _('Nidah')
193 | }
194 |
195 | JEWISH_MONTHS = {
196 | # NOTE It is very important to add space after the month name!!
197 | 'nissan': _('Nisan '),
198 | # NOTE It is very important to add space after the month name!!
199 | 'iyar': _('Iyar '),
200 | # NOTE It is very important to add space after the month name!!
201 | 'sivan': _('Sivan '),
202 | # NOTE It is very important to add space after the month name!!
203 | 'tammuz': _('Tammuz '),
204 | # NOTE It is very important to add space after the month name!!
205 | 'av': _('Av '),
206 | # NOTE It is very important to add space after the month name!!
207 | 'elul': _('Elul '),
208 | # NOTE It is very important to add space after the month name!!
209 | 'tishrei': _('Tishrei '),
210 | # NOTE It is very important to add space after the month name!!
211 | 'cheshvan': _('Cheshvan '),
212 | # NOTE It is very important to add space after the month name!!
213 | 'kislev': _('Kislev '),
214 | # NOTE It is very important to add space after the month name!!
215 | 'teves': _('Tevet '),
216 | # NOTE It is very important to add space after the month name!!
217 | 'shevat': _('Shevat '),
218 | # NOTE It is very important to add space after the month name!!
219 | 'adar': _('Adar '),
220 | # NOTE It is very important to add space after the month name!!
221 | 'adar_ii': _('Adar II '),
222 | }
223 |
224 | JEWISH_MONTHS_GENITIVE = {
225 | # NOTE Genative month name
226 | 'nissan': _('Nisan'),
227 | # NOTE Genative month name
228 | 'iyar': _('Iyar'),
229 | # NOTE Genative month name
230 | 'sivan': _('Sivan'),
231 | # NOTE Genative month name
232 | 'tammuz': _('Tammuz'),
233 | # NOTE Genative month name
234 | 'av': _('Av'),
235 | # NOTE Genative month name
236 | 'elul': _('Elul'),
237 | # NOTE Genative month name
238 | 'tishrei': _('Tishrei'),
239 | # NOTE Genative month name
240 | 'cheshvan': _('Cheshvan'),
241 | # NOTE Genative month name
242 | 'kislev': _('Kislev'),
243 | # NOTE Genative month name
244 | 'teves': _('Tevet'),
245 | # NOTE Genative month name
246 | 'shevat': _('Shevat'),
247 | # NOTE Genative month name
248 | 'adar': _('Adar'),
249 | # NOTE Genative month name
250 | 'adar_ii': _('Adar II'),
251 | }
252 |
--------------------------------------------------------------------------------
/zmanim_bot/handlers/__init__.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import ContentType
2 |
3 | from zmanim_bot import exceptions as e
4 | from zmanim_bot.admin.states import AdminReportResponse
5 | from zmanim_bot.config import config
6 | from zmanim_bot.helpers import CallbackPrefixes, LOCATION_PATTERN
7 | from zmanim_bot.misc import dp
8 | from zmanim_bot.states import FeedbackState, ConverterGregorianDateState, ConverterJewishDateState, \
9 | LocationNameState, ZmanimGregorianDateState
10 | from . import admin, converter, errors, festivals, forms, main, menus, settings, \
11 | incorrect_text_handler, reset_handler, payments, geolocation
12 | from ..texts.single import buttons
13 |
14 | __all__ = ['register_handlers']
15 |
16 |
17 | def register_handlers():
18 | # errors
19 | dp.register_errors_handler(errors.empty_exception_handler, exception=e.UnsupportedChatTypeException)
20 | dp.register_errors_handler(errors.access_denied_exception_handler, exception=e.AccessDeniedException)
21 | dp.register_errors_handler(errors.no_location_exception_handler, exception=e.NoLocationException)
22 | dp.register_errors_handler(errors.incorrect_location_exception_handler, exception=e.IncorrectLocationException)
23 | dp.register_errors_handler(errors.non_unique_location_exception_handler, exception=e.NonUniqueLocationException)
24 | dp.register_errors_handler(errors.non_unique_location_name_exception_handler, exception=e.NonUniqueLocationNameException)
25 | dp.register_errors_handler(errors.location_limit_exception_handler, exception=e.MaxLocationLimitException)
26 | dp.register_errors_handler(errors.no_language_exception_handler, exception=e.NoLanguageException)
27 | dp.register_errors_handler(errors.gregorian_date_exception_handler, exception=e.IncorrectGregorianDateException)
28 | dp.register_errors_handler(errors.jewish_date_exception_handler, exception=e.IncorrectJewishDateException)
29 | dp.register_errors_handler(errors.polar_coordinates_exception_handler, exception=e.PolarCoordinatesException)
30 | dp.register_errors_handler(errors.unknown_processor_exception_handler, exception=e.UnknownProcessorException)
31 | dp.register_errors_handler(errors.main_errors_handler, exception=Exception)
32 |
33 | # reset handlers whech clears any state (should be first!)
34 | dp.register_message_handler(reset_handler.handle_start, commands=['start'])
35 | dp.register_message_handler(reset_handler.handle_back, text=[buttons.back, buttons.cancel], state="*")
36 |
37 | # admin
38 | dp.register_message_handler(admin.handle_report_response, lambda msg: msg.from_user.id in config.REPORT_ADMIN_LIST, state=AdminReportResponse.waiting_for_response_text)
39 | dp.register_message_handler(admin.handle_done_report, text=buttons.done, state=AdminReportResponse.waiting_for_payload)
40 | dp.register_message_handler(admin.handle_report_payload, content_types=ContentType.ANY, state=AdminReportResponse.waiting_for_payload)
41 | dp.register_callback_query_handler(admin.handle_report, lambda call: call.from_user.id in config.REPORT_ADMIN_LIST, text_startswith=CallbackPrefixes.report)
42 |
43 | # forms/states
44 | dp.register_message_handler(forms.handle_report, state=FeedbackState.waiting_for_feedback_text)
45 | dp.register_message_handler(forms.handle_done_report, text=buttons.done, state=FeedbackState.waiting_for_payload)
46 | dp.register_message_handler(forms.handle_report_payload, content_types=ContentType.ANY, state=FeedbackState.waiting_for_payload)
47 | dp.register_message_handler(forms.handle_converter_gregorian_date, state=ConverterGregorianDateState.waiting_for_gregorian_date)
48 | dp.register_message_handler(forms.handle_converter_jewish_date, state=ConverterJewishDateState.waiting_for_jewish_date)
49 | dp.register_message_handler(forms.handle_zmanim_gregorian_date, state=ZmanimGregorianDateState.waiting_for_gregorian_date)
50 | dp.register_message_handler(forms.handle_location_name, state=LocationNameState.waiting_for_location_name_state)
51 |
52 | # main
53 | dp.register_message_handler(main.handle_zmanim, text=buttons.mm_zmanim)
54 | dp.register_message_handler(main.handle_zmanim_by_date, text=buttons.mm_zmanim_by_date)
55 | dp.register_message_handler(main.handle_shabbat, text=buttons.mm_shabbat)
56 | dp.register_message_handler(main.handle_daf_yomi, text=buttons.mm_daf_yomi)
57 | dp.register_message_handler(main.handle_rosh_chodesh, text=buttons.mm_rh)
58 |
59 | dp.register_callback_query_handler(main.handle_zmanim_by_date_callback, text_startswith=CallbackPrefixes.zmanim_by_date)
60 | dp.register_callback_query_handler(main.handle_update_zmanim, text_startswith=CallbackPrefixes.update_zmanim)
61 | dp.register_callback_query_handler(main.handle_update_shabbat, text_startswith=CallbackPrefixes.update_shabbat)
62 |
63 | # menus
64 | dp.register_message_handler(menus.handle_holidays_menu, text=[buttons.mm_holidays, buttons.hom_main])
65 | dp.register_message_handler(menus.handle_more_holidays_menu, text=buttons.hom_more)
66 | dp.register_message_handler(menus.handle_fasts_menu, text=buttons.mm_fasts)
67 | dp.register_message_handler(menus.handle_settings_menu, commands=['settings'])
68 | dp.register_message_handler(menus.handle_settings_menu, text=buttons.mm_settings)
69 | dp.register_message_handler(menus.handle_donate, text=buttons.mm_donate)
70 | dp.register_message_handler(menus.handle_donate, commands=['donate'])
71 |
72 | # festivals
73 | dp.register_message_handler(festivals.handle_fast, text=buttons.FASTS)
74 | dp.register_message_handler(festivals.handle_yom_tov, text=buttons.YOMTOVS)
75 | dp.register_message_handler(festivals.handle_holiday, text=buttons.HOLIDAYS)
76 |
77 | dp.register_callback_query_handler(festivals.handle_fast_update, text_startswith=CallbackPrefixes.update_fast)
78 | dp.register_callback_query_handler(festivals.handle_yom_tov_update, text_startswith=CallbackPrefixes.update_yom_tov)
79 |
80 | # converter
81 | dp.register_message_handler(converter.handle_converter_entry, commands=['converter_api'])
82 | dp.register_message_handler(converter.handle_converter_entry, text=buttons.mm_converter)
83 | dp.register_message_handler(converter.start_greg_to_jew_converter, text=buttons.conv_greg_to_jew)
84 | dp.register_message_handler(converter.start_jew_to_greg_converter, text=buttons.conv_jew_to_greg)
85 |
86 | # settings
87 | dp.register_message_handler(settings.settings_menu_cl, text=buttons.sm_candle)
88 | dp.register_message_handler(settings.settings_menu_havdala, text=buttons.sm_havdala)
89 | dp.register_message_handler(settings.settings_menu_zmanim, text=buttons.sm_zmanim)
90 | dp.register_message_handler(settings.handle_omer_settings, text=buttons.sm_omer)
91 | dp.register_message_handler(settings.handle_language_request, commands=['language'])
92 | dp.register_message_handler(settings.handle_language_request, text=buttons.sm_lang)
93 | dp.register_message_handler(settings.set_language, text=config.LANGUAGE_LIST)
94 | dp.register_message_handler(settings.help_menu_report, commands=['report'])
95 | dp.register_message_handler(settings.help_menu_report, text=buttons.hm_report)
96 | dp.register_message_handler(settings.handle_format_settings, text=buttons.sm_format)
97 |
98 | dp.register_callback_query_handler(settings.set_cl, text_startswith=CallbackPrefixes.cl)
99 | dp.register_callback_query_handler(settings.set_havdala, text_startswith=CallbackPrefixes.havdala)
100 | dp.register_callback_query_handler(settings.set_zmanim, text_startswith=CallbackPrefixes.zmanim)
101 | dp.register_callback_query_handler(settings.set_omer, text_startswith=CallbackPrefixes.omer)
102 | dp.register_callback_query_handler(settings.set_format, text_startswith=CallbackPrefixes.format)
103 |
104 | # location
105 | dp.register_message_handler(geolocation.location_settings, commands=['location'])
106 | dp.register_message_handler(geolocation.location_settings, text=buttons.sm_location)
107 | dp.register_message_handler(geolocation.handle_location, regexp=LOCATION_PATTERN)
108 | dp.register_message_handler(geolocation.handle_location, content_types=[ContentType.LOCATION, ContentType.VENUE])
109 |
110 | dp.register_callback_query_handler(geolocation.add_new_location, text_startswith=CallbackPrefixes.location_add)
111 | dp.register_callback_query_handler(geolocation.manage_saved_locations, text_startswith=CallbackPrefixes.location_namage)
112 | dp.register_callback_query_handler(geolocation.back_to_location_settings, text_startswith=CallbackPrefixes.location_menu_back)
113 | dp.register_callback_query_handler(geolocation.handle_activate_location, text_startswith=CallbackPrefixes.location_activate)
114 | dp.register_callback_query_handler(geolocation.init_location_rename, text_startswith=CallbackPrefixes.location_rename)
115 | dp.register_callback_query_handler(geolocation.handle_delete_location, text_startswith=CallbackPrefixes.location_delete)
116 |
117 | dp.register_inline_handler(geolocation.handle_inline_location_query)
118 |
119 | # payments
120 | dp.register_callback_query_handler(payments.handle_donate, text_startswith=CallbackPrefixes.donate)
121 | dp.register_pre_checkout_query_handler(payments.handle_pre_checkout)
122 | dp.register_message_handler(payments.on_success_payment, content_types=[ContentType.SUCCESSFUL_PAYMENT])
123 | dp.register_message_handler(payments.on_success_payment, content_types=[ContentType])
124 |
125 | # unknown messages (SHOULD BE LAST!)
126 | dp.register_message_handler(incorrect_text_handler.handle_incorrect_text)
127 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/text/composer.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime as dt, date, timedelta
2 | from typing import Tuple, Optional, Union
3 |
4 | from aiogram.types import InlineKeyboardMarkup
5 | from aiogram.utils.markdown import hbold, hitalic, hcode
6 | from babel.support import LazyProxy
7 |
8 | from zmanim_bot import texts
9 | from zmanim_bot.exceptions import PolarCoordinatesException
10 | from zmanim_bot.integrations.zmanim_models import Zmanim, Shabbat, RoshChodesh, DafYomi, Holiday, \
11 | IsraelHolidays, Fast, YomTov, AsurBeMelachaDay
12 | from zmanim_bot.keyboards.inline import get_zmanim_by_date_buttons
13 | from zmanim_bot.middlewares.i18n import gettext as _
14 | from zmanim_bot.processors.text_utils import humanize_date, parse_jewish_date, humanize_time
15 | from zmanim_bot.texts.plural import units
16 | from zmanim_bot.texts.single import names, helpers, headers
17 |
18 |
19 | def _emojize_holiday_title(holiday_type: str) -> str:
20 | emojis = {
21 | 'chanukah': '🕎',
22 | 'tu_bi_shvat': '🌳',
23 | 'purim': '🎭',
24 | 'lag_baomer': '🔥',
25 | 'israel_holidays': '🇮🇱'
26 | }
27 | resp = f'{emojis[holiday_type]} {names.HOLIDAYS_TITLES[holiday_type]}'
28 | return resp
29 |
30 |
31 | def _emojize_yom_tov_title(yom_tov_type: str) -> str:
32 | emojis = {
33 | 'rosh_hashana': '🍯',
34 | 'yom_kippur': '⚖️',
35 | 'succot': '⛺️',
36 | 'shmini_atzeres': '🏁',
37 | 'pesach': '🍷',
38 | 'shavuot': '🌾'
39 | }
40 | resp = f'{emojis[yom_tov_type]} {names.YOMTOVS_TITLES[yom_tov_type]}'
41 | return resp
42 |
43 |
44 | def _compose_response(
45 | title: Union[LazyProxy, str],
46 | content: str,
47 | date_str: Optional[str] = None,
48 | location: Optional[str] = None
49 | ) -> str:
50 | title = hbold(title)
51 | location_str = f'📍 {hcode(location)}\n\n' if location else ''
52 | date_str = f'🗓 {date_str}\n' if date_str else ''
53 | if date_str and not location_str:
54 | date_str = f'{date_str}\n'
55 | resp = f'{title}\n\n' \
56 | f'{date_str}' \
57 | f'{location_str}' \
58 | f'{content}'
59 | return resp
60 |
61 |
62 | def compose_zmanim(data: Zmanim, location_name: str) -> str:
63 | zmanim_rows: dict[str, dt] = data.dict(exclude={'settings', 'is_second_day'}, exclude_none=True)
64 |
65 | if len(zmanim_rows) == 0:
66 | raise PolarCoordinatesException()
67 |
68 | text_lines = []
69 |
70 | for zman_name, zman_value in zmanim_rows.items():
71 | header = hbold(getattr(texts.single.zmanim, zman_name) + ':')
72 | value = humanize_time(zman_value) if isinstance(zman_value, date) \
73 | else humanize_time(zman_value)
74 | line = f'{header} {value}'
75 | text_lines.append(line)
76 |
77 | content = '\n'.join(text_lines)
78 |
79 | date_str = hitalic(f'{humanize_date([data.settings.date_])} / '
80 | f'{parse_jewish_date(data.settings.jewish_date)}')
81 | resp = _compose_response(f'🕓 {names.title_zmanim}', content, date_str, location_name)
82 | return resp
83 |
84 |
85 | def compose_shabbat(data: Shabbat, location_name: str) -> Tuple[str, Optional[InlineKeyboardMarkup]]:
86 | torah_part = names.TORAH_PARTS.get(data.torah_part, '')
87 | date_str = hitalic(humanize_date([data.havdala.date()])) if data.havdala else None
88 | torah_part_str = f'{hbold(headers.parsha + ":")} {torah_part}'
89 |
90 | if not data.candle_lighting:
91 | polar_error = hitalic('\n' + helpers.cl_error_warning)
92 | content = f'{torah_part_str}\n\n{polar_error}'
93 | resp = _compose_response(names.title_shabbath, content, date_str, location_name)
94 | return resp, None
95 |
96 | cl_str = f'{hbold(headers.cl + ":")} {humanize_time(data.candle_lighting)}'
97 | cl_offset = data.settings.cl_offset
98 | cl_offset_str = hitalic(f'({cl_offset} {_(*units.tu_minute, cl_offset)} {helpers.cl_offset})')
99 |
100 | havdala_str = f'{hbold(headers.havdala + ":")} {humanize_time(data.havdala)}'
101 | havdala_opinion = getattr(texts.single.zmanim, data.settings.havdala_opinion)
102 | opinion_value = havdala_opinion.value.split('[')[1].split(']')[0]
103 | havdala_opinion_str = hitalic(f'({opinion_value})')
104 |
105 | late_cl_warning = '\n\n' + hitalic(helpers.cl_late_warning) if data.late_cl_warning else ''
106 |
107 | content = f'{torah_part_str}\n' \
108 | f'{cl_str} {cl_offset_str}\n' \
109 | f'{havdala_str} {havdala_opinion_str} {late_cl_warning}'
110 | resp = _compose_response(f'🕯 {names.title_shabbath}', content, date_str, location_name)
111 |
112 | kb = get_zmanim_by_date_buttons([data.havdala.date()])
113 | return resp, kb
114 |
115 |
116 | def compose_rosh_chodesh(data: RoshChodesh) -> str:
117 | date_str = hitalic(humanize_date(data.days))
118 |
119 | month_str = f'{hbold(_(*units.tu_month, 1).capitalize() + ":")} {names.JEWISH_MONTHS[data.month_name]}'
120 | duration_str = f'{hbold(headers.rh_duration) + ":"} {data.duration}'
121 | molad = data.molad[0]
122 | molad_value = f'{molad.day} {names.MONTH_NAMES_GENETIVE[molad.month]} {molad.year},' \
123 | f'{molad.time().hour} {_(*units.tu_hour, molad.time().hour)} ' \
124 | f'{molad.time().minute} {_(*units.tu_minute, molad.time().minute)} ' \
125 | f'{helpers.and_word} {data.molad[1]} {_(*units.tu_part, data.molad[1])}'
126 | molad_str = f'{hbold(headers.rh_molad) + ":"} {molad_value}'
127 |
128 | content = f'{month_str}\n' \
129 | f'{duration_str}\n' \
130 | f'{molad_str}'
131 |
132 | resp = _compose_response(f'🌙 {names.title_rosh_chodesh}', content, date_str)
133 | return resp
134 |
135 |
136 | def compose_daf_yomi(data: DafYomi) -> str:
137 | date_str = hitalic(f'{humanize_date([data.settings.date_])}')
138 |
139 | masehet_str = f'{hbold(headers.daf_masehet) + ":"} {names.GEMARA_BOOKS.get(data.masehet, "")}'
140 | daf_str = f'{hbold(headers.daf_page) + ":"} {data.daf}'
141 | content = f'{masehet_str}\n{daf_str}'
142 | resp = _compose_response(f'📚 {names.title_daf_yomi}', content, date_str)
143 | return resp
144 |
145 |
146 | def compose_holiday(data: Holiday) -> str:
147 | holiday_last_date = data.date
148 | if data.settings.holiday_name == 'chanukah':
149 | holiday_last_date += timedelta(days=7)
150 |
151 | date_value = humanize_date([data.date, holiday_last_date])
152 | date_str = f'{hbold(headers.date) + ":"} {date_value}'
153 | resp = _compose_response(_emojize_holiday_title(data.settings.holiday_name), date_str)
154 | return resp
155 |
156 |
157 | def compose_israel_holidays(data: IsraelHolidays) -> str:
158 | content_lines = []
159 | for holiday in data.holiday_list:
160 | line = f'{hbold(headers.israel_holidays[holiday[0]]) + ":"} {humanize_date([holiday[1]])}'
161 | content_lines.append(line)
162 |
163 | content = '\n'.join(content_lines)
164 | resp = _compose_response(_emojize_holiday_title('israel_holidays'), content)
165 | return resp
166 |
167 |
168 | def compose_fast(data: Fast, location_name: str) -> Tuple[str, InlineKeyboardMarkup]:
169 | deferred_fast_header = headers.fast_moved.value if data.moved_fast \
170 | else headers.fast_not_moved.value
171 | deferred_fast_str = hitalic(deferred_fast_header)
172 |
173 | fast_start_date = humanize_date([data.fast_start])
174 | fast_start_time = humanize_time(data.fast_start)
175 | fast_start_str = f'{hbold(headers.fast_start) + ":"} {fast_start_date}, {fast_start_time}'
176 |
177 | if data.chatzot:
178 | chatzot_time = humanize_time(data.chatzot)
179 | chatzot_str = f'\n{hbold(texts.single.zmanim.chatzos) + ":"} {chatzot_time}'
180 | else:
181 | chatzot_str = ''
182 |
183 | havdala_header = hbold(headers.fast_end)
184 | havdala_5_95_str = f'{hbold(headers.fast_end_5_95_dgr + ":")} ' \
185 | f'{humanize_time(data.havdala_5_95_dgr)}'
186 | havdala_8_5_str = f'{hbold(headers.fast_end_8_5_dgr + ":")} ' \
187 | f'{humanize_time(data.havdala_8_5_dgr)}'
188 | havdala_42_str = f'{hbold(headers.fast_end_42_min + ":")} ' \
189 | f'{humanize_time(data.havdala_42_min)}'
190 |
191 | content = f'{fast_start_str}\n' \
192 | f'{deferred_fast_str}\n' \
193 | f'{chatzot_str}\n' \
194 | f'{havdala_header}\n' \
195 | f'{havdala_5_95_str}\n' \
196 | f'{havdala_8_5_str}\n' \
197 | f'{havdala_42_str}'
198 |
199 | resp = _compose_response(
200 | names.FASTS_TITLES[data.settings.fast_name],
201 | content,
202 | location=location_name
203 | )
204 | kb = get_zmanim_by_date_buttons([data.havdala_42_min.date()])
205 | return resp, kb
206 |
207 |
208 | def compose_yom_tov(data: YomTov, location_name: str) -> Tuple[str, InlineKeyboardMarkup]:
209 | dates = [
210 | data.pre_shabbat,
211 | data.pesach_eating_chanetz_till and (
212 | data.pesach_eating_chanetz_till,
213 | data.pesach_burning_chanetz_till
214 | ),
215 | data.day_1,
216 | data.day_2,
217 | data.post_shabbat,
218 | data.pesach_part_2_day_1,
219 | data.pesach_part_2_day_2,
220 | data.pesach_part_2_post_shabat,
221 | data.hoshana_rabba
222 | ]
223 | dates = [d for d in dates if d is not None]
224 |
225 | lines = []
226 |
227 | yomtov_last_day = data.pesach_part_2_day_2 or data.pesach_part_2_day_1 \
228 | or data.day_2 or data.day_1
229 | date_str = hitalic(humanize_date([data.day_1.date, yomtov_last_day.date]))
230 |
231 | for date_ in dates:
232 | if isinstance(date_, tuple) and isinstance(date_[0], dt): # for pesach chametz times
233 | header = hbold(headers.pesach_end_eating_chametz + ':')
234 | value = humanize_time(date_[0])
235 | lines.append(f'{header} {value}')
236 |
237 | header = hbold(headers.pesach_end_burning_chametz + ':')
238 | value = humanize_time(date_[1])
239 | lines.append(f'{header} {value}\n')
240 | continue
241 | if isinstance(date_, date): # hoshana rabbah case
242 | header = hbold(headers.hoshana_raba + ':')
243 | value = f'{date_.day} {names.MONTH_NAMES_GENETIVE[date_.month]}, ' \
244 | f'{names.WEEKDAYS[date_.weekday()]}'
245 | lines.append(f'{header} {value}\n')
246 | continue
247 | if date_ == data.pesach_part_2_day_1:
248 | lines.append('\n')
249 | if date_.candle_lighting:
250 |
251 | if date_.candle_lighting.weekday() == 4:
252 | shabbat = f' ({names.shabbat})'
253 | else:
254 | shabbat = ''
255 |
256 | header = f'{headers.cl} {date_.candle_lighting.day} ' \
257 | f'{names.MONTH_NAMES_GENETIVE[date_.candle_lighting.month]}{shabbat}'
258 | lines.append(f'{hbold(header + ":")} {humanize_time(date_.candle_lighting)}')
259 |
260 | if date_.havdala:
261 | header = f'{headers.havdala} {date_.havdala.day} ' \
262 | f'{names.MONTH_NAMES_GENETIVE[date_.havdala.month]}'
263 | lines.append(f'{hbold(header + ":")} {humanize_time(date_.havdala)}')
264 |
265 | content = '\n'.join(lines)
266 | resp = _compose_response(
267 | _emojize_yom_tov_title(data.settings.yomtov_name),
268 | content,
269 | date_str,
270 | location_name
271 | )
272 | kb = get_zmanim_by_date_buttons(
273 | list(map(
274 | lambda d: d.date if isinstance(d, AsurBeMelachaDay) else d,
275 | filter(lambda v: isinstance(v, AsurBeMelachaDay), dates)
276 | ))
277 | )
278 | return resp, kb
279 |
--------------------------------------------------------------------------------
/zmanim_bot/processors/image/renderer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from datetime import date
4 | from datetime import datetime as dt
5 | from datetime import timedelta
6 | from io import BytesIO
7 | from pathlib import Path
8 | from typing import Dict, List, Optional, Tuple, Union
9 |
10 | from PIL import Image, ImageDraw, ImageFont, PngImagePlugin
11 | from aiogram.types import InlineKeyboardMarkup
12 | from babel.support import LazyProxy
13 |
14 | from zmanim_bot import texts
15 | from zmanim_bot.exceptions import PolarCoordinatesException
16 | from zmanim_bot.integrations.zmanim_models import *
17 | from zmanim_bot.keyboards.inline import get_zmanim_by_date_buttons
18 | from zmanim_bot.middlewares.i18n import gettext as _, i18n_
19 | from zmanim_bot.processors.text_utils import humanize_date, humanize_time, parse_jewish_date
20 | from zmanim_bot.texts.plural import units
21 | from zmanim_bot.texts.single import headers, helpers, names, zmanim
22 |
23 | IMG_SIZE = 1181
24 | Line = Tuple[Optional[str], Optional[str], Optional[bool]]
25 | EMPTY_LINE = None, None, None
26 |
27 |
28 | def _convert_img_to_bytes_io(img: PngImagePlugin.PngImageFile) -> BytesIO:
29 | bytes_io = BytesIO()
30 | img.save(bytes_io, 'png')
31 | bytes_io.seek(0)
32 | return bytes_io
33 |
34 |
35 | def _get_draw(background_path: str) -> Tuple[Image.Image, ImageDraw.ImageDraw]:
36 | image = Image.open(background_path)
37 | draw = ImageDraw.Draw(image)
38 | return image, draw
39 |
40 |
41 | class BaseImage:
42 | _font_path = Path(__file__).parent / 'res' / 'fonts' / 'gothic.TTF'
43 | _bold_font_path = Path(__file__).parent / 'res' / 'fonts' / 'gothic-bold.TTF'
44 | _title_font = ImageFont.truetype(str(_bold_font_path), 60)
45 | _font_size = 0
46 | _warning_font_size = 0
47 | _background_path = None
48 |
49 | _image: PngImagePlugin.PngImageFile
50 | _draw: ImageDraw
51 |
52 | _is_rtl: bool
53 |
54 | __x: int
55 | y: int
56 | y_offset: int
57 |
58 | @property
59 | def x(self) -> int:
60 | return self.__x
61 |
62 | @x.setter
63 | def x(self, value: int):
64 | self.__x = value if not self._is_rtl else IMG_SIZE - value
65 |
66 | def shift_y(self):
67 | self.y += self.y_offset
68 |
69 | def get_x_for_text(self, text: str) -> ...:
70 | length = self._x_font_offset(text)
71 | return self.__x + length if not self._is_rtl else self.__x - length
72 |
73 | def __init__(self):
74 | self._font = ImageFont.truetype(str(self._font_path), self._font_size)
75 | self._bold_font = ImageFont.truetype(str(self._bold_font_path), self._font_size)
76 |
77 | if self._background_path:
78 | self._image, self._draw = _get_draw(str(self._background_path))
79 |
80 | self._warning_font = ImageFont.truetype(str(self._bold_font_path), self._warning_font_size)
81 | self._is_rtl = i18n_.is_rtl()
82 |
83 | def _draw_title(self, draw: ImageDraw, title: LazyProxy) -> None:
84 | coordinates = (180, 30)
85 | font = self._title_font
86 | draw.text(coordinates, title.value, font=font)
87 |
88 | def _draw_date(
89 | self,
90 | greg_date: List[Union[date, dt]],
91 | jewish_date: Optional[str] = None,
92 | no_weekday: bool = False
93 | ):
94 | x = 180
95 | y = 100
96 |
97 | greg_date_str = humanize_date(greg_date)
98 | if no_weekday:
99 | greg_date_str = greg_date_str.split(', ')[0]
100 |
101 | jewish_date_str = f' / {parse_jewish_date(jewish_date)}' if jewish_date else ''
102 | date_str = f'{greg_date_str}{jewish_date_str}'
103 |
104 | date_font = ImageFont.truetype(str(self._font_path), 40)
105 | self._draw.text((x, y), date_str, font=date_font)
106 |
107 | def _draw_location(self, location_name: str, *, is_fast: bool = False):
108 | x = 185
109 | y = 150 if not is_fast else 100
110 |
111 | icon_font = ImageFont.truetype(str(Path(__file__).parent / 'res' / 'fonts' / 'fontello.ttf'), 40)
112 | text_font = ImageFont.truetype(str(self._font_path), 40)
113 |
114 | self._draw.text((x, y + 5), f'\ue800', font=icon_font, embedded_color=True)
115 | self._draw.text((x + 20, y), f' {location_name} ', font=text_font, embedded_color=True)
116 |
117 | def _x_font_offset(self, text: str) -> int:
118 | """Returns size in px of given text in axys x"""
119 | last_line = min(text.split('\n'))
120 | offset = self._bold_font.getsize(last_line)[0]
121 | if self._is_rtl:
122 | offset *= -1
123 |
124 | return offset
125 |
126 | def _y_font_offset(self, text: str) -> int:
127 | """Returns size in px of given text in axys y"""
128 | return self._bold_font.getsize(text)[1]
129 |
130 | def _draw_line(
131 | self,
132 | header: Optional[str],
133 | value: Optional[str] = None,
134 | *,
135 | value_on_new_line: bool = False,
136 | value_without_x_offset: bool = False
137 | ):
138 | header = f'{str(header)}{":" if value else ""} ' if header else ''
139 | header_offset = self._x_font_offset(header)
140 | x = self.x + header_offset if self._is_rtl else self.x
141 | header and self._draw.text((x, self.y), text=header, font=self._bold_font)
142 |
143 | if not value:
144 | return
145 |
146 | if not value_without_x_offset:
147 | x += self._x_font_offset(header) if not self._is_rtl else self._x_font_offset(value)
148 | if value_on_new_line or value_without_x_offset:
149 | self.y += self._y_font_offset(header.split('\n')[0])
150 |
151 | self._draw.text(
152 | (x, self.y),
153 | text=str(value),
154 | font=self._font,
155 | direction='rtl' if self._is_rtl else 'ltr',
156 | align='left' if not self._is_rtl else 'right'
157 | )
158 |
159 | def get_image(self):
160 | raise NotImplemented
161 |
162 |
163 | class DafYomImage(BaseImage):
164 | def __init__(self, data: DafYomi):
165 | self._font_size = 90
166 | self.data = data
167 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / 'daf_yomi.png'
168 |
169 | super().__init__()
170 | self._draw_title(self._draw, names.title_daf_yomi)
171 | self._draw_date([self.data.settings.date_], self.data.settings.jewish_date)
172 |
173 | self.y = 470
174 | self.x = 100
175 | self.y_offset = 100
176 |
177 | def get_image(self) -> BytesIO:
178 | # draw masehet
179 | self._draw_line(headers.daf_masehet, names.GEMARA_BOOKS.get(self.data.masehet, ''))
180 | self.shift_y()
181 |
182 | # draw daf
183 | self._draw_line(headers.daf_page, str(self.data.daf))
184 | return _convert_img_to_bytes_io(self._image)
185 |
186 |
187 | class RoshChodeshImage(BaseImage):
188 | def __init__(self, data: RoshChodesh):
189 | self.data = data
190 | self._font_size = 52
191 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / 'rosh_hodesh.png'
192 |
193 | super().__init__()
194 | self._draw_title(self._draw, names.title_rosh_chodesh)
195 | self._draw_date(self.data.days, self.data.settings.jewish_date)
196 |
197 | self.y = 370
198 | self.x = 100
199 | self.y_offset = 80
200 |
201 | def get_image(self) -> BytesIO:
202 |
203 | # draw month
204 | self._draw_line(_(*units.tu_month, 1).capitalize(), names.JEWISH_MONTHS[self.data.month_name])
205 | self.shift_y()
206 |
207 | # draw duration
208 | self._draw_line(headers.rh_duration, str(self.data.duration))
209 | self.shift_y()
210 |
211 | # draw molad string
212 | molad = self.data.molad[0]
213 | molad_value = f'{molad.day} {names.MONTH_NAMES_GENETIVE[molad.month]} {molad.year},\n'\
214 | f'{molad.time().hour} {_(*units.tu_hour, molad.time().hour)} ' \
215 | f'{molad.time().minute} {_(*units.tu_minute, molad.time().minute)} ' \
216 | f'{helpers.and_word} {self.data.molad[1]} {_(*units.tu_part, self.data.molad[1])}'
217 | self._draw_line(headers.rh_molad, molad_value)
218 |
219 | return _convert_img_to_bytes_io(self._image)
220 |
221 |
222 | class ShabbatImage(BaseImage):
223 |
224 | def __init__(self, data: Shabbat, location_name: str):
225 | self.data = data
226 | self._font_size = 60
227 | self._warning_font_size = 48
228 | self.location_name = location_name
229 |
230 | super().__init__()
231 |
232 | if not data.candle_lighting or data.late_cl_warning:
233 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / 'shabbos_attention.png'
234 | else:
235 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / 'shabbos.png'
236 | self._image, self._draw = _get_draw(str(self._background_path))
237 |
238 | self._draw_title(self._draw, names.title_shabbath)
239 | self._draw_location(self.location_name)
240 |
241 | if self.data.havdala:
242 | self._draw_date([self.data.havdala.date()], no_weekday=True)
243 |
244 | self.y = 400 if self.data.candle_lighting else 470
245 | self.x = 100
246 | self.y_offset: int = 80
247 |
248 | def get_image(self) -> Tuple[BytesIO, Optional[InlineKeyboardMarkup]]:
249 | # draw parashat hashavua
250 | torah_part = names.TORAH_PARTS.get(self.data.torah_part, '')
251 | if (self._x_font_offset(headers.parsha.value) + self._x_font_offset(torah_part) + self.x) > IMG_SIZE:
252 | value_on_new_line = True
253 | else:
254 | value_on_new_line = False
255 |
256 | self._draw_line(headers.parsha, torah_part, value_without_x_offset=value_on_new_line)
257 | self.shift_y()
258 | value_on_new_line and self.shift_y()
259 |
260 | # if polar error, draw error message and return
261 | if not self.data.candle_lighting:
262 | x = 100
263 | y = 840 if helpers.cl_error_warning.value.count('\n') < 2 else 810
264 |
265 | self._draw.text((x, y), helpers.cl_error_warning.value, font=self._warning_font,
266 | fill='#ff5959')
267 | return _convert_img_to_bytes_io(self._image), None
268 |
269 | # draw candle lighting
270 | self._draw_line(headers.cl, self.data.candle_lighting.time().isoformat('minutes'))
271 | self.shift_y()
272 |
273 | # draw shekiah offset
274 | cl_offset = self.data.settings.cl_offset
275 | offset_value = f'({cl_offset} {_(*units.tu_minute, cl_offset)} {helpers.cl_offset})'
276 | self._draw_line(None, offset_value)
277 | self.shift_y()
278 |
279 | # draw havdala
280 | self._draw_line(headers.havdala, self.data.havdala.time().isoformat('minutes'))
281 | self.shift_y()
282 |
283 | # draw havdala opinion
284 | havdala_opinion = getattr(texts.single.zmanim, self.data.settings.havdala_opinion)
285 | opinion_value = havdala_opinion.value.split('[')[1].split(']')[0]
286 | self._draw_line(None, f'({opinion_value})')
287 | self.shift_y()
288 |
289 | kb = get_zmanim_by_date_buttons([self.data.havdala.date()])
290 |
291 | # draw warning if need
292 | if not self.data.late_cl_warning:
293 | return _convert_img_to_bytes_io(self._image), kb
294 |
295 | x, y = 100, 840 if not self._is_rtl else 860
296 | self._draw.text((x, y), helpers.cl_late_warning.value, font=self._warning_font, fill='#ff5959')
297 |
298 | return _convert_img_to_bytes_io(self._image), kb
299 |
300 |
301 | class ZmanimImage(BaseImage):
302 |
303 | def __init__(self, data: Zmanim, location_name: str):
304 | self.data = data
305 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / 'zmanim.png'
306 | super().__init__()
307 |
308 | self._draw_title(self._draw, names.title_zmanim)
309 | self._draw_date([self.data.settings.date_], self.data.settings.jewish_date)
310 | self._draw_location(location_name)
311 |
312 | self.zmanim_rows: Dict[str, dt] = self.data.dict(exclude={'settings', 'is_second_day'}, exclude_none=True)
313 |
314 | if len(self.zmanim_rows) == 0:
315 | raise PolarCoordinatesException()
316 |
317 | self._set_font_properties(len(self.zmanim_rows))
318 |
319 | self.y: int = 200 + self._start_y_offset
320 | self.x: int = 50
321 |
322 | def _set_font_properties(self, number_of_lines: int):
323 | p = {
324 | # [font_size, y_offset, start_y_offset
325 | 1: [44, 53, 350],
326 | 2: [44, 53, 300],
327 | 3: [44, 53, 270],
328 | 4: [44, 53, 220],
329 | 5: [44, 53, 180],
330 | 6: [44, 53, 160],
331 | 7: [44, 53, 140],
332 | 8: [44, 53, 100],
333 | 9: [44, 53, 100],
334 | 10: [44, 53, 80],
335 | 11: [44, 53, 40],
336 | 12: [44, 53, 25],
337 | 13: [44, 53, 30],
338 | 14: [44, 53, 30],
339 | 15: [44, 53, 20],
340 | 16: [40, 42, 20],
341 | 17: [40, 42, 20],
342 | 18: [40, 42, 0],
343 | 19: [40, 42, 0]
344 | }
345 | self._font_size, self.y_offset, self._start_y_offset = p.get(number_of_lines)
346 | self._font = ImageFont.truetype(str(self._font_path), size=self._font_size)
347 | self._bold_font = ImageFont.truetype(str(self._bold_font_path), size=self._font_size)
348 |
349 | def get_image(self) -> BytesIO:
350 | for header, value in self.zmanim_rows.items():
351 | self._draw_line(
352 | getattr(texts.single.zmanim, header),
353 | value.time().isoformat('minutes') if isinstance(value, date) else value.isoformat('minutes')
354 | )
355 | self.shift_y()
356 |
357 | return _convert_img_to_bytes_io(self._image)
358 |
359 |
360 | class FastImage(BaseImage):
361 |
362 | def __init__(self, data: Fast, location_name: str):
363 | self.data = data
364 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / 'fast.png'
365 | self._font_size = 60
366 |
367 | super().__init__()
368 |
369 | self._draw_title(self._draw, names.FASTS_TITLES[data.settings.fast_name])
370 | self._draw_location(location_name, is_fast=True)
371 |
372 | self.x = 100
373 | self.y = 300 if data.chatzot else 350
374 | self.y_offset = 80
375 |
376 | def get_image(self) -> Tuple[BytesIO, InlineKeyboardMarkup]:
377 |
378 | fast_moved_header = headers.fast_moved.value if self.data.moved_fast else headers.fast_not_moved.value
379 | self._draw.text(
380 | (
381 | 210 if not self._is_rtl else (IMG_SIZE - 60 + self._x_font_offset(fast_moved_header)),
382 | 155
383 | ),
384 | fast_moved_header,
385 | font=self._bold_font,
386 | fill='#ff5959' if self.data.moved_fast else '#8bff59'
387 | )
388 |
389 | # draw date and start time
390 | fast_date, fast_weekday = humanize_date([self.data.fast_start]).split(', ')
391 | fast_start_value = f'{fast_date},\n{fast_weekday}, {self.data.fast_start.time().isoformat("minutes")}'
392 | self._draw_line(headers.fast_start, fast_start_value)
393 | self.y += self._y_font_offset(fast_start_value) + self.y_offset * 2
394 |
395 | # draw hatzot, if need
396 | if self.data.chatzot:
397 | self._draw_line(zmanim.chatzos, self.data.chatzot.time().isoformat('minutes'))
398 | self.shift_y()
399 |
400 | # draw havdala
401 | self._draw_line(headers.fast_end, '')
402 | self.shift_y()
403 |
404 | havdala_options = (
405 | (self.data.havdala_5_95_dgr, headers.fast_end_5_95_dgr),
406 | (self.data.havdala_8_5_dgr, headers.fast_end_8_5_dgr),
407 | (self.data.havdala_42_min, headers.fast_end_42_min)
408 | )
409 | for havdala_value, havdala_header in havdala_options:
410 | self._draw_line(havdala_header, havdala_value.time().isoformat('minutes'))
411 | self.shift_y()
412 |
413 | kb = get_zmanim_by_date_buttons([self.data.havdala_42_min.date()])
414 | return _convert_img_to_bytes_io(self._image), kb
415 |
416 |
417 | class HolidayImage(BaseImage):
418 |
419 | def __init__(self, data: Holiday):
420 | self.data = data
421 | background_and_font_params = {
422 | 'chanukah': ('chanuka.png', 60),
423 | 'tu_bi_shvat': ('tubishvat.png', 70),
424 | 'purim': ('purim.png', 70),
425 | 'lag_baomer': ('lagbaomer.png', 70),
426 | 'israel_holidays': ('israel_holidays.png', 50),
427 | }
428 | background, font_size = background_and_font_params[data.settings.holiday_name]
429 |
430 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / background
431 | self._font_size = font_size
432 |
433 | super().__init__()
434 |
435 | self._draw_title(self._draw, names.HOLIDAYS_TITLES[data.settings.holiday_name])
436 |
437 | self.x = 100
438 | self.y = 450
439 |
440 | def get_image(self) -> BytesIO:
441 |
442 | holiday_last_date = self.data.date
443 | line_break = False
444 | if self.data.settings.holiday_name == 'chanukah':
445 | holiday_last_date += timedelta(days=7)
446 | line_break = True
447 |
448 | date_value = humanize_date([self.data.date, holiday_last_date], weekday_on_new_line=line_break)
449 |
450 | if (self.x + self._x_font_offset(headers.date.value) + self._x_font_offset(date_value)) > IMG_SIZE:
451 | date_value = humanize_date([self.data.date, holiday_last_date], weekday_on_new_line=True)
452 |
453 | self._draw_line(headers.date, date_value)
454 | return _convert_img_to_bytes_io(self._image)
455 |
456 |
457 | class IsraelHolidaysImage(BaseImage):
458 |
459 | def __init__(self, data: IsraelHolidays):
460 | self.data = data
461 | self._background_path = Path(
462 | __file__).parent / 'res' / 'backgrounds' / 'israel_holidays.png'
463 | self._font_size = 53
464 |
465 | super().__init__()
466 | self.x = 80
467 | self.y = 300
468 | self.y_offset = 90
469 |
470 | self._draw_title(self._draw, names.HOLIDAYS_TITLES['israel_holidays'])
471 |
472 | def get_image(self) -> BytesIO:
473 | y_offset_small = 60
474 |
475 | for holiday in self.data.holiday_list:
476 | header = f'{headers.israel_holidays[holiday[0]]}:'
477 | x = self.x + self._x_font_offset(header) if self._is_rtl else self.x
478 |
479 | self._draw.text((x, self.y), header, font=self._bold_font)
480 | self.y += y_offset_small
481 |
482 | self._draw_line(headers.date, humanize_date([holiday[1]]))
483 | self.shift_y()
484 |
485 | return _convert_img_to_bytes_io(self._image)
486 |
487 |
488 | class YomTovImage(BaseImage):
489 |
490 | def __init__(self, data: YomTov, location_name: str):
491 | backgrounds = {
492 | 'rosh_hashana': 'rosh_hashana.png',
493 | 'yom_kippur': 'yom_kippur.png',
494 | 'succot': 'succos.png',
495 | 'shmini_atzeres': 'shmini_atzeret.png',
496 | 'pesach': 'pesah.png',
497 | 'shavuot': 'shavuot.png',
498 | }
499 | background = backgrounds[data.settings.yomtov_name]
500 |
501 | self._background_path = Path(__file__).parent / 'res' / 'backgrounds' / background
502 |
503 | self.data = data
504 | super().__init__()
505 | self.x = 80
506 |
507 | self.dates = self._get_dates()
508 | self.lines = self._prepare_lines(self.dates)
509 | self.y, self.y_offset, self._font_size = self._get_font_properties(len(self.lines))
510 |
511 | self._font = ImageFont.truetype(str(self._font_path), self._font_size)
512 | self._bold_font = ImageFont.truetype(str(self._bold_font_path), self._font_size)
513 |
514 | self._draw_title(self._draw, names.YOMTOVS_TITLES[data.settings.yomtov_name])
515 | self._draw_location(location_name)
516 |
517 | @staticmethod
518 | def _humanize_header_date(header_type: str, date_: Union[date, dt]) -> Tuple[str, bool]:
519 | """
520 | Use this function for date header, not for date title.
521 | :return header and flag indicates if it's two-lines height header
522 | """
523 | if header_type == headers.cl and date_.weekday() == 4:
524 | shabbat = f'\n({names.shabbat})'
525 | else:
526 | shabbat = ''
527 |
528 | resp = f'{header_type} {date_.day} {names.MONTH_NAMES_GENETIVE[date_.month]}{shabbat}'
529 | return resp, bool(shabbat)
530 |
531 | def _get_dates(self) -> List[Union[AsurBeMelachaDay, date, Tuple[dt, str]]]:
532 | dates = [
533 | self.data.pre_shabbat,
534 | self.data.pesach_eating_chanetz_till and (
535 | self.data.pesach_eating_chanetz_till,
536 | self.data.pesach_burning_chanetz_till
537 | ),
538 | self.data.day_1,
539 | self.data.day_2,
540 | self.data.post_shabbat,
541 | self.data.pesach_part_2_day_1,
542 | self.data.pesach_part_2_day_2,
543 | self.data.pesach_part_2_post_shabat,
544 | self.data.hoshana_rabba
545 | ]
546 | return [d for d in dates if d is not None]
547 |
548 | def _prepare_lines(self, dates: List[Union[AsurBeMelachaDay, date]]) -> List[Line]:
549 | lines = []
550 |
551 | yomtov_last_day = self.data.pesach_part_2_day_2 \
552 | or self.data.pesach_part_2_day_1 \
553 | or self.data.day_2 \
554 | or self.data.day_1
555 | self._draw_date([self.data.day_1.date, yomtov_last_day.date])
556 |
557 | for date_ in dates:
558 | if isinstance(date_, tuple) and isinstance(date_[0], dt): # for pesach chametz times
559 | header = str(headers.pesach_end_eating_chametz)
560 | value = humanize_time(date_[0])
561 | lines.append((header, value, False))
562 |
563 | header = str(headers.pesach_end_burning_chametz)
564 | value = humanize_time(date_[1])
565 | lines.append((header, value, False))
566 | lines.append(EMPTY_LINE)
567 | continue
568 |
569 | if isinstance(date_, date): # hoshana rabbah case
570 | header = str(headers.hoshana_raba)
571 | sep = '\n' if not self._is_rtl else ''
572 | value = f'{date_.day} {names.MONTH_NAMES_GENETIVE[date_.month]},{sep} {names.WEEKDAYS[date_.weekday()]}'
573 | lines.append(EMPTY_LINE)
574 | lines.append((header, value, False))
575 | continue
576 |
577 | if date_ == self.data.pesach_part_2_day_1:
578 | lines.append(EMPTY_LINE)
579 |
580 | if date_.candle_lighting:
581 | header, new_line = self._humanize_header_date(headers.cl, date_.candle_lighting)
582 | value = humanize_time(date_.candle_lighting)
583 | lines.append((header, value, new_line))
584 | if date_.havdala:
585 | header, new_line = self._humanize_header_date(headers.havdala, date_.havdala)
586 | value = humanize_time(date_.havdala)
587 | lines.append((header, value, new_line))
588 |
589 | return lines
590 |
591 | @staticmethod
592 | def _get_font_properties(number_of_lines: int) -> Tuple[int, int, int]:
593 | p = {
594 | # [font_size, y_offset, start_y_position]
595 | 2: (50, 70, 400),
596 | 3: (50, 70, 400),
597 | 4: (50, 70, 400),
598 | 5: (50, 80, 260),
599 | 6: (50, 70, 260),
600 | 7: (50, 50, 260),
601 | 8: (50, 50, 260),
602 | 9: (45, 50, 230),
603 | 10: (45, 50, 230),
604 | 11: (45, 50, 230)
605 | }
606 | font_size, y_offset, start_position_y = p.get(number_of_lines)
607 | return start_position_y, y_offset, font_size
608 |
609 | def get_image(self) -> Tuple[BytesIO, Optional[InlineKeyboardMarkup]]:
610 | for header, value, new_line in self.lines:
611 | if not header:
612 | self.y += self.y_offset * 2
613 | continue
614 |
615 | if '\n' in header:
616 | if abs(self._x_font_offset(header.replace('\n', ' '))) < IMG_SIZE * 0.8:
617 | header = header.replace('\n', ' ')
618 | self._draw_line(header, value)
619 | else:
620 | header_1, header_2 = header.split('\n')
621 | self._draw_line(header_1)
622 | self.y += self._y_font_offset(header_1)
623 | self._draw_line(header_2, value)
624 | else:
625 | self._draw_line(header, value)
626 |
627 | if new_line:
628 | self.y += self._y_font_offset(header)
629 | self.shift_y()
630 |
631 | kb = get_zmanim_by_date_buttons(
632 | list(map(
633 | lambda d: d.date if isinstance(d, AsurBeMelachaDay) else d,
634 | filter(lambda v: isinstance(v, AsurBeMelachaDay), self.dates)
635 | ))
636 | )
637 | return _convert_img_to_bytes_io(self._image), kb
638 |
--------------------------------------------------------------------------------