Initial commit

This commit is contained in:
2023-08-27 22:43:16 +02:00
commit 502ed0406e
40 changed files with 1854 additions and 0 deletions

30
classes/callbacks.py Normal file
View File

@@ -0,0 +1,30 @@
from dataclasses import dataclass
from pyrogram.types import CallbackQuery
@dataclass
class CallbackLanguage:
__slots__ = ("language",)
language: str
@classmethod
def from_callback(cls, callback: CallbackQuery):
"""Parse callback query and extract language data from it.
### Args:
* callback (`CallbackQuery`): Callback query got from user interaction.
### Raises:
* `ValueError`: Raised when callback provided is not a language one.
### Returns:
* `CallbackLanguage`: Parsed callback query.
"""
action, language = str(callback.data).split(":")
if action.lower() != "language":
raise ValueError("Callback provided is not a language callback")
return cls(language)

View File

@@ -0,0 +1 @@
from .garbage_type import GarbageType

View File

@@ -0,0 +1,10 @@
from enum import Enum
class GarbageType(Enum):
BIO = 0
PLASTIC = 1
PAPER = 2
GENERAL = 3
GLASS = 4
UNSPECIFIED = 5

73
classes/garbage_entry.py Normal file
View File

@@ -0,0 +1,73 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Any, List, Mapping, Union
from bson import ObjectId
from classes.enums.garbage_type import GarbageType
from classes.location import Location
@dataclass
class GarbageEntry:
__slots__ = (
"_id",
"locations",
"garbage_type",
"date",
)
_id: Union[ObjectId, None]
locations: List[Location]
garbage_type: GarbageType
date: datetime
@classmethod
async def from_dict(cls, data: Mapping[str, Any]):
"""Generate GarbageEntry object from the mapping provided
### Args:
* data (`Mapping[str, Any]`): Entry
### Raises:
* `KeyError`: Key is missing.
* `TypeError`: Key of a wrong type provided.
* `ValueError`: "date" is not a valid ISO string.
### Returns:
* `GarbageEntry`: Valid GarbageEntry object.
"""
for key in ("locations", "garbage_type", "date"):
if key not in data:
raise KeyError
if key == "locations" and not isinstance(data[key], list):
raise TypeError
if key == "garbage_type" and not isinstance(data[key], int):
raise TypeError
if key == "date":
datetime.fromisoformat(str(data[key]))
locations = [
await Location.get(location_id) for location_id in data["locations"]
]
garbage_type = GarbageType(data["garbage_type"])
return cls(
None,
locations,
garbage_type,
data["date"],
)
@classmethod
async def from_record(cls, data: Mapping[str, Any]):
locations = [
await Location.get(location_id) for location_id in data["locations"]
]
garbage_type = GarbageType(data["garbage_type"])
return cls(
data["_id"],
locations,
garbage_type,
data["date"],
)

View File

@@ -0,0 +1,20 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
from classes.location import Location
class GeoBase(ABC):
@abstractmethod
async def get_location(self) -> Location:
pass
@abstractmethod
async def find_location(self) -> List[Location]:
pass
@abstractmethod
async def nearby_location(self) -> List[Location]:
pass

View File

@@ -0,0 +1,23 @@
from typing import List
from aiohttp import ClientSession
from classes.geobase.geobase import GeoBase
from classes.location import Location
# from urllib.parse import urlencode
class GeoBaseAPI(GeoBase):
async def get_location(self, session: ClientSession, location_id: int) -> Location:
# query = {"geoNameId": location_id, "style": "MEDIUM"}
# response = await session.get(f"http://api.geonames.org/get?{urlencode(query)}")
pass
async def find_location(self, session: ClientSession, name: str) -> List[Location]:
pass
async def nearby_location(
self, session: ClientSession, lat: float, lon: float, radius: int
) -> List[Location]:
pass

58
classes/location.py Normal file
View File

