4 Commits

14 changed files with 79 additions and 149 deletions

View File

@@ -10,7 +10,7 @@ class CallbackLanguage:
language: str language: str
@classmethod @classmethod
def from_callback(cls, callback: CallbackQuery) -> "CallbackLanguage": def from_callback(cls, callback: CallbackQuery):
"""Parse callback query and extract language data from it. """Parse callback query and extract language data from it.
### Args: ### Args:

View File

@@ -23,7 +23,7 @@ class GarbageEntry:
date: datetime date: datetime
@classmethod @classmethod
async def from_dict(cls, data: Mapping[str, Any]) -> "GarbageEntry": async def from_dict(cls, data: Mapping[str, Any]):
"""Generate GarbageEntry object from the mapping provided """Generate GarbageEntry object from the mapping provided
### Args: ### Args:
@@ -60,7 +60,7 @@ class GarbageEntry:
) )
@classmethod @classmethod
async def from_record(cls, data: Mapping[str, Any]) -> "GarbageEntry": async def from_record(cls, data: Mapping[str, Any]):
locations = [ locations = [
await Location.get(location_id) for location_id in data["locations"] await Location.get(location_id) for location_id in data["locations"]
] ]

View File

@@ -28,7 +28,7 @@ class Location:
timezone: Union[BaseTzInfo, DstTzInfo] timezone: Union[BaseTzInfo, DstTzInfo]
@classmethod @classmethod
async def get(cls, id: int) -> "Location": async def get(cls, id: int):
db_entry = await col_locations.find_one({"id": id}) db_entry = await col_locations.find_one({"id": id})
if db_entry is None: if db_entry is None:
@@ -40,7 +40,7 @@ class Location:
return cls(**db_entry) return cls(**db_entry)
@classmethod @classmethod
async def find(cls, name: str) -> "Location": async def find(cls, name: str):
db_entry = await col_locations.find_one({"name": {"$regex": name}}) db_entry = await col_locations.find_one({"name": {"$regex": name}})
if db_entry is None: if db_entry is None:
@@ -52,7 +52,7 @@ class Location:
return cls(**db_entry) return cls(**db_entry)
@classmethod @classmethod
async def nearby(cls, lat: float, lon: float) -> "Location": async def nearby(cls, lat: float, lon: float):
db_entry = await col_locations.find_one({"location": {"$near": [lon, lat]}}) db_entry = await col_locations.find_one({"location": {"$near": [lon, lat]}})
if db_entry is None: if db_entry is None:

View File

@@ -91,7 +91,6 @@ class PyroClient(LibPyroClient):
] ]
async def check_updates(self) -> None: async def check_updates(self) -> None:
"""Check for updates and send a message to the owner if newer version was found"""
if await self.updater.check_updates( if await self.updater.check_updates(
self.__version__, self.config["strings"]["url_updater"] self.__version__, self.config["strings"]["url_updater"]
): ):

View File

