from __future__ import annotations import functools from datetime import UTC, datetime from typing import Annotated, Literal import timeago from beanie import Document, Indexed, Link from pydantic import BaseModel, Field from db.models.video import Video MAXIMUM_TASKS = {"free": 1, "plus": 6, "pro": 15} utc_now = functools.partial(datetime.now, UTC) class UserSettings(BaseModel): language: Annotated[str, Field("en", min_length=2, max_length=3)] class User(Document): user_id: int subscription: Literal["free", "plus", "pro"] = "free" is_admin: bool = False is_banned: bool = False invited_by: Link[User] | None = None points: int = Field(0, ge=0) settings: UserSettings created_at: datetime = Field(default_factory=utc_now) async def add_task(self: User, video: Video | None = None) -> Task: if not video: t = Task(user=self) await t.insert() else: t = Task(user=self, video=video) await t.insert() return t def is_paid(self: User) -> bool: return self.subscription != "free" or self.is_admin @property def language(self: User) -> str: return self.settings.language async def remaining_tasks(self: User) -> int: now = utc_now().replace(hour=0, minute=0, second=0, microsecond=0) t = await Task.find( Task.created_at >= now, Task.user.id == self.id, fetch_links=True, ).count() remaining_tasks = (MAXIMUM_TASKS[self.subscription] + self.points) - t return remaining_tasks if remaining_tasks > 0 else self.points async def users_joined_by(self: User) -> int: pipeline = [ { "$match": { "invited_by.$id": self.id, } }, {"$group": {"_id": None, "count": {"$sum": 1}}}, ] result: list[dict[str, int]] = await User.find().aggregate(pipeline).to_list() return result[0]["count"] if result else 0 async def can_send_task(self: User, take_point: bool = False) -> tuple[bool, bool]: if self.is_admin: return True, False now = utc_now().replace(hour=0, minute=0, second=0, microsecond=0) t = await Task.find( Task.created_at >= now, Task.user.user_id == self.user_id, fetch_links=True, ).count() if MAXIMUM_TASKS[self.subscription] > t: return True, False if self.points > 0 and max(t - MAXIMUM_TASKS[self.subscription], 0) < 2: if take_point: self.points -= 1 await self.save() return True, True return True, False return False, False def joined_since(self: User, locale: str = "en") -> str: return timeago.format(self.created_at, now=utc_now().replace(tzinfo=None), locale=locale) class Task(Document): user: Link[User] video: Link[Video] | None = None created_at: datetime = Field(default_factory=utc_now) finished: Annotated[bool, Indexed()] = False