Attempt to work around timezones

This commit is contained in:
Profitroll 2023-09-24 23:47:09 +02:00
parent b0f76f4c49
commit 3fa2f5a800
Signed by: profitroll
GPG Key ID: FA35CAB49DACD3B2
13 changed files with 99 additions and 36 deletions

View File

@ -1,6 +1,9 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union
from bson import ObjectId from bson import ObjectId
from pytz import timezone as pytz_timezone
from pytz.tzinfo import BaseTzInfo, DstTzInfo
from classes.point import Point from classes.point import Point
from modules.database import col_locations from modules.database import col_locations
@ -22,7 +25,7 @@ class Location:
name: str name: str
location: Point location: Point
country: int country: int
timezone: str timezone: Union[BaseTzInfo, DstTzInfo]
@classmethod @classmethod
async def get(cls, id: int): async def get(cls, id: int):
@ -32,6 +35,7 @@ class Location:
raise ValueError(f"No location with ID {id} found.") raise ValueError(f"No location with ID {id} found.")
db_entry["location"] = Point(*db_entry["location"]) # type: ignore db_entry["location"] = Point(*db_entry["location"]) # type: ignore
db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore
return cls(**db_entry) return cls(**db_entry)
@ -43,6 +47,7 @@ class Location:
raise ValueError(f"No location with name {name} found.") raise ValueError(f"No location with name {name} found.")
db_entry["location"] = Point(*db_entry["location"]) # type: ignore db_entry["location"] = Point(*db_entry["location"]) # type: ignore
db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore
return cls(**db_entry) return cls(**db_entry)
@ -54,5 +59,6 @@ class Location:
raise ValueError(f"No location near {lat}, {lon} found.") raise ValueError(f"No location near {lat}, {lon} found.")
db_entry["location"] = Point(*db_entry["location"]) # type: ignore db_entry["location"] = Point(*db_entry["location"]) # type: ignore
db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore
return cls(**db_entry) return cls(**db_entry)

View File

@ -46,6 +46,7 @@
"import_invalid": "Dies ist kein gültiges Abfallterminen JSON.", "import_invalid": "Dies ist kein gültiges Abfallterminen JSON.",
"import": "Okay. Senden Sie mir ein gültiges JSON.", "import": "Okay. Senden Sie mir ein gültiges JSON.",
"locale_choice": "Prima. Bitte wählen Sie die Sprache mit der Tastatur unten.", "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_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_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.", "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_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_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.", "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_empty": "Keine Müllabfuhr-Einträge für die nächsten 30 Tage bei **{name}** gefunden",
"upcoming": "Bevorstehende Müllabfuhr:\n\n{entries}" "upcoming": "Bevorstehende Müllabfuhr:\n\n{entries}"
}, },
@ -95,4 +95,4 @@
"callbacks": { "callbacks": {
"locale_set": "Ihre Sprache ist jetzt: {locale}" "locale_set": "Ihre Sprache ist jetzt: {locale}"
} }
} }

View File

@ -46,6 +46,7 @@
"import_invalid": "This is not a valid garbage collection JSON.", "import_invalid": "This is not a valid garbage collection JSON.",
"import": "Alright. Send me a valid JSON.", "import": "Alright. Send me a valid JSON.",
"locale_choice": "Alright. Please choose the language using keyboard below.", "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_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_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.", "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_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_enabled": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time}. Use /setup to select your location.",
"toggle": "Execute /toggle to enable notifications.", "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_empty": "No garbage collection entries found for the next 30 days at **{name}**",
"upcoming": "Upcoming garbage collection:\n\n{entries}" "upcoming": "Upcoming garbage collection:\n\n{entries}"
}, },

View File

@ -22,6 +22,7 @@
"import_invalid": "Це недійсний JSON даних збору сміття.", "import_invalid": "Це недійсний JSON даних збору сміття.",
"import": "Гаразд. Надішліть правильний JSON.", "import": "Гаразд. Надішліть правильний JSON.",
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.", "locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.",
"location_empty": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.",
"location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}", "location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}",
"location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}", "location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}",
"location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.", "location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.",
@ -47,7 +48,6 @@
"toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.", "toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.",
"toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.", "toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.",
"toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.", "toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.",
"upcoming_empty_location": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.",
"upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**", "upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**",
"upcoming": "Найближчі вивози сміття:\n\n{entries}" "upcoming": "Найближчі вивози сміття:\n\n{entries}"
}, },
@ -95,4 +95,4 @@
"set_offset": "Кількість днів", "set_offset": "Кількість днів",
"set_time": "Час у вигляді ГГ:ХХ" "set_time": "Час у вигляді ГГ:ХХ"
} }
} }

View File

@ -46,6 +46,7 @@
"import_invalid": "Це недійсний JSON даних збору сміття.", "import_invalid": "Це недійсний JSON даних збору сміття.",
"import": "Гаразд. Надішліть правильний JSON.", "import": "Гаразд. Надішліть правильний JSON.",
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.", "locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.",
"location_empty": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.",
"location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}", "location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}",
"location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}", "location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}",
"location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.", "location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.",
@ -72,7 +73,6 @@
"toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.", "toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.",
"toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.", "toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.",
"toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.", "toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.",
"upcoming_empty_location": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.",
"upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**", "upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**",
"upcoming": "Найближчі вивози сміття:\n\n{entries}" "upcoming": "Найближчі вивози сміття:\n\n{entries}"
}, },

View File

