From 3fa2f5a8009997f3dd6b4c2c271125d2e6803792 Mon Sep 17 00:00:00 2001 From: profitroll Date: Sun, 24 Sep 2023 23:47:09 +0200 Subject: [PATCH] Attempt to work around timezones --- classes/location.py | 8 ++++++- locale/de.json | 4 ++-- locale/en.json | 2 +- locale/ru.json | 4 ++-- locale/uk.json | 2 +- modules/reminder.py | 18 +++++++--------- modules/utils.py | 38 ++++++++++++++++++++++++++++++++++ plugins/commands/set_offset.py | 12 +++++++++-- plugins/commands/set_time.py | 14 +++++++++++-- plugins/commands/setup.py | 8 ++++--- plugins/commands/start.py | 8 ++++--- plugins/commands/toggle.py | 6 +++++- plugins/commands/upcoming.py | 11 +++------- 13 files changed, 99 insertions(+), 36 deletions(-) diff --git a/classes/location.py b/classes/location.py index 26d9ebd..5f51a90 100644 --- a/classes/location.py +++ b/classes/location.py @@ -1,6 +1,9 @@ from dataclasses import dataclass +from typing import Union from bson import ObjectId +from pytz import timezone as pytz_timezone +from pytz.tzinfo import BaseTzInfo, DstTzInfo from classes.point import Point from modules.database import col_locations @@ -22,7 +25,7 @@ class Location: name: str location: Point country: int - timezone: str + timezone: Union[BaseTzInfo, DstTzInfo] @classmethod async def get(cls, id: int): @@ -32,6 +35,7 @@ class Location: raise ValueError(f"No location with ID {id} found.") db_entry["location"] = Point(*db_entry["location"]) # type: ignore + db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore return cls(**db_entry) @@ -43,6 +47,7 @@ class Location: raise ValueError(f"No location with name {name} found.") db_entry["location"] = Point(*db_entry["location"]) # type: ignore + db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore return cls(**db_entry) @@ -54,5 +59,6 @@ class Location: raise ValueError(f"No location near {lat}, {lon} found.") db_entry["location"] = Point(*db_entry["location"]) # type: ignore + db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore return cls(**db_entry) diff --git a/locale/de.json b/locale/de.json index 708b858..9c84eda 100644 --- a/locale/de.json +++ b/locale/de.json @@ -46,6 +46,7 @@ "import_invalid": "Dies ist kein gültiges Abfallterminen JSON.", "import": "Okay. Senden Sie mir ein gültiges JSON.", "locale_choice": "Prima. Bitte wählen Sie die Sprache mit der Tastatur unten.", + "location_empty": "Sie haben keinen Standort festgelegt. Verwenden Sie /setup, um Ihren Standort auszuwählen.", "location_name_empty": "Es konnten keine Orte mit diesem Namen gefunden werden. Versuchen Sie, ihn umzuformulieren, oder stellen Sie sicher, dass Sie dieselbe Sprache und denselben Namen verwenden, wie er von Ihren örtlichen Behörden im Müllabfuhrplan angegeben ist.\n\n{cancel_notice}", "location_name_invalid": "Bitte senden Sie den Namen des Ortes als Text. {cancel_notice}", "location_name": "Bitte senden Sie mir einen Standortnamen. Es sollte der Name sein, der im Müllabfuhrplan Ihrer örtlichen Behörde verwendet wird. In der Regel ist dies der Name des Bezirks oder sogar der Stadt selbst.", @@ -72,7 +73,6 @@ "toggle_enabled_location": "🔔 Benachrichtigungen wurden aktiviert {offset} T. vor der Sammlung um {time} am **{name}**.", "toggle_enabled": "🔔 Benachrichtigungen wurden aktiviert {offset} T. vor der Sammlung um {time}. Verwenden Sie /setup, um Ihren Standort auszuwählen.", "toggle": "Führen Sie /toggle aus, um Benachrichtigungen zu aktivieren.", - "upcoming_empty_location": "Sie haben keinen Standort festgelegt. Verwenden Sie /setup, um Ihren Standort auszuwählen.", "upcoming_empty": "Keine Müllabfuhr-Einträge für die nächsten 30 Tage bei **{name}** gefunden", "upcoming": "Bevorstehende Müllabfuhr:\n\n{entries}" }, @@ -95,4 +95,4 @@ "callbacks": { "locale_set": "Ihre Sprache ist jetzt: {locale}" } -} +} \ No newline at end of file diff --git a/locale/en.json b/locale/en.json index 9f96544..1509a69 100644 --- a/locale/en.json +++ b/locale/en.json @@ -46,6 +46,7 @@ "import_invalid": "This is not a valid garbage collection JSON.", "import": "Alright. Send me a valid JSON.", "locale_choice": "Alright. Please choose the language using keyboard below.", + "location_empty": "You have no location set. Use /setup to select your location first.", "location_name_empty": "Could not find any locations by this name. Try rephrasing it or make sure you use the same location language and name itself as it in written by your local authorities in garbage collection schedule.\n\n{cancel_notice}", "location_name_invalid": "Please, send the name of the location as a text. {cancel_notice}", "location_name": "Please, send me a location name. It should be the name used in your local authorities' garbage collection schedule. This usually is a name of the district or even the town itself.", @@ -72,7 +73,6 @@ "toggle_enabled_location": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time} at the **{name}**.", "toggle_enabled": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time}. Use /setup to select your location.", "toggle": "Execute /toggle to enable notifications.", - "upcoming_empty_location": "You have no location set. Use /setup to select your location.", "upcoming_empty": "No garbage collection entries found for the next 30 days at **{name}**", "upcoming": "Upcoming garbage collection:\n\n{entries}" }, diff --git a/locale/ru.json b/locale/ru.json index 5f1168f..27adcea 100644 --- a/locale/ru.json +++ b/locale/ru.json @@ -22,6 +22,7 @@ "import_invalid": "Це недійсний JSON даних збору сміття.", "import": "Гаразд. Надішліть правильний JSON.", "locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.", + "location_empty": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.", "location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}", "location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}", "location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.", @@ -47,7 +48,6 @@ "toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.", "toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.", "toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.", - "upcoming_empty_location": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.", "upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**", "upcoming": "Найближчі вивози сміття:\n\n{entries}" }, @@ -95,4 +95,4 @@ "set_offset": "Кількість днів", "set_time": "Час у вигляді ГГ:ХХ" } -} +} \ No newline at end of file diff --git a/locale/uk.json b/locale/uk.json index 263a3c0..4ed1758 100644 --- a/locale/uk.json +++ b/locale/uk.json @@ -46,6 +46,7 @@ "import_invalid": "Це недійсний JSON даних збору сміття.", "import": "Гаразд. Надішліть правильний JSON.", "locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.", + "location_empty": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.", "location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}", "location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}", "location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.", @@ -72,7 +73,6 @@ "toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.", "toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.", "toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.", - "upcoming_empty_location": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.", "upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**", "upcoming": "Найближчі вивози сміття:\n\n{entries}" }, diff --git a/modules/reminder.py b/modules/reminder.py index cf2b939..a662e36 100644 --- a/modules/reminder.py +++ b/modules/reminder.py @@ -2,25 +2,25 @@ import logging from datetime import datetime, timedelta, timezone from libbot.pyrogram.classes import PyroClient -from pytz import timezone as pytz_timezone from classes.enums import GarbageType from classes.location import Location from classes.pyrouser import PyroUser from modules.database import col_entries, col_users +from modules.utils import from_utc logger = logging.getLogger(__name__) async def remind(app: PyroClient) -> None: - now = datetime.now() + utcnow = datetime.utcnow() users = await col_users.find( - {"time_hour": now.hour, "time_minute": now.minute} + {"time_hour": utcnow.hour, "time_minute": utcnow.minute} ).to_list() for user_db in users: - user = PyroUser(**user_db) + user = await PyroUser.from_dict(**user_db) if not user.enabled or user.location is None: continue @@ -30,12 +30,10 @@ async def remind(app: PyroClient) -> None: except ValueError: continue - user_date = ( - datetime.now(pytz_timezone(location.timezone)).replace( - second=0, microsecond=0 - ) - + timedelta(days=user.offset) - ).replace(tzinfo=timezone.utc) + user_date = from_utc( + datetime(1970, 1, 1, user.time_hour, user.time_minute), + user.location.timezone.zone, + ) entries = await col_entries.find( { diff --git a/modules/utils.py b/modules/utils.py index e69de29..aaece4b 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -0,0 +1,38 @@ +from datetime import datetime +from typing import Union + +from pytz import UTC +from pytz import timezone as pytz_timezone + + +def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime: + """Move timezone unaware datetime object to UTC timezone and return it. + + ### Args: + * date (`datetime`): Datetime to be converted. + * timezone (`Union[str, None] = None`): Timezone name (must be pytz-compatible). Defaults to `None` (UTC). + + ### Returns: + * `datetime`: Timezone unaware datetime in UTC with timezone's offset applied to it. + """ + timezone = "UTC" if timezone is None else timezone + return pytz_timezone(timezone).localize(date).astimezone(UTC).replace(tzinfo=None) + + +def from_utc(date: datetime, timezone: Union[str, None] = None) -> datetime: + """Move timezone unaware datetime object to the timezone specified and return it. + + ### Args: + * date (`datetime`): Datetime to be converted. + * timezone (`Union[str, None] = None`): Timezone name (must be pytz-compatible). Defaults to `None` (UTC). + + ### Returns: + * `datetime`: Timezone unaware datetime in timezone provided with offset from UTC applied to it. + """ + timezone = "UTC" if timezone is None else timezone + return ( + pytz_timezone("UTC") + .localize(date) + .astimezone(pytz_timezone(timezone)) + .replace(tzinfo=None) + ) diff --git a/plugins/commands/set_offset.py b/plugins/commands/set_offset.py index c0981de..30fce12 100644 --- a/plugins/commands/set_offset.py +++ b/plugins/commands/set_offset.py @@ -7,6 +7,7 @@ from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove from classes.pyroclient import PyroClient from modules import custom_filters +from modules.utils import from_utc logger = logging.getLogger(__name__) @@ -17,6 +18,12 @@ logger = logging.getLogger(__name__) async def command_set_offset(app: PyroClient, message: Message): user = await app.find_user(message.from_user) + if user.location is None: + await message.reply_text( + app._("location_empty", "messages", locale=user.locale) + ) + return + await message.reply_text( app._("set_offset", "messages", locale=user.locale), reply_markup=ForceReply( @@ -58,8 +65,9 @@ async def command_set_offset(app: PyroClient, message: Message): logger.info("User %s has set offset to %s", user.id, offset) - garbage_time = datetime( - 1970, 1, 1, hour=user.time_hour, minute=user.time_minute + garbage_time = from_utc( + datetime(1970, 1, 1, user.time_hour, user.time_minute), + None if user.location is None else user.location.timezone.zone, ).strftime(app._("time", "formats")) await answer.reply_text( diff --git a/plugins/commands/set_time.py b/plugins/commands/set_time.py index fbee4fb..5e4fb40 100644 --- a/plugins/commands/set_time.py +++ b/plugins/commands/set_time.py @@ -7,6 +7,7 @@ from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove from classes.pyroclient import PyroClient from modules import custom_filters +from modules.utils import to_utc logger = logging.getLogger(__name__) @@ -17,6 +18,12 @@ logger = logging.getLogger(__name__) async def command_set_time(app: PyroClient, message: Message): user = await app.find_user(message.from_user) + if user.location is None: + await message.reply_text( + app._("location_empty", "messages", locale=user.locale) + ) + return + await message.reply_text( app._("set_time", "messages", locale=user.locale), reply_markup=ForceReply( @@ -48,7 +55,10 @@ async def command_set_time(app: PyroClient, message: Message): break - user_time = datetime.strptime(answer.text, "%H:%M") + parsed_time = datetime.strptime(answer.text, "%H:%M").replace( + year=1970, month=1, day=1, second=0, microsecond=0 + ) + user_time = to_utc(parsed_time, user.location.timezone.zone) await user.update_time(hour=user_time.hour, minute=user_time.minute) @@ -58,7 +68,7 @@ async def command_set_time(app: PyroClient, message: Message): user_time.strftime("%H:%M"), ) - garbage_time = user_time.strftime(app._("time", "formats")) + garbage_time = parsed_time.strftime(app._("time", "formats")) await answer.reply_text( app._("set_time_finished", "messages", locale=user.locale).format( diff --git a/plugins/commands/setup.py b/plugins/commands/setup.py index 5bebffe..3790831 100644 --- a/plugins/commands/setup.py +++ b/plugins/commands/setup.py @@ -11,6 +11,7 @@ from classes.pyroclient import PyroClient from modules import custom_filters from modules.search_name import search_name from modules.search_nearby import search_nearby +from modules.utils import from_utc logger = logging.getLogger(__name__) @@ -73,9 +74,10 @@ async def command_setup(app: PyroClient, message: Message): await user.update_location(location.id) - user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime( - app._("time", "formats", locale=user.locale) - ) + user_time = from_utc( + datetime(1970, 1, 1, user.time_hour, user.time_minute), + None if user.location is None else user.location.timezone.zone, + ).strftime(app._("time", "formats", locale=user.locale)) await message.reply_text( app._("setup_finished", "messages", locale=user.locale).format( diff --git a/plugins/commands/start.py b/plugins/commands/start.py index 3df7e34..6cdd43e 100644 --- a/plugins/commands/start.py +++ b/plugins/commands/start.py @@ -11,6 +11,7 @@ from pyrogram.types import ( from classes.pyroclient import PyroClient from modules import custom_filters +from modules.utils import from_utc @PyroClient.on_message( @@ -85,9 +86,10 @@ async def command_start(app: PyroClient, message: Message): await user.update_location(location.id) - user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime( - app._("time", "formats", locale=user.locale) - ) + user_time = from_utc( + datetime(1970, 1, 1, user.time_hour, user.time_minute), + None if user.location is None else user.location.timezone.zone, + ).strftime(app._("time", "formats", locale=user.locale)) await answer.reply_text( app._("start_selection_yes", "messages", locale=user.locale).format( name=location.name, offset=user.offset, time=user_time diff --git a/plugins/commands/toggle.py b/plugins/commands/toggle.py index ac97f2b..1b67107 100644 --- a/plugins/commands/toggle.py +++ b/plugins/commands/toggle.py @@ -5,6 +5,7 @@ from pyrogram.types import Message from classes.pyroclient import PyroClient from modules import custom_filters +from modules.utils import from_utc @PyroClient.on_message( @@ -21,7 +22,10 @@ async def command_toggle(app: PyroClient, message: Message): ) return - user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime("%H:%M") + user_time = from_utc( + datetime(1970, 1, 1, user.time_hour, user.time_minute), + None if user.location is None else user.location.timezone.zone, + ).strftime(app._("time", "formats")) if user.location is None: await message.reply_text( diff --git a/plugins/commands/upcoming.py b/plugins/commands/upcoming.py index 163fd09..9c67396 100644 --- a/plugins/commands/upcoming.py +++ b/plugins/commands/upcoming.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta, timezone from pyrogram import filters from pyrogram.types import Message -from pytz import timezone as pytz_timezone from classes.garbage_entry import GarbageEntry from classes.pyroclient import PyroClient @@ -18,19 +17,15 @@ async def command_upcoming(app: PyroClient, message: Message): if user.location is None: await message.reply_text( - app._("upcoming_empty_location", "messages", locale=user.locale) + app._("location_empty", "messages", locale=user.locale) ) return date_min = ( - datetime.now(pytz_timezone(user.location.timezone)).replace( - second=0, microsecond=0 - ) + datetime.now(user.location.timezone).replace(second=0, microsecond=0) ).replace(tzinfo=timezone.utc) date_max = ( - datetime.now(pytz_timezone(user.location.timezone)).replace( - second=0, microsecond=0 - ) + datetime.now(user.location.timezone).replace(second=0, microsecond=0) + timedelta(days=30) ).replace(tzinfo=timezone.utc)