Untitled

4ae4d avatar
4ae4d
plain_text
01/19/2026 2:15 PM
22.2 KB
10
Indexable
127.0.0.1:57958 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:36420 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:53868 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:56342 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:52800 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:38058 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:48106 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:48696 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:53630 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:33286 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:58426 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:44042 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:37850 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:46244 - "GET /api/v1/docs HTTP/1.1" 200
10.89.0.22:39720 - "GET /api/v1/tags/app/all?offset=0&limit=10 HTTP/1.1" 200
<users: firstname=new, messenger_id=24936466, employee_number=2198857, role=user, points=76, city_id=be9da3ba-a0e4-42f9-9ae2-9875c8625083, id=d7cea288-097d-47d7-8a27-c0be26d7fcdb, created_at=2025-12-01 08:51:36.128288+00:00, updated_at=2025-12-29 12:49:58.768926+00:00>
10.89.0.22:39722 - "GET /api/v1/users/id/24936466 HTTP/1.1" 200
10.89.0.22:39730 - "GET /api/v1/cities/id/be9da3ba-a0e4-42f9-9ae2-9875c8625083 HTTP/1.1" 200
127.0.0.1:59762 - "GET /api/v1/docs HTTP/1.1" 200
10.89.0.22:39742 - "POST /api/v1/events HTTP/1.1" 500
127.0.0.1:39962 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:40032 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:36738 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:38494 - "GET /api/v1/docs HTTP/1.1" 200
127.0.0.1:57546 - "GET /api/v1/docs HTTP/1.1" 200

это логи, которые ты попросил прислать. если тут нет того что ты хочешь - то скажи, где достать.


--- src/bot/core/services/event.py ---
from datetime import datetime, timezone
from typing import Any
from uuid import UUID

from core.schemas.event import EventSchema
from core.schemas.user import UserSchema
from core.clients.event import EventClient
from core.services.base import BaseService
from core.services.registry import BaseServiceRegistry

DEFAULT_CITY_UUID = "be9da3ba-a0e4-42f9-9ae2-9875c8625083" # SPB
# TODO убрать хардкод, брать city_id из профиля пользователя