@ -2,25 +2,25 @@ import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from libbot.pyrogram.classes import PyroClient from libbot.pyrogram.classes import PyroClient
from pytz import timezone as pytz_timezone
from classes.enums import GarbageType from classes.enums import GarbageType
from classes.location import Location from classes.location import Location
from classes.pyrouser import PyroUser from classes.pyrouser import PyroUser
from modules.database import col_entries, col_users from modules.database import col_entries, col_users
from modules.utils import from_utc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def remind(app: PyroClient) -> None: async def remind(app: PyroClient) -> None:
now = datetime.now() utcnow = datetime.utcnow()
users = await col_users.find( users = await col_users.find(
{"time_hour": now.hour, "time_minute": now.minute} {"time_hour": utcnow.hour, "time_minute": utcnow.minute}
).to_list() ).to_list()
for user_db in users: 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: if not user.enabled or user.location is None:
continue continue
@ -30,12 +30,10 @@ async def remind(app: PyroClient) -> None:
except ValueError: except ValueError:
continue continue
user_date = ( user_date = from_utc(
datetime.now(pytz_timezone(location.timezone)).replace( datetime(1970, 1, 1, user.time_hour, user.time_minute),
second=0, microsecond=0 user.location.timezone.zone,
) )
+ timedelta(days=user.offset)
).replace(tzinfo=timezone.utc)
entries = await col_entries.find( entries = await col_entries.find(
{ {

View File

@ -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)
)

View File

@ -7,6 +7,7 @@ from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules import custom_filters from modules import custom_filters
from modules.utils import from_utc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,6 +18,12 @@ logger = logging.getLogger(__name__)
async def command_set_offset(app: PyroClient, message: Message): async def command_set_offset(app: PyroClient, message: Message):
user = await app.find_user(message.from_user) 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( await message.reply_text(
app._("set_offset", "messages", locale=user.locale), app._("set_offset", "messages", locale=user.locale),
reply_markup=ForceReply( 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) logger.info("User %s has set offset to %s", user.id, offset)
garbage_time = datetime( garbage_time = from_utc(
1970, 1, 1, hour=user.time_hour, minute=user.time_minute 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")) ).strftime(app._("time", "formats"))
await answer.reply_text( await answer.reply_text(

View File

@ -7,6 +7,7 @@ from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules import custom_filters from modules import custom_filters
from modules.utils import to_utc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,6 +18,12 @@ logger = logging.getLogger(__name__)
async def command_set_time(app: PyroClient, message: Message): async def command_set_time(app: PyroClient, message: Message):
user = await app.find_user(message.from_user) 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( await message.reply_text(
app._("set_time", "messages", locale=user.locale), app._("set_time", "messages", locale=user.locale),
reply_markup=ForceReply( reply_markup=ForceReply(
@ -48,7 +55,10 @@ async def command_set_time(app: PyroClient, message: Message):
break 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) 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"), 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( await answer.reply_text(
app._("set_time_finished", "messages", locale=user.locale).format( app._("set_time_finished", "messages", locale=user.locale).format(

View File

@ -11,6 +11,7 @@ from classes.pyroclient import PyroClient
from modules import custom_filters from modules import custom_filters
from modules.search_name import search_name from modules.search_name import search_name
from modules.search_nearby import search_nearby from modules.search_nearby import search_nearby
from modules.utils import from_utc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -73,9 +74,10 @@ async def command_setup(app: PyroClient, message: Message):
await user.update_location(location.id) await user.update_location(location.id)
user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime( user_time = from_utc(
app._("time", "formats", locale=user.locale) 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( await message.reply_text(
app._("setup_finished", "messages", locale=user.locale).format( app._("setup_finished", "messages", locale=user.locale).format(

View File

@ -11,6 +11,7 @@ from pyrogram.types import (
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules import custom_filters from modules import custom_filters
from modules.utils import from_utc
@PyroClient.on_message( @PyroClient.on_message(
@ -85,9 +86,10 @@ async def command_start(app: PyroClient, message: Message):
await user.update_location(location.id) await user.update_location(location.id)
user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime( user_time = from_utc(
app._("time", "formats", locale=user.locale) 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( await answer.reply_text(
app._("start_selection_yes", "messages", locale=user.locale).format( app._("start_selection_yes", "messages", locale=user.locale).format(
name=location.name, offset=user.offset, time=user_time name=location.name, offset=user.offset, time=user_time

View File

@ -5,6 +5,7 @@ from pyrogram.types import Message
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules import custom_filters from modules import custom_filters
from modules.utils import from_utc
@PyroClient.on_message( @PyroClient.on_message(
@ -21,7 +22,10 @@ async def command_toggle(app: PyroClient, message: Message):
) )
return 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: if user.location is None:
await message.reply_text( await message.reply_text(

View File

@ -2,7 +2,6 @@ from datetime import datetime, timedelta, timezone
from pyrogram import filters from pyrogram import filters
from pyrogram.types import Message from pyrogram.types import Message
from pytz import timezone as pytz_timezone
from classes.garbage_entry import GarbageEntry from classes.garbage_entry import GarbageEntry
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
@ -18,19 +17,15 @@ async def command_upcoming(app: PyroClient, message: Message):
if user.location is None: if user.location is None:
await message.reply_text( await message.reply_text(
app._("upcoming_empty_location", "messages", locale=user.locale) app._("location_empty", "messages", locale=user.locale)
) )
return return
date_min = ( date_min = (
datetime.now(pytz_timezone(user.location.timezone)).replace( datetime.now(user.location.timezone).replace(second=0, microsecond=0)
second=0, microsecond=0
)
).replace(tzinfo=timezone.utc) ).replace(tzinfo=timezone.utc)
date_max = ( date_max = (
datetime.now(pytz_timezone(user.location.timezone)).replace( datetime.now(user.location.timezone).replace(second=0, microsecond=0)
second=0, microsecond=0
)
+ timedelta(days=30) + timedelta(days=30)
).replace(tzinfo=timezone.utc) ).replace(tzinfo=timezone.utc)