@@ -1,9 +1,8 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Any, Mapping, Tuple, Union from typing import Any, Union
import pytz
from bson import ObjectId from bson import ObjectId
from classes.location import Location from classes.location import Location
@@ -46,7 +45,7 @@ class PyroUser:
offset: int = 1, offset: int = 1,
time_hour: int = 16, time_hour: int = 16,
time_minute: int = 0, time_minute: int = 0,
) -> "PyroUser": ):
db_entry = await col_users.find_one({"id": id}) db_entry = await col_users.find_one({"id": id})
if db_entry is None: if db_entry is None:
@@ -74,7 +73,7 @@ class PyroUser:
return cls(**db_entry) return cls(**db_entry)
@classmethod @classmethod
async def from_dict(cls, **kwargs) -> "PyroUser": async def from_dict(cls, **kwargs):
if "location" in kwargs: if "location" in kwargs:
try: try:
kwargs["location"] = await Location.get(kwargs["location"]) # type: ignore kwargs["location"] = await Location.get(kwargs["location"]) # type: ignore
@@ -82,134 +81,52 @@ class PyroUser:
kwargs["location"] = None # type: ignore kwargs["location"] = None # type: ignore
return cls(**kwargs) return cls(**kwargs)
async def update_locale(self, locale: Union[str, None]) -> Union[str, None]: async def update_locale(self, locale: Union[str, None]) -> None:
"""Change user's locale stored in the database. """Change user's locale stored in the database.
### Args: ### Args:
* locale (`Union[str, None]`): New locale to be set. * locale (`Union[str, None]`): New locale to be set.
""" """
logger.info("%s's locale has been set to %s", self.id, locale) logger.debug("%s's locale has been set to %s", self.id, locale)
await col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}}) await col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}})
self.locale = locale self.locale = locale
return self.locale async def update_state(self, enabled: bool = False) -> None:
logger.debug("%s's state has been set to %s", self.id, enabled)
async def update_state(self, enabled: bool = False) -> bool:
"""Update user's state (enabled/disabled)
### Args:
* enabled (`bool`, *optional*): Whether the user is enabled. Defaults to `False`.
### Returns:
* `bool`: User's current state
"""
logger.info("%s's state has been set to %s", self.id, enabled)
await col_users.update_one({"_id": self._id}, {"$set": {"enabled": enabled}}) await col_users.update_one({"_id": self._id}, {"$set": {"enabled": enabled}})
self.enabled = enabled self.enabled = enabled
return self.enabled async def update_location(self, location_id: int = 0) -> None:
logger.debug("%s's location has been set to %s", self.id, location_id)
async def update_location(self, location_id: int) -> Location:
"""Update user's location and move their time to the new timezone (if the user had a location set previously)
### Args:
* location_id (`int`): ID of the location
### Returns:
`Location`: New location
"""
logger.info("%s's location has been set to %s", self.id, location_id)
await col_users.update_one( await col_users.update_one(
{"_id": self._id}, {"$set": {"location": location_id}} {"_id": self._id}, {"$set": {"location": location_id}}
) )
self.location = await Location.get(location_id)
location = await Location.get(location_id) async def update_offset(self, offset: int = 1) -> None:
logger.debug("%s's offset has been set to %s", self.id, offset)
# Execute if timezones of old and new locations are different
if self.location and (self.location.timezone.zone != location.timezone.zone):
# Get UTC time for selected reminder time
now_utc = datetime.now(pytz.utc).replace(
hour=self.time_hour, minute=self.time_minute, second=0, microsecond=0
)
# Get the time for the reminder time of old and new location
local_old = now_utc.astimezone(self.location.timezone)
local_new = (
location.timezone.localize(local_old.replace(tzinfo=None))
).astimezone(pytz.utc)
# Update the time to match the new timezone
await self.update_time(hour=local_new.hour, minute=local_new.minute)
self.location = location
return self.location
async def update_offset(self, offset: int = 1) -> int:
"""Update the offset of the reminder (in days)
### Args:
* offset (`int`, *optional*): Offset in days. Defaults to `1`.
### Returns:
* `int`: Offset in days
"""
logger.info("%s's offset has been set to %s", self.id, offset)
await col_users.update_one({"_id": self._id}, {"$set": {"offset": offset}}) await col_users.update_one({"_id": self._id}, {"$set": {"offset": offset}})
self.offset = offset self.offset = offset
return offset async def update_time(self, hour: int = 16, minute: int = 0) -> None:
logger.debug("%s's time has been set to %s h. %s m.", self.id, hour, minute)
async def update_time(self, hour: int = 16, minute: int = 0) -> Tuple[int, int]:
"""Update the time of the reminder (hour and minute, for UTC timezone)
### Args:
* hour (`int`, *optional*): Hour of the reminder. Defaults to `16`.
* minute (`int`, *optional*): Minute of the reminder. Defaults to `0`.
### Returns:
* `Tuple[int, int]`: Hour and minute of the reminder
"""
logger.info("%s's time has been set to %s h. %s m.", self.id, hour, minute)
await col_users.update_one( await col_users.update_one(
{"_id": self._id}, {"$set": {"time_hour": hour, "time_minute": minute}} {"_id": self._id}, {"$set": {"time_hour": hour, "time_minute": minute}}
) )
self.time_hour = hour self.time_hour = hour
self.time_minute = minute self.time_minute = minute
return self.time_hour, self.time_minute
async def delete(self) -> None: async def delete(self) -> None:
"""Delete the database record of the user""" logger.debug("%s's data has been deleted", self.id)
logger.info("%s's data has been deleted", self.id)
await col_users.delete_one({"_id": self._id}) await col_users.delete_one({"_id": self._id})
async def checkout(self) -> Mapping[str, Any]: async def checkout(self) -> Any:
"""Checkout the user's database record logger.debug("%s's data has been checked out", self.id)
### Raises:
* `KeyError`: Database record of the user was not found
### Returns:
* `Mapping[str, Any]`: Database record
"""
logger.info("%s's data has been checked out", self.id)
db_entry = await col_users.find_one({"_id": self._id}) db_entry = await col_users.find_one({"_id": self._id})
if db_entry is None: if db_entry is None:
raise KeyError( raise KeyError(
f"DB record with id {self._id} of user {self.id} was not found" f"DB record with id {self._id} of user {self.id} is not found"
) )
del db_entry["_id"] # type: ignore del db_entry["_id"] # type: ignore
@@ -238,7 +155,7 @@ class PyroUser:
logger.warning("Location %s does not have a timezone set", self.location.id) logger.warning("Location %s does not have a timezone set", self.location.id)
return ( return (
datetime.now(self.location.timezone or pytz.utc) + timedelta(days=1) datetime.now(self.location.timezone or timezone.utc) + timedelta(days=1)
).replace(hour=0, minute=0, second=0, microsecond=0) ).replace(hour=0, minute=0, second=0, microsecond=0)
def get_reminder_time(self) -> datetime: def get_reminder_time(self) -> datetime:
@@ -263,12 +180,12 @@ class PyroUser:
logger.warning("Location %s does not have a timezone set", self.location.id) logger.warning("Location %s does not have a timezone set", self.location.id)
return ( return (
datetime.now(pytz.utc) datetime.now(timezone.utc)
.replace( .replace(
hour=self.time_hour, hour=self.time_hour,
minute=self.time_minute, minute=self.time_minute,
second=0, second=0,
microsecond=0, microsecond=0,
) )
.astimezone(self.location.timezone or pytz.utc) .astimezone(self.location.timezone or timezone.utc)
) )