class EventService(BaseService):
    """
    сервис событий на стороне бота.

    на данный момент:
    - ходит в backend через EventClient
    - умеет получать список событий по городу
    - умеет получать одно событие по id
    - создает событие через клиент используя мастера создания
    """

    def __init__(self, registry: BaseServiceRegistry, client: EventClient):
        # DI может вызвать клиент без аргументов.
        super().__init__(registry)
        self.client = client

    def get_events(
        self,
        city_id: str,
        offset: int = 0,
        limit: int = 50
    ) -> list[EventSchema]:
        """
        возвращает список EventSchema для города
        пока что используем дефолтный город - спб
        """
        resp = self.client.get_city_events(city_id=city_id, offset=offset, limit=limit)

        if resp.status_code != 200:
            raise Exception(f"Error while getting events: {resp.status_code}\n{resp.text}")

        body = resp.json()
        return [self._map_event(item) for item in body]

    def get_event_by_id(self, id: str) -> EventSchema | None:
        resp = self.client.get_event_info(id)

        print(
            "[EventService] get_event_by_id",
            f"event_id={id!r}",
            f"status={resp.status_code!r}",
            f"body={resp.text!r}",
            f"url={resp.request.url!r}",
            f"method={resp.request.method!r}",
        )

        if resp.status_code == 404:
            return None

        if resp.status_code != 200:
            # TODO сделать нормальную обработку
            print(f"[EventService] get_events failed: {resp.status_code} {resp.text}")
            return None

        return self._map_event(resp.json())


    def sign_up(self, event_id: str, messenger_id: int) -> bool:
        """ messenger_id= = peer.id (как на бэкенде: user.get_by_messenger_id) """
        resp = self.client.sign_up(event_id=event_id, messenger_id=messenger_id)
        return resp.status_code == 200

    def delete_event(self, event_id: str) -> bool:
        resp = self.client.delete_event(event_id)
        return resp.status_code == 204 # in [200, 204]

    def update_event(self, event_id: str, payload: dict) -> EventSchema | None:
        resp = self.client.update_event(event_id, payload)
        if resp.status_code != 200:
            print(f"[EventService] update_event failed: {resp.status_code}\n{resp.text}")
            return None
        return self._map_event(resp.json())

    def _get_user_participations_raw(self, messenger_id: int) -> list[dict]:
        """
        GET /events/my?user_id=<messenger_id>
        на бекэнд возвращается список participation (без response_model),
        поэтому на стороне бота парсим максимально терпимо
        """
        resp = self.client.get_my_events(messenger_id=messenger_id)
        if resp.status_code != 200:
            raise Exception(f"Error while getting user events: {resp.status_code}\n{resp.text}")

        body = resp.json()
        # ожидаем list[dict]
        if isinstance(body, list):
            return body
        return []

    def _find_participation_for_event(
        self,
        messenger_id: int,
        event_id: str,
    ) -> tuple[str | None, bool | None]:
        """
        ищем participation_id для конкретного event_id
        возвращаем (participation_id, is claimed)
        """
        items = self._get_user_participations_raw(messenger_id)
        target_event_id = str(event_id)

        for item in items:
            if not isinstance(item, dict):
                continue

            pid = item.get("id")
            ev_id = item.get("event_id")
            is_claimed = item.get("is_claimed")

            # если event вложен
            if ev_id is None and isinstance(item.get("event"), dict):
                ev_id = item["event"].get("id")

            if ev_id is None:
                continue

            if str(ev_id) == target_event_id:
                return (str(pid) if pid else None, bool(is_claimed) if is_claimed is not None else None)

        return (None, None)

    def sign_out_by_event(self, event_id: str, messenger_id: int) -> bool:
        """
        backend sign-out requires participation_id, so:
        1. GET /event/my
        2. находим participation_id для event_id
        3. DELETE /events/sign-out
        """
        participation_id, _ = self._find_participation_for_event(messenger_id, event_id)
        if not participation_id:
            return False
        resp = self.client.sign_out(participation_id=participation_id)
        return resp.status_code == 200

    def enter_code_by_event(
        self,
        event_id: str,
        messenger_id: int,
        bonus_code: str,
    ) -> dict[str, Any] | None:
        """
        аналогично sign-out: нужен participation_id
        возвращает обновленного пользователя (ResponseUserSchema) на успехе
        """
        participation_id, _ = self._find_participation_for_event(messenger_id, event_id)
        if not participation_id:
            return None
        resp = self.client.enter_code(participation_id=participation_id, bonus_code=bonus_code)
        if resp.status_code != 200:
            return None
        return resp.json()

    def get_participation_status(
        self,
        event_id: str,
        messenger_id: int,
    ) -> dict[str, Any] | None:
        participation_id, is_claimed = self._find_participation_for_event(messenger_id, event_id)
        if not participation_id:
            return None
        return {
            "participation_id": participation_id,
            "is_claimed": bool(is_claimed) if is_claimed is not None else False,
        }

    def enter_code(self, user_id: int, code: str):
        # TODO: replace with client usage
        return code in ["code", "код"]

    def create_event(
        self,
        *,
        name: str,
        description: str,
        bonus_code: str,
        points: int,
        date_obj: datetime,
        city_id: str,
        creator_id: str | None = None,
    ):
        """
        создает событие через backend и возвращает dict из backend'a
        - ожидает, что все данные уже провалидированы и разобраны
        - метод не мапит ответ backend'a в EventSchema и не делает никакой обработки, а возвращает ровно тот json, который вернул backend (dect)
        - может бросить Exception, если status_code не 201/200
        """
        date_iso = date_obj.isoformat()

        resp = self.client.create_event(
            name=name,
            description=description,
            bonus_code=bonus_code,
            points=points,
            date=date_iso,
            city_id=city_id,
            creator_id=creator_id,
        )

        if resp.status_code not in [200, 201]:
            raise Exception(f"Error while creating event: {resp.status_code}\n{resp.text}")

        return resp.json()

    def create_from_wizard(self, user: UserSchema, data: dict[str, Any]) -> dict[str, Any]:
        """
        обрабатываем данные полученный из мастера создания,
        посылаем их в бекенд.
        """

        name = data.get("event_name")
        date_obj = data.get("event_date")
        time_obj = data.get("event_time")
        location = data.get("event_location")
        organizer_raw = data.get("event_organizer") or ""
        description = data.get("event_description") or ""
        points = data.get("event_points")
        app_tag_ids = data.get("event_app_tag_ids") or []
        bonus_code = data.get("event_bonus_code")

        event_dt = datetime.combine(date_obj, time_obj).replace(tzinfo=timezone.utc)
        date = event_dt.isoformat().replace("+00:00", "Z")

        creator_id: str | None = None
        if organizer_raw == "-":
            if not user.id:
                raise Exception("Cannot use '-' for organizer, current user has no id")
            creator_id = user.id
        else:
            try:
                creator_id = str(UUID(organizer_raw))
            except Exception:
                raise Exception(f"Invalid organizer UUID: {organizer_raw!r}")

        resp = self.client.create_event(
            name=name,
            description=description,
            bonus_code=bonus_code,
            points=points,
            date=date,
            city_id=DEFAULT_CITY_UUID,
            location=location,
            app_tag_ids=app_tag_ids,
            creator_id=creator_id,
        )

        if resp.status_code not in [200, 201]:
            raise Exception(
                f"Error while creating event: {resp.status_code}\n{resp.text}"
            )

        return self._map_event(resp.json())


    def _map_event(self, body: dict) -> EventSchema:
        """ преобразует json из backend'a в EventSchema бота """
        date_raw = body.get("date")
        date = self._parse_iso_datetime(date_raw)

        return EventSchema(
            id=body.get("id"),
            name=body.get("name", ""),
            date=date,
            description=body.get("description", "") or "",
            location=body.get("location"),
            tags=body.get("tags"),
            bonus_code=body.get("bonus_code"),
            points=body.get("points"),
            city_id=body.get("city_id"),
            creator_id=body.get("creator_id"),
        )

    def _parse_iso_datetime(self, value: str | None) -> datetime:
        if not value:
            return datetime.now()

        if value.endswith("Z"):
            value = value[:-1] + "+00:00"

        try:
            return datetime.fromisoformat(value)
        except Exception as e:
            print(f"[EventService] failed to parse date {value!r}, returing now(): {e}")
            return datetime.now()


