Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
a5824dbd6b | |||
f8472b1b3f | |||
13be95f0f8 | |||
524087f59f | |||
cc66cc386b | |||
39f7904bdc | |||
b5173a8dba | |||
0a06e8493f | |||
c2ee35b3d9 | |||
896262b83e | |||
f6b1749408 | |||
6867b64a18 | |||
f42117e542 | |||
a5a513cb82 | |||
3fef2eb028 | |||
11ca3223ab | |||
6b138126c1 | |||
e6adb03f61 | |||
d51fa1e04c | |||
8b2456c2fd | |||
6a6b4cd6cd | |||
852f4307f8 | |||
e73797d819 | |||
bfd99a44a6 | |||
d078ab37d8 | |||
ea0ab6443f | |||
04ee8e9c60 | |||
2c15bbb4d2 | |||
99d621d90f | |||
dc389ac1b7 | |||
3f20fdb46a | |||
65e9e830c1 | |||
1c76c8d911 | |||
e307d60e8e | |||
0562521f0d | |||
7293cafd2e | |||
b5bfbcd375 | |||
de483cd450 | |||
94e229949d | |||
b7fc1715fd | |||
f6731d5734 | |||
e1a7b6309e | |||
2404ee9095 | |||
3bd4f794d3 | |||
bebd6b4e4f | |||
fd52c8f74e | |||
b2f09339ee | |||
95a9b5cb2b | |||
b7779fffd0 |
@ -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:
|
||||
|
@ -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"]
|
||||
]
|
||||
|
@ -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:
|
||||
|
@ -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"],
|
||||
),
|
||||
)
|
||||
|
@ -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
50
classes/updater.py
Normal 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
|
@ -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
|
||||
}
|
@ -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",
|
||||
@ -99,4 +100,4 @@
|
||||
"callbacks": {
|
||||
"locale_set": "Ihre Sprache ist jetzt: {locale}"
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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": "Я погоджуюсь і хочу продовжити",
|
||||
@ -99,4 +100,4 @@
|
||||
"set_offset": "Кількість днів",
|
||||
"set_time": "Час у вигляді ГГ:ХХ"
|
||||
}
|
||||
}
|
||||
}
|
@ -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": "Я погоджуюсь і хочу продовжити",
|
||||
@ -99,4 +100,4 @@
|
||||
"set_offset": "Кількість днів",
|
||||
"set_time": "Час у вигляді ГГ:ХХ"
|
||||
}
|
||||
}
|
||||
}
|
14
main.py
14
main.py
@ -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()
|
||||
|
16
migrations/202405261500.py
Normal file
16
migrations/202405261500.py
Normal 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)
|
@ -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(
|
||||
{
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -4,6 +4,6 @@
|
||||
"enabled": true,
|
||||
"location": 1,
|
||||
"offset": 1,
|
||||
"time_hour": 18,
|
||||
"time_hour": 16,
|
||||
"time_minute": 0
|
||||
}
|
Reference in New Issue
Block a user