View File

@@ -13,4 +13,4 @@ class Migration(BaseMigration):
def downgrade(self): def downgrade(self):
sync.config_delete("update_checker", missing_ok=True) sync.config_delete("update_checker", missing_ok=True)
sync.config_delete("url_updater", "strings", missing_ok=True) sync.config_delete("url_updater", "strings")

View File

@@ -1,7 +1,6 @@
import logging import logging
from datetime import datetime from datetime import datetime, timezone
import pytz
from bson import json_util from bson import json_util
from libbot.pyrogram.classes import PyroClient from libbot.pyrogram.classes import PyroClient
@@ -15,7 +14,7 @@ logger = logging.getLogger(__name__)
async def remind(app: PyroClient) -> None: async def remind(app: PyroClient) -> None:
utcnow = datetime.now(pytz.utc) utcnow = datetime.now(timezone.utc)
logger.debug("Performing reminder lookup for %s (UTCNOW)", utcnow) logger.debug("Performing reminder lookup for %s (UTCNOW)", utcnow)
@@ -43,7 +42,13 @@ async def remind(app: PyroClient) -> None:
logger.warning("Skipping reminder for %s due to invalid location", user.id) logger.warning("Skipping reminder for %s due to invalid location", user.id)
continue continue
user_date = user.get_reminder_date().replace(tzinfo=None) try:
user_date = user.get_reminder_date()
except AttributeError:
logger.warning(
"Skipping reminder for %s due to missing attributes", user.id
)
continue
entries = await col_entries.find( entries = await col_entries.find(
{ {

View File

@@ -1,7 +1,8 @@
from datetime import datetime from datetime import datetime
from typing import Union from typing import Union
import pytz from pytz import UTC
from pytz import timezone as pytz_timezone
def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime: def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
@@ -17,9 +18,7 @@ def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
* `datetime`: Timezone unaware datetime in UTC with timezone's offset applied to it. * `datetime`: Timezone unaware datetime in UTC with timezone's offset applied to it.
""" """
timezone = "UTC" if timezone is None else timezone timezone = "UTC" if timezone is None else timezone
return ( return pytz_timezone(timezone).localize(date).astimezone(UTC).replace(tzinfo=None)
pytz.timezone(timezone).localize(date).astimezone(pytz.utc).replace(tzinfo=None)
)
def from_utc(date: datetime, timezone: Union[str, None] = None) -> datetime: def from_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
@@ -36,5 +35,8 @@ def from_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
""" """
timezone = "UTC" if timezone is None else timezone timezone = "UTC" if timezone is None else timezone
return ( return (
pytz.utc.localize(date).astimezone(pytz.timezone(timezone)).replace(tzinfo=None) pytz_timezone("UTC")
.localize(date)
.astimezone(pytz_timezone(timezone))
.replace(tzinfo=None)
) )

View File

@@ -1,7 +1,6 @@
import logging import logging
from datetime import datetime from datetime import datetime, timezone
import pytz
from convopyro import listen_message from convopyro import listen_message
from pyrogram import filters from pyrogram import filters
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
@@ -66,9 +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 = ( garbage_time = (
datetime.now(pytz.utc) datetime.now(timezone.utc)
.replace(hour=user.time_hour, minute=user.time_minute) .replace(hour=user.time_hour, minute=user.time_minute)
.astimezone(user.location.timezone or pytz.utc) .astimezone(user.location.timezone or timezone.utc)
) )
await answer.reply_text( await answer.reply_text(

View File

@@ -1,7 +1,6 @@
import logging import logging
from datetime import datetime from datetime import datetime, timezone
import pytz
from convopyro import listen_message from convopyro import listen_message
from pyrogram import filters from pyrogram import filters
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
@@ -55,30 +54,34 @@ async def command_set_time(app: PyroClient, message: Message):
break break
# Time we got from the user now = datetime.now()
parsed_time = datetime.strptime(answer.text, "%H:%M")
# Datetime user means in their timezone parsed_time = datetime.strptime(answer.text, "%H:%M").replace(
user_time = datetime.now(user.location.timezone).replace( year=now.year,
hour=parsed_time.hour, minute=parsed_time.minute, second=0, microsecond=0 month=now.month,
day=now.day,
second=0,
microsecond=0,
tzinfo=timezone.utc,
) )
# Datetime in user's timezone moved to UTC timezone user_time = parsed_time.astimezone(user.location.timezone or timezone.utc)
utc_time = user_time.astimezone(pytz.utc)
await user.update_time(hour=utc_time.hour, minute=utc_time.minute) await user.update_time(hour=user_time.hour, minute=user_time.minute)
logger.info( logger.info(
"User %s has selected notification time of %s (%s UTC)", "User %s has selected notification time of %s (%s UTC)",
user.id, user.id,
parsed_time.strftime("%H:%M"),
user_time.strftime("%H:%M"), user_time.strftime("%H:%M"),
utc_time.strftime("%H:%M"),
) )
garbage_time = parsed_time.strftime(app._("time", "formats", locale=user.locale))
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(
offset=user.offset, offset=user.offset,
time=user_time.strftime(app._("time", "formats", locale=user.locale)), time=garbage_time,
toggle_notice=( toggle_notice=(
"" if user.enabled else app._("toggle", "messages", locale=user.locale) "" if user.enabled else app._("toggle", "messages", locale=user.locale)
), ),

View File

@@ -72,9 +72,12 @@ async def command_setup(app: PyroClient, message: Message):
await user.update_location(location.id) await user.update_location(location.id)
try:
user_time = user.get_reminder_time().strftime( user_time = user.get_reminder_time().strftime(
app._("time", "formats", locale=user.locale) app._("time", "formats", locale=user.locale)
) )
except AttributeError:
user_time = "N/A"
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

@@ -88,9 +88,12 @@ async def command_start(app: PyroClient, message: Message):
await user.update_location(location.id) await user.update_location(location.id)
try:
user_time = user.get_reminder_time().strftime( user_time = user.get_reminder_time().strftime(
app._("time", "formats", locale=user.locale) app._("time", "formats", locale=user.locale)
) )
except AttributeError:
user_time = "N/A"
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(

View File

@@ -1,6 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import pytz
from pyrogram import filters from pyrogram import filters
from pyrogram.types import Message from pyrogram.types import Message
@@ -24,11 +23,11 @@ async def command_upcoming(app: PyroClient, message: Message):
date_min = ( date_min = (
datetime.now(user.location.timezone).replace(second=0, microsecond=0) datetime.now(user.location.timezone).replace(second=0, microsecond=0)
).replace(tzinfo=pytz.utc) ).replace(tzinfo=timezone.utc)
date_max = ( date_max = (
datetime.now(user.location.timezone).replace(second=0, microsecond=0) datetime.now(user.location.timezone).replace(second=0, microsecond=0)
+ timedelta(days=30) + timedelta(days=30)
).replace(tzinfo=pytz.utc) ).replace(tzinfo=timezone.utc)
entries = [ entries = [
await GarbageEntry.from_record(entry) await GarbageEntry.from_record(entry)

View File

@@ -2,7 +2,7 @@ aiohttp~=3.9.5
apscheduler~=3.10.4 apscheduler~=3.10.4
convopyro==0.5 convopyro==0.5
mongodb-migrations==1.3.1 mongodb-migrations==1.3.1
pytz>=2024.1 pytz<=2023.2
tgcrypto==1.2.5 tgcrypto==1.2.5
ujson>=5.0.0 ujson>=5.0.0
uvloop==0.19.0 uvloop==0.19.0