4 Commits

19 changed files with 63 additions and 251 deletions

View File

@@ -1,8 +1,5 @@
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
@@ -10,32 +7,18 @@ 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, 2)
super().__init__(**kwargs)
self.updater = Updater(ClientSession())
self.contexts = []
if self.scheduler is not None:
self.scheduler.add_job(
remind, CronTrigger.from_crontab("* * * * *"), args=(self,)
)
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),
)
self.contexts = []
async def start(self, **kwargs):
await col_locations.create_index(
@@ -48,10 +31,6 @@ 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.
@@ -89,24 +68,3 @@ class PyroClient(LibPyroClient):
return [
await Location.get(record["id"]) async for record in col_locations.find({})
]
async def check_updates(self) -> None:
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,6 +1,5 @@
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Union
from bson import ObjectId
@@ -43,7 +42,7 @@ class PyroUser:
enabled: bool = True,
location_id: int = 0,
offset: int = 1,
time_hour: int = 16,
time_hour: int = 18,
time_minute: int = 0,
):
db_entry = await col_users.find_one({"id": id})
@@ -89,32 +88,26 @@ class PyroUser:
"""
logger.debug("%s's locale has been set to %s", self.id, locale)
await col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}})
self.locale = locale
async def update_state(self, enabled: bool = False) -> None:
logger.debug("%s's state has been set to %s", self.id, enabled)
await col_users.update_one({"_id": self._id}, {"$set": {"enabled": enabled}})
self.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)
await col_users.update_one(
{"_id": self._id}, {"$set": {"location": location_id}}
)
self.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)
await col_users.update_one({"_id": self._id}, {"$set": {"offset": offset}})
self.offset = offset
async def update_time(self, hour: int = 16, minute: int = 0) -> None:
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)
await col_users.update_one(
{"_id": self._id}, {"$set": {"time_hour": hour, "time_minute": minute}}
)
self.time_hour = hour
self.time_minute = minute
async def delete(self) -> None:
logger.debug("%s's data has been deleted", self.id)
@@ -132,60 +125,3 @@ class PyroUser:
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 timezone.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(timezone.utc)
.replace(
hour=self.time_hour,
minute=self.time_minute,
second=0,
microsecond=0,
)
.astimezone(self.location.timezone or timezone.utc)
)

View File

@@ -1,44 +0,0 @@
import logging
from typing import Any, Dict, Tuple
from aiohttp import ClientSession
logger = logging.getLogger(__name__)
class Updater:
def __init__(self, client_session: ClientSession) -> None:
self.client_session: ClientSession = client_session
async def check_updates(
self, version_current: Tuple[int, int, int], api_url: str
) -> bool:
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]:
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,8 +32,6 @@
"disabled_plugins": [],
"strings": {
"url_repo": "https://git.end-play.xyz/GarbageReminder/TelegramBot",
"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
"url_contact": "https://git.end-play.xyz/GarbageReminder/TelegramBot/issues"
}
}

View File

@@ -78,8 +78,7 @@
"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}",
"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)"
"upcoming": "Bevorstehende Müllabfuhr:\n\n{entries}"
},
"force_replies": {
"import": "JSON mit Abfalltermine",

View File

@@ -78,8 +78,7 @@
"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}",
"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)"
"upcoming": "Upcoming garbage collection:\n\n{entries}"
},
"buttons": {
"delete_confirm": "I agree and want to proceed",

View File

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

View File

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

View File

@@ -35,16 +35,14 @@ with contextlib.suppress(ImportError):
def main():
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)
if args.migrate:
migrate_database()
try:
client.run()
except KeyboardInterrupt:

View File

@@ -1,16 +0,0 @@
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")

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timezone
from datetime import datetime, timedelta
from bson import json_util
from libbot.pyrogram.classes import PyroClient
@@ -9,12 +9,13 @@ 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.now(timezone.utc)
utcnow = datetime.utcnow()
logger.debug("Performing reminder lookup for %s (UTCNOW)", utcnow)
@@ -39,16 +40,12 @@ 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
try:
user_date = user.get_reminder_date()
except AttributeError:
logger.warning(
"Skipping reminder for %s due to missing attributes", 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)
entries = await col_entries.find(
{

View File

@@ -6,9 +6,7 @@ from pytz import timezone as pytz_timezone
def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
"""*DEPRECATED AND WILL BE REMOVED IN FUTURE RELEASES*
Move timezone unaware datetime object to UTC timezone and return it.
"""Move timezone unaware datetime object to UTC timezone and return it.
### Args:
* date (`datetime`): Datetime to be converted.
@@ -22,9 +20,7 @@ def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
def from_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
"""*DEPRECATED AND WILL BE REMOVED IN FUTURE RELEASES*
Move timezone unaware datetime object to the timezone specified and return it.
"""Move timezone unaware datetime object to the timezone specified and return it.
### Args:
* date (`datetime`): Datetime to be converted.

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timezone
from datetime import datetime
from convopyro import listen_message
from pyrogram import filters
@@ -7,6 +7,7 @@ from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
from classes.pyroclient import PyroClient
from modules import custom_filters
from modules.utils import from_utc
logger = logging.getLogger(__name__)
@@ -64,19 +65,18 @@ async def command_set_offset(app: PyroClient, message: Message):
logger.info("User %s has set offset to %s", user.id, offset)
garbage_time = (
datetime.now(timezone.utc)
.replace(hour=user.time_hour, minute=user.time_minute)
.astimezone(user.location.timezone or timezone.utc)
)
garbage_time = from_utc(
datetime(1970, 1, 1, user.time_hour, user.time_minute),
None if user.location is None else user.location.timezone.zone,
).strftime(app._("time", "formats"))
await answer.reply_text(
app._("set_offset_finished", "messages", locale=user.locale).format(
offset=offset,
time=garbage_time.strftime(app._("time", "formats", locale=user.locale)),
toggle_notice=(
"" if user.enabled else app._("toggle", "messages", locale=user.locale)
),
time=garbage_time,
toggle_notice=""
if user.enabled
else app._("toggle", "messages", locale=user.locale),
),
reply_markup=ReplyKeyboardRemove(),
)

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timezone
from datetime import datetime
from convopyro import listen_message
from pyrogram import filters
@@ -7,6 +7,7 @@ from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
from classes.pyroclient import PyroClient
from modules import custom_filters
from modules.utils import to_utc
logger = logging.getLogger(__name__)
@@ -57,34 +58,28 @@ async def command_set_time(app: PyroClient, message: Message):
now = datetime.now()
parsed_time = datetime.strptime(answer.text, "%H:%M").replace(
year=now.year,
month=now.month,
day=now.day,
second=0,
microsecond=0,
tzinfo=timezone.utc,
year=now.year, month=now.month, day=now.day, second=0, microsecond=0
)
user_time = parsed_time.astimezone(user.location.timezone or timezone.utc)
user_time = to_utc(parsed_time, user.location.timezone.zone)
await user.update_time(hour=user_time.hour, minute=user_time.minute)
logger.info(
"User %s has selected notification time of %s (%s UTC)",
"User %s has selected notification time of %s",
user.id,
parsed_time.strftime("%H:%M"),
user_time.strftime("%H:%M"),
)
garbage_time = parsed_time.strftime(app._("time", "formats", locale=user.locale))
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)
),
toggle_notice=""
if user.enabled
else app._("toggle", "messages", locale=user.locale),
),
reply_markup=ReplyKeyboardRemove(),
)

View File

@@ -1,4 +1,5 @@
import logging
from datetime import datetime
from convopyro import listen_message
from libbot import i18n
@@ -10,6 +11,7 @@ from classes.pyroclient import PyroClient
from modules import custom_filters
from modules.search_name import search_name
from modules.search_nearby import search_nearby
from modules.utils import from_utc
logger = logging.getLogger(__name__)
@@ -72,12 +74,10 @@ async def command_setup(app: PyroClient, message: Message):
await user.update_location(location.id)
try:
user_time = user.get_reminder_time().strftime(
app._("time", "formats", locale=user.locale)
)
except AttributeError:
user_time = "N/A"
user_time = from_utc(
datetime(1970, 1, 1, user.time_hour, user.time_minute),
None if user.location is None else user.location.timezone.zone,
).strftime(app._("time", "formats", locale=user.locale))
await message.reply_text(
app._("setup_finished", "messages", locale=user.locale).format(

View File

@@ -1,3 +1,5 @@
from datetime import datetime
from convopyro import listen_message
from pyrogram import filters
from pyrogram.types import (
@@ -9,6 +11,7 @@ from pyrogram.types import (
from classes.pyroclient import PyroClient
from modules import custom_filters
from modules.utils import from_utc
@PyroClient.on_message(
@@ -88,18 +91,13 @@ async def command_start(app: PyroClient, message: Message):
await user.update_location(location.id)
try:
user_time = user.get_reminder_time().strftime(
app._("time", "formats", locale=user.locale)
)
except AttributeError:
user_time = "N/A"
user_time = from_utc(
datetime(1970, 1, 1, user.time_hour, user.time_minute),
None if user.location is None else user.location.timezone.zone,
).strftime(app._("time", "formats", locale=user.locale))
await answer.reply_text(
app._("start_selection_yes", "messages", locale=user.locale).format(
name=location.name,
offset=user.offset,
time=user_time,
name=location.name, offset=user.offset, time=user_time
),
reply_markup=ReplyKeyboardRemove(),
)

View File

@@ -1,8 +1,11 @@
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(
@@ -19,12 +22,10 @@ async def command_toggle(app: PyroClient, message: Message):
)
return
try:
user_time = user.get_reminder_time().strftime(
app._("time", "formats", locale=user.locale)
)
except AttributeError:
user_time = "N/A"
user_time = from_utc(
datetime(1970, 1, 1, user.time_hour, user.time_minute),
None if user.location is None else user.location.timezone.zone,
).strftime(app._("time", "formats"))
if user.location is None:
await message.reply_text(

View File

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

View File

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