Compare commits

..

49 Commits
v0.1.1 ... main

Author SHA1 Message Date
a5824dbd6b Merge pull request 'v0.1.3' (#74) from dev into main
Reviewed-on: #74
2024-10-10 12:54:38 +03:00
f8472b1b3f Bump version tp 0.1.3 2024-10-10 12:52:38 +03:00
13be95f0f8 Closes #70 2024-10-09 15:10:55 +00:00
524087f59f Merge pull request 'Update dependency async_pymongo to v0.1.9' (#73) from renovate/async_pymongo-0.x into dev
Reviewed-on: #73
2024-10-08 16:49:38 +03:00
cc66cc386b Update dependency async_pymongo to v0.1.9 2024-10-08 16:29:25 +03:00
39f7904bdc Merge pull request 'Update dependency async_pymongo to v0.1.8' (#72) from renovate/async_pymongo-0.x into dev
Reviewed-on: #72
2024-09-25 22:25:42 +03:00
b5173a8dba Update dependency async_pymongo to v0.1.8 2024-09-25 17:13:14 +03:00
0a06e8493f Merge pull request 'Update dependency async_pymongo to v0.1.7' (#71) from renovate/async_pymongo-0.x into dev
Reviewed-on: #71
2024-09-21 01:52:54 +03:00
c2ee35b3d9 Update dependency async_pymongo to v0.1.7 2024-09-20 17:24:31 +03:00
896262b83e Merge pull request 'Update dependency uvloop to v0.20.0' (#69) from renovate/uvloop-0.x into dev
Reviewed-on: #69
2024-08-15 23:48:30 +03:00
f6b1749408 Update dependency uvloop to v0.20.0 2024-08-15 23:25:25 +03:00
6867b64a18
Should resolve #87 2024-08-10 14:10:59 +02:00
f42117e542 revert a5a513cb82
revert Merge pull request 'Update dependency aiohttp to ~=3.10.0' (#65) from renovate/aiohttp-3.x into dev

Reviewed-on: #65
2024-08-01 00:47:16 +03:00
a5a513cb82 Merge pull request 'Update dependency aiohttp to ~=3.10.0' (#65) from renovate/aiohttp-3.x into dev
Reviewed-on: #65
2024-07-31 02:13:51 +03:00
3fef2eb028 Update dependency aiohttp to ~=3.10.0 2024-07-31 00:47:50 +03:00
11ca3223ab Merge pull request 'Update dependency libbot to v3.2.3' (#64) from renovate/libbot-3.x into dev
Reviewed-on: #64
2024-07-10 08:12:59 +03:00
6b138126c1 Update dependency libbot to v3.2.3 2024-07-10 00:43:50 +03:00
e6adb03f61 Merge pull request 'Update dependency async_pymongo to v0.1.6' (#63) from renovate/async_pymongo-0.x into dev
Reviewed-on: #63
2024-06-23 14:33:51 +03:00
d51fa1e04c Update dependency async_pymongo to v0.1.6 2024-06-23 13:30:14 +03:00
8b2456c2fd Merge pull request 'Update dependency async_pymongo to v0.1.5' (#62) from renovate/async_pymongo-0.x into dev
Reviewed-on: #62
2024-06-02 12:57:32 +03:00
6a6b4cd6cd Selected async_pymongo from PyPi 2024-06-02 12:56:46 +03:00
852f4307f8 Update dependency async_pymongo to v0.1.5 2024-06-01 15:32:20 +03:00
e73797d819
Replaced "source code" with "learn more" 2024-05-31 00:03:24 +02:00
bfd99a44a6 Merge pull request 'Changed pytz<=2024.1 to pytz>=2024.1' (#61) from dev into main
Reviewed-on: #61
2024-05-31 00:55:30 +03:00
d078ab37d8 Changed pytz<=2024.1 to pytz>=2024.1 2024-05-31 00:55:07 +03:00
ea0ab6443f Merge pull request 'v0.1.2' (#60) from dev into main
Reviewed-on: #60
2024-05-31 00:46:09 +03:00
04ee8e9c60
Improved docstrings and set some logging events to INFO level instead of DEBUG 2024-05-30 23:41:52 +02:00
2c15bbb4d2 Merge pull request 'Update dependency pytz to v2024' (#59) from renovate/pytz-2024.x into dev
Reviewed-on: #59
2024-05-31 00:31:01 +03:00
99d621d90f Update dependency pytz to v2024 2024-05-31 00:28:46 +03:00
dc389ac1b7 Merge pull request 'Possibly fixed #8' (#57) from profitroll/timezones-fix into dev
Reviewed-on: #57
2024-05-31 00:25:53 +03:00
3f20fdb46a Removed wrong localize 2024-05-31 00:25:40 +03:00
65e9e830c1 Fixed possible bugs and renamed pytz imports 2024-05-31 00:25:40 +03:00
1c76c8d911 Added automatic timezone update on location change; Update methods now return changed values 2024-05-31 00:25:40 +03:00
e307d60e8e pytz.utc instead of datetime.timezone.utc 2024-05-31 00:25:40 +03:00
0562521f0d Improved typing 2024-05-31 00:25:40 +03:00
7293cafd2e Removed unnecessary checks 2024-05-31 00:25:40 +03:00
b5bfbcd375 Attempted a fix for #8 2024-05-31 00:25:40 +03:00
de483cd450 Default time is now 16:00 2024-05-31 00:25:40 +03:00
94e229949d Added get_reminder_date() and get_reminder_time() methods; update_* methods now also change the attributes of the object 2024-05-31 00:25:40 +03:00
b7fc1715fd Added pytz as a dependency 2024-05-31 00:25:40 +03:00
f6731d5734
Fixed downgrade for strings.url_updater config key 2024-05-30 12:51:47 +02:00
e1a7b6309e
Closes #49 2024-05-26 22:56:30 +02:00
2404ee9095
Closes #48 2024-05-26 22:56:11 +02:00
3bd4f794d3 Merge pull request 'Update dependency libbot to v3.2.2' (#56) from renovate/libbot-3.x into dev
Reviewed-on: #56
2024-05-26 23:55:09 +03:00
bebd6b4e4f Update dependency libbot to v3.2.2 2024-05-26 23:12:56 +03:00
fd52c8f74e Merge pull request 'Update dependency libbot to v3.2.1' (#55) from renovate/libbot-3.x into dev
Reviewed-on: #55
2024-05-26 19:41:24 +03:00
b2f09339ee Update dependency libbot to v3.2.1 2024-05-26 19:01:48 +03:00
95a9b5cb2b Merge pull request 'Update dependency libbot to v3.1.0' (#54) from renovate/libbot-3.x into dev
Reviewed-on: #54
2024-05-24 22:46:56 +03:00
b7779fffd0 Update dependency libbot to v3.1.0 2024-05-24 22:42:27 +03:00
23 changed files with 377 additions and 111 deletions

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
import logging
from datetime import datetime, timedelta
from typing import List, Union
from aiohttp import ClientSession
from apscheduler.triggers.cron import CronTrigger
from libbot.pyrogram.classes import PyroClient as LibPyroClient
from pymongo import ASCENDING, GEOSPHERE, TEXT
@ -7,18 +10,32 @@ from pyrogram.types import User
from classes.location import Location
from classes.pyrouser import PyroUser
from classes.updater import Updater
from modules.database_api import col_locations
from modules.reminder import remind
logger = logging.getLogger(__name__)
class PyroClient(LibPyroClient):
def __init__(self, **kwargs):
self.__version__ = (0, 1, 3)
super().__init__(**kwargs)
self.updater = Updater()
self.contexts = []
if self.scheduler is not None:
self.scheduler.add_job(
remind, CronTrigger.from_crontab("* * * * *"), args=(self,)
)
self.contexts = []
if self.config["update_checker"]:
self.scheduler.add_job(
self.check_updates,
CronTrigger.from_crontab("0 12 */3 * *"),
next_run_time=datetime.now() + timedelta(seconds=10),
)
async def start(self, **kwargs):
await col_locations.create_index(
@ -31,6 +48,10 @@ class PyroClient(LibPyroClient):
await col_locations.create_index([("name", TEXT)], name="location_name")
return await super().start(**kwargs)
async def stop(self, **kwargs):
await self.updater.client_session.close()
await super().stop(**kwargs)
async def find_user(self, user: Union[int, User]) -> PyroUser:
"""Find User by it's ID or User object.
@ -68,3 +89,25 @@ class PyroClient(LibPyroClient):
return [
await Location.get(record["id"]) async for record in col_locations.find({})
]
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(
self.__version__, self.config["strings"]["url_updater"]
):
try:
release = await self.updater.get_latest_release(
self.config["strings"]["url_updater"]
)
except Exception as exc:
logger.error("Could not fetch the latest version: %s", exc)
return
await self.send_message(
self.owner,
self._("update_available", "messages").format(
version_current=f"v{'.'.join(str(subversion) for subversion in self.__version__)}",
version_new=release["tag_name"],
release_url=release["html_url"],
),
)

View File

@ -1,7 +1,9 @@
import logging
from dataclasses import dataclass
from typing import Any, Union
from datetime import datetime, timedelta
from typing import Any, Mapping, Tuple, Union
import pytz
from bson import ObjectId
from classes.location import Location
@ -42,9 +44,9 @@ class PyroUser:
enabled: bool = True,
location_id: int = 0,
offset: int = 1,
time_hour: int = 18,
time_hour: int = 16,
time_minute: int = 0,
):
) -> "PyroUser":
db_entry = await col_users.find_one({"id": id})
if db_entry is None:
@ -72,7 +74,7 @@ class PyroUser:
return cls(**db_entry)
@classmethod
async def from_dict(cls, **kwargs):
async def from_dict(cls, **kwargs) -> "PyroUser":
if "location" in kwargs:
try:
kwargs["location"] = await Location.get(kwargs["location"]) # type: ignore
@ -80,48 +82,193 @@ class PyroUser:
kwargs["location"] = None # type: ignore
return cls(**kwargs)
async def update_locale(self, locale: Union[str, None]) -> None:
async def update_locale(self, locale: Union[str, None]) -> Union[str, None]:
"""Change user's locale stored in the database.
### Args:
* locale (`Union[str, None]`): New locale to be set.
"""
logger.debug("%s's locale has been set to %s", self.id, locale)
logger.info("%s's locale has been set to %s", self.id, locale)
await col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}})
async def update_state(self, enabled: bool = False) -> None:
logger.debug("%s's state has been set to %s", self.id, enabled)
self.locale = locale
return self.locale
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}})
async def update_location(self, location_id: int = 0) -> None:
logger.debug("%s's location has been set to %s", self.id, location_id)
self.enabled = enabled
return self.enabled
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(
{"_id": self._id}, {"$set": {"location": location_id}}
)
async def update_offset(self, offset: int = 1) -> None:
logger.debug("%s's offset has been set to %s", self.id, offset)
location = await Location.get(location_id)
# 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}})
async def update_time(self, hour: int = 18, minute: int = 0) -> None:
logger.debug("%s's time has been set to %s h. %s m.", self.id, hour, minute)
self.offset = offset
return offset
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(
{"_id": self._id}, {"$set": {"time_hour": hour, "time_minute": minute}}
)
self.time_hour = hour
self.time_minute = minute
return self.time_hour, self.time_minute
async def delete(self) -> None:
logger.debug("%s's data has been deleted", self.id)
"""Delete the database record of the user"""
logger.info("%s's data has been deleted", self.id)
await col_users.delete_one({"_id": self._id})
async def checkout(self) -> Any:
logger.debug("%s's data has been checked out", self.id)
async def checkout(self) -> Mapping[str, Any]:
"""Checkout the user's database record
### 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})
if db_entry is None:
raise KeyError(
f"DB record with id {self._id} of user {self.id} is not found"
f"DB record with id {self._id} of user {self.id} was not found"
)
del db_entry["_id"] # type: ignore
return db_entry
def get_reminder_date(self) -> datetime:
"""Get next reminder date (year, month and day)
### Raises:
* `AttributeError`: Some attribute(s) are missing
### Returns:
* `datetime`: Datetime object of the next reminder date
"""
if self.location is None:
logger.warning(
"Could not get the reminder date for %s: User does not have some attribute(s) set",
self.id,
)
raise AttributeError(
f"Could not get the reminder date for {self.id}: User does not have some attribute(s) set"
)
if not self.location.timezone:
logger.warning("Location %s does not have a timezone set", self.location.id)
return (
datetime.now(self.location.timezone or pytz.utc) + timedelta(days=1)
).replace(hour=0, minute=0, second=0, microsecond=0)
def get_reminder_time(self) -> datetime:
"""Get reminder time (hour and minute)
### Raises:
* `AttributeError`: Some attribute(s) are missing
### Returns:
* `datetime`: Datetime object of the next reminder date
"""
if self.time_hour is None or self.time_minute is None or self.location is None:
logger.warning(
"Could not get the reminder time for %s: User does not have some attribute(s) set",
self.id,
)
raise AttributeError(
f"Could not get the reminder time for {self.id}: User does not have some attribute(s) set"
)
if not self.location.timezone:
logger.warning("Location %s does not have a timezone set", self.location.id)
return (
datetime.now(pytz.utc)
.replace(
hour=self.time_hour,
minute=self.time_minute,
second=0,
microsecond=0,
)
.astimezone(self.location.timezone or pytz.utc)
)

50
classes/updater.py Normal file
View File

@ -0,0 +1,50 @@
import logging
from typing import Any, Dict, Tuple, Union
from aiohttp import ClientSession
logger = logging.getLogger(__name__)
class Updater:
def __init__(self, client_session: Union[ClientSession, None] = None) -> None:
self.client_session: Union[ClientSession, None] = client_session
async def check_updates(
self, version_current: Tuple[int, int, int], api_url: str
) -> bool:
if not self.client_session:
self.client_session = ClientSession()
response = await self.client_session.get(api_url)
if response.status != 200:
return False
try:
version_latest = (await response.json())["tag_name"][1:].split(".")
except Exception as exc:
logger.error("Error parsing latest version: %s", exc)
return False
return any(
version_current[index] < int(subversion)
for index, subversion in enumerate(version_latest)
)
async def get_latest_release(self, api_url: str) -> Dict[str, Any]:
if not self.client_session:
self.client_session = ClientSession()
response = await self.client_session.get(api_url)
if response.status != 200:
raise RuntimeError(f"Could not fetch latest release: {response.status}")
try:
return await response.json()
except Exception as exc:
logger.error("Error parsing latest release: %s", exc)
raise RuntimeError(
f"Error parsing latest release: {response.status}"
) from exc

View File

@ -32,6 +32,8 @@
"disabled_plugins": [],
"strings": {
"url_repo": "https://git.end-play.xyz/GarbageReminder/TelegramBot",
"url_contact": "https://git.end-play.xyz/GarbageReminder/TelegramBot/issues"
}
"url_contact": "https://git.end-play.xyz/GarbageReminder/TelegramBot/issues",
"url_updater": "https://git.end-play.xyz/api/v1/repos/GarbageReminder/TelegramBot/releases/latest"
},
"update_checker": true
}

View File

@ -8,7 +8,7 @@
},
"bot": {
"name": "Garbage Reminder",
"about": "Nie wieder Müllabfuhrtermin verpassen. Quellcode: https://garbagebot.eu",
"about": "Nie wieder Müllabfuhrtermin verpassen. Mehr erfahren: https://garbagebot.eu",
"description": "Sie können Erinnerungen an die Müllabfuhr für Orte Ihrer Wahl erhalten.\n\nVerwenden Sie /help, um die Funktionsweise des Bots besser zu verstehen, oder verwenden Sie /setup, um Ihre Erinnerungen zu konfigurieren."
},
"formats": {
@ -78,7 +78,8 @@
"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": "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}",
"update_available": "Es gibt eine neue Version von GarbageBot!\n\nVersion: `{version_current}` -> `{version_new}`\n\n[Release-Seite]({release_url}) | [Update-Anleitung](https://garbagebot.eu/bot_telegram/upgrading)"
},
"force_replies": {
"import": "JSON mit Abfalltermine",

View File

@ -8,7 +8,7 @@
},
"bot": {
"name": "Garbage Reminder",
"about": "Never forget about garbage collection again. Source code: https://garbagebot.eu",
"about": "Never forget about garbage collection again. Learn more: https://garbagebot.eu",
"description": "You can receive reminders about garbage collection for locations of your choice.\n\nUse /help to better understand how the bot works or use /setup to configure your reminders."
},
"formats": {
@ -78,7 +78,8 @@
"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": "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}",
"update_available": "There is a new version of GarbageBot available!\n\nVersion: `{version_current}` -> `{version_new}`\n\n[Release page]({release_url}) | [Update instructions](https://garbagebot.eu/bot_telegram/upgrading)"
},
"buttons": {
"delete_confirm": "I agree and want to proceed",

View File

@ -8,7 +8,7 @@
},
"bot": {
"name": "Garbage Reminder 🇺🇦",
"about": "Більше ніколи не забувайте про вивезення сміття. Вихідний код: https://garbagebot.eu",
"about": "Більше ніколи не забувайте про вивезення сміття. Дізнатись більше: https://garbagebot.eu",
"description": "Ви можете отримувати нагадування про вивезення сміття для обраних вами місць.\n\nВикористовуйте /help, щоб краще зрозуміти, як працює бот, або /setup, щоб налаштувати нагадування."
},
"formats": {
@ -78,7 +78,8 @@
"toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.",
"toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.",
"upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**",
"upcoming": "Найближчі вивози сміття:\n\n{entries}"
"upcoming": "Найближчі вивози сміття:\n\n{entries}",
"update_available": "Доступна нова версія GarbageBot!\n\nВерсія: `{version_current}` -> `{version_new}`\n\n[Сторінка релізу]({release_url}) | [Інструкція з оновлення](https://garbagebot.eu/bot_telegram/upgrading)"
},
"buttons": {
"delete_confirm": "Я погоджуюсь і хочу продовжити",

View File

@ -8,7 +8,7 @@
},
"bot": {
"name": "Garbage Reminder 🇺🇦",
"about": "Більше ніколи не забувайте про вивезення сміття. Вихідний код: https://garbagebot.eu",
"about": "Більше ніколи не забувайте про вивезення сміття. Дізнатись більше: https://garbagebot.eu",
"description": "Ви можете отримувати нагадування про вивезення сміття для обраних вами місць.\n\nВикористовуйте /help, щоб краще зрозуміти, як працює бот, або /setup, щоб налаштувати нагадування."
},
"formats": {
@ -78,7 +78,8 @@
"toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.",
"toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.",
"upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**",
"upcoming": "Найближчі вивози сміття:\n\n{entries}"
"upcoming": "Найближчі вивози сміття:\n\n{entries}",
"update_available": "Доступна нова версія GarbageBot!\n\nВерсія: `{version_current}` -> `{version_new}`\n\n[Сторінка релізу]({release_url}) | [Інструкція з оновлення](https://garbagebot.eu/bot_telegram/upgrading)"
},
"buttons": {
"delete_confirm": "Я погоджуюсь і хочу продовжити",

14
main.py
View File

@ -4,6 +4,7 @@ from argparse import ArgumentParser
from os import getpid
from pathlib import Path
from aiohttp import ClientSession
from convopyro import Conversation
from libbot import sync
@ -35,13 +36,16 @@ with contextlib.suppress(ImportError):
def main():
client = PyroClient(
scheduler=scheduler, commands_source=sync.json_read(Path("commands.json"))
)
Conversation(client)
if args.migrate:
migrate_database()
logger.info("Migration finished. Exiting...")
exit()
client = PyroClient(
scheduler=scheduler,
commands_source=sync.json_read(Path("commands.json")),
)
Conversation(client)
try:
client.run()

View File

@ -0,0 +1,16 @@
from libbot import sync
from mongodb_migrations.base import BaseMigration
class Migration(BaseMigration):
def upgrade(self):
sync.config_set("update_checker", True)
sync.config_set(
"url_updater",
"https://git.end-play.xyz/api/v1/repos/GarbageReminder/TelegramBot/releases/latest",
"strings",
)
def downgrade(self):
sync.config_delete("update_checker", missing_ok=True)
sync.config_delete("url_updater", "strings", missing_ok=True)

View File

@ -1,6 +1,7 @@
import logging
from datetime import datetime, timedelta
from datetime import datetime
import pytz
from bson import json_util
from libbot.pyrogram.classes import PyroClient
@ -9,13 +10,12 @@ from classes.location import Location
from classes.pyrouser import PyroUser
from modules.database import col_users
from modules.database_api import col_entries
from modules.utils import from_utc
logger = logging.getLogger(__name__)
async def remind(app: PyroClient) -> None:
utcnow = datetime.utcnow()
utcnow = datetime.now(pytz.utc)
logger.debug("Performing reminder lookup for %s (UTCNOW)", utcnow)
@ -40,12 +40,10 @@ async def remind(app: PyroClient) -> None:
try:
location: Location = await app.get_location(user.location.id) # type: ignore
except ValueError:
logger.warning("Skipping reminder for %s due to invalid location", user.id)
continue
user_date = from_utc(
datetime.utcnow() + timedelta(days=user.offset),
user.location.timezone.zone,
).replace(hour=0, minute=0, second=0, microsecond=0)
user_date = user.get_reminder_date().replace(tzinfo=None)
entries = await col_entries.find(
{

View File

@ -1,12 +1,13 @@
from datetime import datetime
from typing import Union
from pytz import UTC
from pytz import timezone as pytz_timezone
import pytz
def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
"""Move timezone unaware datetime object to UTC timezone and return it.
"""*DEPRECATED AND WILL BE REMOVED IN FUTURE RELEASES*
Move timezone unaware datetime object to UTC timezone and return it.
### Args:
* date (`datetime`): Datetime to be converted.
@ -16,11 +17,15 @@ def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
* `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)
return (
pytz.timezone(timezone).localize(date).astimezone(pytz.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.
"""*DEPRECATED AND WILL BE REMOVED IN FUTURE RELEASES*
Move timezone unaware datetime object to the timezone specified and return it.
### Args:
* date (`datetime`): Datetime to be converted.
@ -31,8 +36,5 @@ def from_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
"""
timezone = "UTC" if timezone is None else timezone
return (
pytz_timezone("UTC")
.localize(date)
.astimezone(pytz_timezone(timezone))
.replace(tzinfo=None)
pytz.utc.localize(date).astimezone(pytz.timezone(timezone)).replace(tzinfo=None)
)

View File

@ -1,13 +1,13 @@
import logging
from datetime import datetime
import pytz
from convopyro import listen_message
from pyrogram import filters
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__)
@ -65,18 +65,19 @@ async def command_set_offset(app: PyroClient, message: Message):
logger.info("User %s has set offset to %s", user.id, offset)
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"))
garbage_time = (
datetime.now(pytz.utc)
.replace(hour=user.time_hour, minute=user.time_minute)
.astimezone(user.location.timezone or pytz.utc)
)
await answer.reply_text(
app._("set_offset_finished", "messages", locale=user.locale).format(
offset=offset,
time=garbage_time,
toggle_notice=""
if user.enabled
else app._("toggle", "messages", locale=user.locale),
time=garbage_time.strftime(app._("time", "formats", locale=user.locale)),
toggle_notice=(
"" if user.enabled else app._("toggle", "messages", locale=user.locale)
),
),
reply_markup=ReplyKeyboardRemove(),
)

View File

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

View File

@ -1,5 +1,4 @@
import logging
from datetime import datetime
from convopyro import listen_message
from libbot import i18n
@ -11,7 +10,6 @@ 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__)
@ -74,10 +72,9 @@ async def command_setup(app: PyroClient, message: Message):
await user.update_location(location.id)
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))
user_time = user.get_reminder_time().strftime(
app._("time", "formats", locale=user.locale)
)
await message.reply_text(
app._("setup_finished", "messages", locale=user.locale).format(

View File

@ -1,5 +1,3 @@
from datetime import datetime
from convopyro import listen_message
from pyrogram import filters
from pyrogram.types import (
@ -11,7 +9,6 @@ from pyrogram.types import (
from classes.pyroclient import PyroClient
from modules import custom_filters
from modules.utils import from_utc
@PyroClient.on_message(
@ -91,13 +88,15 @@ async def command_start(app: PyroClient, message: Message):
await user.update_location(location.id)
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))
user_time = user.get_reminder_time().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
name=location.name,
offset=user.offset,
time=user_time,
),
reply_markup=ReplyKeyboardRemove(),
)

View File

@ -1,11 +1,8 @@
from datetime import datetime
from pyrogram import filters
from pyrogram.types import Message
from classes.pyroclient import PyroClient
from modules import custom_filters
from modules.utils import from_utc
@PyroClient.on_message(
@ -16,16 +13,18 @@ async def command_toggle(app: PyroClient, message: Message):
await user.update_state(not user.enabled)
if user.enabled:
if not user.enabled:
await message.reply_text(
app._("toggle_disabled", "messages", locale=user.locale)
)
return
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"))
try:
user_time = user.get_reminder_time().strftime(
app._("time", "formats", locale=user.locale)
)
except AttributeError:
user_time = "N/A"
if user.location is None:
await message.reply_text(

View File

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

View File

@ -1,11 +1,12 @@
aiohttp~=3.9.5
aiohttp~=3.10.2
apscheduler~=3.10.4
async_pymongo==0.1.9
convopyro==0.5
mongodb-migrations==1.3.1
pytz>=2024.1
tgcrypto==1.2.5
ujson>=5.0.0
uvloop==0.19.0
uvloop==0.20.0
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
async_pymongo==0.1.4
libbot[speed,pyrogram]==3.0.0
libbot[speed,pyrogram]==3.2.3
pykeyboard==0.1.7

View File

@ -4,6 +4,6 @@
"enabled": true,
"location": 1,
"offset": 1,
"time_hour": 18,
"time_hour": 16,
"time_minute": 0
}