--- src/bot/core/clients/event.py ---
from core.clients.base import BaseApiClient

class EventClient(BaseApiClient):

    def get_city_events(self, city_id: str, offset: int = 0, limit: int = 50):
        """ GET /events/id/{city_id} """
        return self.get(
            f"/events/id/{city_id}",
            params={"offset": offset, "limit": limit},
        )

    def get_event_info(self, event_id: str):
        """ GET /events/info/{event_id} """
        return self.get(f"/events/info/{event_id}")

    def get_my_events(self, messenger_id: int):
        """ GET /events/my?user_id={messenger_id} """
        return self.get(
            "/events/my",
            params={"user_id": messenger_id},
        )

    def create_event(
        self,
        *,
        name: str,
        description: str,
        bonus_code: str,
        points: int,
        date: str,
        city_id: str,
        location: str | None = None,
        app_tag_ids: list[str] | None = None,
        creator_id: str | None = None,
    ):
        """ POST /events """
        payload = {
            "name": name,
            "description": description,
            "bonus_code": bonus_code,
            "points": points,
            "date": date,
            "city_id": city_id,
            "location": location,
            "app_tag_ids": app_tag_ids or [],
        }
        if creator_id is not None:
            payload["creator_id"] = creator_id
        return self.post("/events", json=payload)

    def delete_event(self, event_id: str):
        """ DELETE /events/{event_id} """
        return self.delete(f"/events/{event_id}")

    def sign_out(self, participation_id: str):
        """ DELETE /events/sign-out """
        return self.delete(
            "/events/sign-out",
            json={"participation_id": participation_id},
        )

    def sign_up(self, event_id: str, messenger_id: str):
        """ POST /events/sign-up """
        return self.post(
            "/events/sign-up",
            json={"event_id": event_id, "messenger_id": messenger_id},
        )

    def enter_code(self, participation_id: str, bonus_code: str):
        """ POST /events/code """
        return self.post(
            "/events/code",
            json={"participation_id": participation_id, "bonus_code": bonus_code},
        )

    def update_event(self, event_id: str, payload: dict):
        """ PATCH /events/{event_id} """
        return self.patch(f"/events/{event_id}", json=payload)


--- src/bot/core/schemas/event.py ---
from datetime import datetime

class EventSchema:
    def __init__(
        self,
        id: str,
        name: str,
        date: datetime,
        description: str,
        *,
        location: str | None = None,
        tags: list | None = None,
        bonus_code: str | None = None,
        points: int | None = None,
        city_id: str | None = None,
        creator_id: str | None = None,
    ):
        self.id = id
        self.name = name
        self.date = date
        self.description = description
        self.location = location
        self.tags = tags
        self.bonus_code = bonus_code
        self.points = points
        self.city_id = city_id
        self.creator_id = creator_id


--- src/bot/core/clients/base.py ---
from requests import Session
from ..config.settings import settings

class BaseApiClient:
    def __init__(self, base_url=None):
        self.base_url = settings.API_URL + "/api/v1"
        self.session = Session()
        if base_url:
            self.base_url = base_url


    def health_check(self) -> bool:
        return self.get(self.base_url + "api/v1/docs").status_code == 200

    def get(self, url, headers=None, params=None):
        return self.session.get(self.base_url + url, headers=headers, params=params)

    def post(self, url, headers=None, json=None):
        return self.session.post(self.base_url + url, headers=headers, json=json)

    def delete(self, url, headers=None, json=None):
        return self.session.delete(self.base_url + url, headers=headers, json=json)

    def patch(self, url, headers=None, json=None):
        return self.session.patch(self.base_url + url, headers=headers, json=json)

    def put(self, url, headers=None, json=None):
        return self.session.put(self.base_url + url, headers=headers, json=json)

Editor is loading...
Leave a Comment