@@ -0,0 +1,58 @@
from dataclasses import dataclass
from bson import ObjectId
from classes.point import Point
from modules.database import col_locations
@dataclass
class Location:
__slots__ = (
"_id",
"id",
"name",
"location",
"country",
"timezone",
)
_id: ObjectId
id: int
name: str
location: Point
country: int
timezone: str
@classmethod
async def get(cls, id: int):
db_entry = await col_locations.find_one({"id": id})
if db_entry is None:
raise ValueError(f"No location with ID {id} found.")
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
return cls(**db_entry)
@classmethod
async def find(cls, name: str):
db_entry = await col_locations.find_one({"name": {"$regex": name}})
if db_entry is None:
raise ValueError(f"No location with name {name} found.")
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
return cls(**db_entry)
@classmethod
async def nearby(cls, lat: float, lon: float):
db_entry = await col_locations.find_one({"location": {"$near": [lon, lat]}})
if db_entry is None:
raise ValueError(f"No location near {lat}, {lon} found.")
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
return cls(**db_entry)

12
classes/point.py Normal file
View File

@@ -0,0 +1,12 @@
from dataclasses import dataclass
@dataclass
class Point:
__slots__ = (
"lon",
"lat",
)
lon: float
lat: float

68
classes/pyroclient.py Normal file
View File

@@ -0,0 +1,68 @@
from typing import List, Union
from apscheduler.triggers.cron import CronTrigger
from libbot.pyrogram.classes import PyroClient as LibPyroClient
from pymongo import ASCENDING, GEO2D
from pyrogram.types import User
from classes.location import Location
from classes.pyrouser import PyroUser
from modules.database import col_locations
from modules.reminder import remind
class PyroClient(LibPyroClient):
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.scheduler is not None:
self.scheduler.add_job(
remind, CronTrigger.from_crontab("* * * * *"), args=(self,)
)
async def start(self, **kwargs):
await col_locations.create_index(
[("id", ASCENDING)], name="location_id", unique=True
)
await col_locations.create_index(
[("location", GEO2D)],
name="location_location",
)
return await super().start(**kwargs)
async def find_user(self, user: Union[int, User]) -> PyroUser:
"""Find User by it's ID or User object.
### Args:
* user (`Union[int, User]`): ID or User object to extract ID from.
### Returns:
* `PyroUser`: User in database representation.
"""
return (
await PyroUser.find(user)
if isinstance(user, int)
else await PyroUser.find(user.id, locale=user.language_code)
)
async def get_location(self, id: int) -> Location:
"""Get Location by it's ID.
### Args:
* id (`int`): Location's ID. Defaults to `None`.
### Returns:
* `Location`: Location from database as an object.
"""
return await Location.get(id)
async def list_locations(self) -> List[Location]:
"""Get all locations stored in database.
### Returns:
* `List[Location]`: List of `Location` objects.
"""
return [
await Location.get(record["id"]) async for record in col_locations.find({})
]

118
classes/pyrouser.py Normal file
View File

@@ -0,0 +1,118 @@
import logging
from dataclasses import dataclass
from typing import Any, Union
from bson import ObjectId
from classes.location import Location
from modules.database import col_users
logger = logging.getLogger(__name__)
@dataclass
class PyroUser:
"""Dataclass of DB entry of a user"""
__slots__ = (
"_id",
"id",
"locale",
"enabled",
"location",
"offset",
"time_hour",
"time_minute",
)
_id: ObjectId
id: int
locale: Union[str, None]
enabled: bool
location: Union[Location, None]
offset: int
time_hour: int
time_minute: int
@classmethod
async def find(
cls,
id: int,
locale: Union[str, None] = None,
enabled: bool = True,
location_id: int = 0,
offset: int = 1,
time_hour: int = 18,
time_minute: int = 0,
):
db_entry = await col_users.find_one({"id": id})
if db_entry is None:
inserted = await col_users.insert_one(
{
"id": id,
"locale": locale,
"enabled": enabled,
"location": location_id,
"offset": offset,
"time_hour": time_hour,
"time_minute": time_minute,
}
)
db_entry = await col_users.find_one({"_id": inserted.inserted_id})
if db_entry is None:
raise RuntimeError("Could not find inserted user entry.")
try:
db_entry["location"] = await Location.get(db_entry["location"]) # type: ignore
except ValueError:
db_entry["location"] = None # type: ignore
return cls(**db_entry)
async def update_locale(self, locale: Union[str, None]) -> 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)
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)
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)
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)
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)
await col_users.update_one(
{"_id": self._id}, {"$set": {"time_hour": hour, "time_minute": minute}}
)
async def delete(self) -> None:
logger.debug("%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)
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"
)
del db_entry["_id"] # type: ignore
return db_entry