Untitled
4ae4d
plain_text
02/16/2026 9:34 AM
36.8 KB
9
Indexable
f=src/bot/core/markups/pagination.py
f=src/bot/core/markups/inline.py
f=src/bot/core/markups/points.py
f=src/bot/core/utils/fsm/storage/memory.py
f=src/backend/app/dependencies/auth/authorization.py
f=src/backend/app/schemas/event.py
f=src/backend/app/schemas/report.py
f=src/backend/app/repositories/base.py
f=src/backend/app/dependencies/auth/authentication.py
f=src/backend/app/api/v1/report.py
--- src/bot/core/markups/pagination.py ---
from __future__ import annotations
from dialog_bot_sdk.entities.messaging import InteractiveMediaStyle
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder
def pagination_keyboard(
*,
offset: int,
limit: int,
has_prev: bool,
has_next: bool,
prev_media_id: str,
next_media_id: str,
prev_label: str = "⬅️ Назад",
next_label: str = "➡️ Вперёд",
include_leave: bool = True,
leave_media_id: str = "leave",
leave_value: str | None = None,
leave_label: str | None = None,
style: InteractiveMediaStyle | None = None,
keep_layout: bool = False,
placeholder_media_id: str = "noop",
placeholder_value: str = "noop",
placeholder_label: str = " ",
hide_when_all_placeholders: bool = False,
add_view_by_uuid_button: bool = False,
view_by_uuid_media_id: str = "view_by_uuid",
view_by_uuid_value: str = "events",
view_by_uuid_label: str = "🔎 По UUID",
) -> list[InteractiveMediaGroup]:
"""
унифицированная пагинация.
keep_layout=True:
если add_view_by_uuid_button=True:
- если include_leave=True: 4 слота [prev|ph] [leave|ph] [uuid] [next|ph]
- если include_leave=False: 3 слота [prev|ph] [uuid] [next|ph]
если add_view_by_uuid_button=False:
- если include_leave=True: 3 слота [prev|ph] [leave|ph] [next|ph]
- если include_leave=False: 2 слота [prev|ph] [next|ph]
hide_when_all_placeholders=True: если из кнопок только ph - то возвращаем пустой лист.
"""
def _btn(media_id: str, value: str, label: str) -> Button:
if style is None:
return Button(media_id=media_id, value=value, label=label)
return Button(media_id, value, label, style)
def _ph(slot: str) -> Button:
return _btn(
placeholder_media_id + "_" + slot,
placeholder_value,
placeholder_label,
)
def _prev() -> Button:
return _btn(prev_media_id, str(max(offset - limit, 0)), prev_label)
def _next() -> Button:
return _btn(next_media_id, str(offset + limit), next_label)
def _view_by_uuid() -> Button:
return _btn(
view_by_uuid_media_id,
str(view_by_uuid_value),
str(view_by_uuid_label),
)
def _leave() -> Button | None:
if not (include_leave and leave_value and leave_label):
return None
return _btn(leave_media_id, str(leave_value), str(leave_label))
# --- 3-slot layout for include_leave
if include_leave and keep_layout:
buttons: list[Button] = []
leave_btn = _leave()
buttons.append(_prev() if has_prev else _ph("prev"))
buttons.append(leave_btn if leave_btn is not None else _ph("mid"))
# --- 4-slot: [prev] [leave] [uuid] [next]
if add_view_by_uuid_button:
buttons.append(_view_by_uuid())
buttons.append(_next() if has_next else _ph("next"))
if hide_when_all_placeholders and all(
str(getattr(b, "media_id", "")).startswith(placeholder_media_id + "_") for b in buttons
):
return []
return MediaGroupBuilder(buttons).build()
# --- default linear layout (как было)
buttons: list[Button] = []
if has_prev:
buttons.append(_prev())
elif keep_layout:
buttons.append(_ph("prev"))
leave_btn = _leave()
if leave_btn is not None:
buttons.append(leave_btn)
if add_view_by_uuid_button:
buttons.append(_view_by_uuid())
if has_next:
buttons.append(_next())
elif keep_layout:
buttons.append(_ph("next"))
if hide_when_all_placeholders and all(
str(getattr(b, "media_id", "")).startswith(placeholder_media_id + "_") for b in buttons
):
return []
return MediaGroupBuilder(buttons).build() if buttons else []
--- src/bot/core/markups/inline.py ---
from dialog_bot_sdk.entities.messaging import InteractiveMediaStyle
from dialog_bot_sdk.interactive_media import Button, MediaGroupBuilder
from core.markups.pagination import pagination_keyboard
# Основные клавиатуры
def main_keyboard_admin():
group_builder_1 = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"🏡 Дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2",
"ai_assistant",
"✨ ИИ Волонтёр",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button(
"3",
"moderation",
"🛡️ Модерация",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"4",
"administration",
"⚙️ Администрация",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
return group_builder_1.merge([group_builder_2])
def main_keyboard_moder():
group_builder_1 = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"🏡 Дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2",
"ai_assistant",
"✨ ИИ Волонтёр",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button(
"3", "moderation", "Модерация", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
)
]
)
return group_builder_1.merge([group_builder_2])
def main_keyboard_user():
builder = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"🏡 Дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2",
"ai_assistant",
"✨ ИИ Волонтёр",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
return builder.build()
def back_to_main_menu_keyboard():
builder = MediaGroupBuilder(
[Button("1", "main_menu", "⬅️ В меню", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT)]
)
return builder.build()
# Дом волонтёра
def volunteer_home_keyboard():
group_builder_1 = MediaGroupBuilder(
[
Button(
"1",
"my_profile",
"👤 Мой профиль",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2", "events", "🌐 Мероприятия", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button("3", "points", "🏆 Баллы", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT),
Button(
"4",
"part_events",
"🎉 Мои меропиятия",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
group_builder_3 = MediaGroupBuilder(
[
Button(
"5", "main_menu", "⬅️ В меню", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
),
]
)
return group_builder_1.merge([group_builder_2, group_builder_3])
def back_to_moderation_keyboard():
builder = MediaGroupBuilder(
[
Button(
"1",
"moderation",
"⬅️ В меню модерации",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
return builder.build()
def back_to_volunteer_home_keyboard():
builder = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"⬅️ В дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
)
]
)
return builder.build()
# Модерация
def moderation_menu_keyboard():
group_builder_1 = MediaGroupBuilder(
[
Button("1", "create_event", "🗓️ Создать мероприятие"),
Button("4", "my_events", "📋 Мои мероприятия"),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button(
"5",
"all_events",
"🌐 Все мероприятия",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button("7", "download_reports_list", "📥 Выгрузить отчёты"),
]
)
group_builder_3 = MediaGroupBuilder(
[
Button("10", "main_menu", "⬅️ В меню"),
]
)
return group_builder_1.merge([group_builder_2, group_builder_3])
# Администрация
def admin_menu_keyboard():
group_builder_1 = MediaGroupBuilder(
[
Button("1", "add_admin", "Добавить администратора"),
Button("2", "delete_admin", "Убрать администратора"),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button("3", "add_moderator", "Добавить модератора"),
Button("4", "delete_moderator", "Убрать модератора"),
]
)
group_builder_3 = MediaGroupBuilder(
[
Button("5", "ban_user", "Забанить пользователя"),
Button("6", "unban_user", "Разбанить пользователя"),
]
)
group_builder_4 = MediaGroupBuilder([Button("7", "main_menu", "⬅️ В меню")])
return group_builder_1.merge([group_builder_2, group_builder_3, group_builder_4])
def reports_export_keyboard():
"""
value везде = 'download_reports_list', обработчик один
выбор различаем по media_id
"""
group_1 = MediaGroupBuilder(
[
Button("rep_day", "download_reports_list", "📅 Последний день"),
Button("rep_week", "download_reports_list", "📆 Неделя"),
]
)
group_2 = MediaGroupBuilder(
[
Button("rep_month", "download_reports_list", "🗓️ Месяц"),
Button("rep_all", "download_reports_list", "🌐 Всё время"),
]
)
group_3 = MediaGroupBuilder(
[
Button("rep_from", "download_reports_list", "📌 От даты до сегодня"),
Button("rep_range", "download_reports_list", "🗓️ От даты до даты"),
]
)
group_4 = MediaGroupBuilder(
[
Button("rep_back", "moderation", "⬅️ В меню модерации"),
]
)
return group_1.merge([group_2, group_3, group_4])
def all_events_pagination_keyboard(*, offset: int, limit: int, has_prev: bool, has_next: bool):
return pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
prev_media_id="all_events_prev",
next_media_id="all_events_next",
leave_value="moderation",
leave_label="🛡️ В меню модерации",
style=InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
keep_layout=True,
add_view_by_uuid_button=True,
view_by_uuid_value="moderation",
)
--- src/bot/core/markups/points.py ---
from dialog_bot_sdk.interactive_media import Button, MediaGroupBuilder
from core.markups.pagination import pagination_keyboard
def points_menu_keyboard():
return MediaGroupBuilder(
[
Button("1", "points_leaderboard", "🏆 Лидерборд"),
Button("2", "volunteer_home", "⬅️ В дом волонтёра"),
]
).build()
def back_to_points_menu():
return MediaGroupBuilder([Button("1", "points", "⬅️ К баллам")]).build()
def leaderboard_keyboard():
return MediaGroupBuilder(
[Button("1", "points_leaderboard", "🔂 Обновить"), Button("2", "points", "⬅️ К баллам")]
).build()
def points_events_pagination_keyboard(*, offset: int, limit: int, has_prev: bool, has_next: bool):
return pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
prev_media_id="points_prev",
next_media_id="points_next",
include_leave=False,
keep_layout=True,
hide_when_all_placeholders=True,
)
--- src/bot/core/utils/fsm/storage/memory.py ---
from .base import BaseStorage
class MemoryStorage(BaseStorage):
def __init__(self):
self._data = {}
def set(self, user_id: int, state: str | int, data: dict):
self._data[user_id] = {"state": state, "data": data or {}}
def get(self, user_id: int) -> dict:
return self._data.get(user_id, {"state": None, "data": {}})
def clear(self, user_id: int):
self._data.pop(user_id, None)
--- src/backend/app/dependencies/auth/authorization.py ---
from datetime import UTC, datetime, timedelta
from typing import Literal
from jose import jwt
from sqlalchemy.ext.asyncio import AsyncSession
from ...config.settings import settings
from ...schemas import TokenData
from ...services import AdminService
from ...utils.hasher import Hasher
class AuthorizationService:
@classmethod
async def login(
cls, data: str | dict, role: Literal["admin"], session: AsyncSession
) -> TokenData | None:
if role == "admin":
return await cls._process_authorization_admin(data=data, session=session)
return None
@classmethod
async def _process_authorization_admin(cls, data: dict, session: AsyncSession):
admin = await AdminService.get_admin_by_login(session, data["login"])
if admin and Hasher.verify_password(data["password"], admin.password):
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_token_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_DAYS)
access_token = cls.create_access_token(
data={"sub": admin.login, "scopes": ["admin"]}, expires_delta=access_token_expires
)
refresh_token = cls.create_refresh_token(
data={"sub": admin.login, "scopes": ["admin"]}, expires_delta=refresh_token_expires
)
return TokenData(access_token=access_token, refresh_token=refresh_token)
return None
@staticmethod
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.now(UTC) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
@staticmethod
def create_refresh_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.now(UTC) + (
expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
@classmethod
def refresh_tokens(cls, refresh_token: str):
payload = jwt.decode(refresh_token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
new_access_token = cls.create_access_token(
data={"sub": payload["sub"], "scopes": payload["scopes"]}
)
new_refresh_token = cls.create_refresh_token(
data={"sub": payload["sub"], "scopes": payload["scopes"]}
)
return TokenData(access_token=new_access_token, refresh_token=new_refresh_token)
--- src/backend/app/schemas/event.py ---
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, constr
from .app_tag import ResponseAppTagSchema
from .base import TimestampSchema
class BaseEvent(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str
description: str | None = None
location: str | None = None
tags: list[ResponseAppTagSchema] = Field(default_factory=list)
bonus_code: str
points: int
date: datetime
class EventCreateSchema(BaseModel):
"""то, что приходит от бота при создании мероприятия"""
model_config = ConfigDict(extra="forbid")
name: constr(max_length=128)
description: str | None = None
location: constr(max_length=255) | None = None
app_tag_ids: list[UUID] = Field(default_factory=list)
bonus_code: constr(max_length=16)
points: int = 0
date: datetime
city_id: UUID
creator_id: UUID | None = None
class RequestEventSchema(BaseEvent):
pass
class ResponseEventSchema(BaseEvent, TimestampSchema):
id: UUID
creator_id: UUID | None = (
None # TODO решить, оставляем ли или всегда приписываем UUID создателя
)
creator_name: str | None = None
city_id: UUID
class EventSignUpSchema(BaseModel):
event_id: UUID
messenger_id: int
class EventSignOutSchema(BaseModel):
participation_id: UUID
class EventCodeSchema(EventSignOutSchema):
bonus_code: str
class EventUpdateSchema(BaseModel):
model_config = ConfigDict(extra="forbid")
name: constr(max_length=128) | None = None
description: str | None = None
location: str | None = None
app_tag_ids: list[UUID] | None = None
bonus_code: constr(max_length=16) | None = None
points: int | None = None
date: datetime | None = None
city_id: UUID | None = None
creator_id: UUID | None = None
class ResponseEventParticipantSchema(BaseModel):
"""один участник мероприятия"""
model_config = ConfigDict(extra="forbid")
participation_id: UUID
is_claimed: bool
user_id: UUID
firstname: str | None = None
messenger_id: int
employee_number: int
points: int
--- src/backend/app/schemas/report.py ---
from __future__ import annotations
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, constr
from .base import TimestampSchema
class ReportCreateSchema(BaseModel):
model_config = ConfigDict(extra="forbid")
event_id: UUID
requester_messenger_id: int
how_it_went: constr(min_length=1)
beneficiaries_count: int | None = None
class ResponseReportSchema(TimestampSchema):
model_config = ConfigDict(from_attributes=True)
id: UUID
event_id: UUID
how_it_went: str
beneficiaries_count: int | None = None
participants_count_snapshot: int = Field(..., ge=0)
--- src/backend/app/repositories/base.py ---
from uuid import UUID
from sqlalchemy import asc as sa_asc
from sqlalchemy import delete, insert, select, update
from sqlalchemy import desc as sa_desc
from sqlalchemy.ext.asyncio import AsyncSession
from ..utils.errors import BadRequestError
class SQLRepository:
model = None
def __init__(self, session: AsyncSession):
self.session = session
async def insert(self, data: dict):
query = insert(self.model).values(**data).returning(self.model)
result = await self.session.execute(query)
instance = result.fetchone()
if not instance:
raise BadRequestError("Insert failed")
return instance[0]
async def get_by_id(self, id: UUID):
query = select(self.model).where(self.model.id == id)
result = await self.session.execute(query)
return result.scalars().first()
async def update(self, id: UUID, data: dict):
query = update(self.model).where(self.model.id == id).values(**data).returning(self.model)
result = await self.session.execute(query)
return result.scalar()
async def delete(self, id: UUID):
query = delete(self.model).where(self.model.id == id).returning(self.model)
result = await self.session.execute(query)
return result.scalar()
async def get_all(
self,
order_by: str | None = None,
desc: bool | None = None,
offset: int = 0,
limit: int = 25,
):
query = select(self.model)
if order_by:
column = getattr(self.model, order_by, None)
if column is None:
raise ValueError(f"Invalid order_by column: {order_by}")
query = query.order_by(sa_desc(column) if desc else sa_asc(column))
query = query.offset(offset).limit(limit)
result = await self.session.execute(query)
return result.scalars().all()
--- src/backend/app/dependencies/auth/authentication.py ---
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import SecurityScopes
from jose import JWTError, jwt
from jose.exceptions import ExpiredSignatureError
from pydantic import ValidationError
from sqlalchemy.ext.asyncio import AsyncSession
from ...config.settings import settings
from ...db.connection import get_db
from ...schemas import ResponseAdminSchema, TokenData
from ...utils import logger
from .oauth import CookieAuthorization
from .profile import get_profile
cookie_oauth2_scheme = CookieAuthorization()
async def authentification_process(
tokens: TokenData, security_scopes: SecurityScopes, session: AsyncSession | None = None
):
authenticate_value = "Bearer"
access_token = tokens.access_token
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials. Incorrect authorization value",
headers={"WWW-Authenticate": authenticate_value},
)
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
try:
payload = jwt.decode(
access_token,
settings.SECRET_KEY,
algorithms=[settings.ALGORITHM],
)
sub: str = payload.get("sub")
token_scopes: list = payload.get("scopes", [])
if sub is None:
raise credentials_exception
await logger.write(
f"| {sub} | An attempt to access a private resource. Checking the token for rights"
)
except (JWTError, ValidationError, ExpiredSignatureError) as e:
raise credentials_exception from e
for scope in token_scopes:
if scope not in security_scopes.scopes:
await logger.write(
f"| {sub} | Profile have not enough permissions for executing this request. Access is denied"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
profile = await get_profile(filter_=sub, role=security_scopes.scopes[0], session=session)
if not profile:
raise credentials_exception
return profile
async def authentificate_admin(
tokens: TokenData, session: Annotated[AsyncSession, Depends(get_db)]
) -> ResponseAdminSchema:
admin = await authentification_process(
tokens=tokens, security_scopes=SecurityScopes(scopes=["admin"]), session=session
)
await logger.write(f"The admin {admin.login} passed the checking")
return admin
--- src/backend/app/api/v1/report.py ---
from __future__ import annotations
from datetime import date, datetime
from io import BytesIO
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, Path, Query, status
from fastapi.responses import StreamingResponse
from openpyxl import Workbook
from sqlalchemy.ext.asyncio import AsyncSession
from ...db.connection import get_db
from ...dependencies.administrator import Administrator
from ...schemas.report import ReportCreateSchema, ResponseReportSchema
from ...services.report import ReportService
api_router = APIRouter(prefix="/reports", tags=["Reports"])
SessionDep = Annotated[AsyncSession, Depends(get_db)]
REPORT_COLUMNS: list[tuple[str, str]] = [
("event_name", "Название"),
("city", "Город"),
("location", "Локация"),
("date", "Дата"),
("time", "Время"),
("participants_count", "Кол-во участников"),
("points", "Кол-во баллов"),
("tags", "Теги"),
("organizer_fio", "ФИО организатора"),
("beneficiaries_count", "Кол-во благополучателей"),
("how_it_went", "Как прошло"),
]
@api_router.get("/export")
async def export_reports_xlsx(
session: SessionDep,
date_from: Annotated[date | None, Query(description="YYYY-MM-DD")] = None,
date_to: Annotated[date | None, Query(description="YYYY-MM-DD")] = None,
):
"""
Эскпорт отчётов в xlsx из БД за период.
сортировка по дате мероприятия.
в колонках нет описания мероприятия
"""
admin = Administrator(session)
service = ReportService(admin)
wb = Workbook()
ws = wb.active
ws.title = "Reports"
ws.append([title for _, title in REPORT_COLUMNS])
rows = await service.export_reports_rows(date_from=date_from, date_to=date_to)
for row in rows:
ws.append(row)
buf = BytesIO()
wb.save(buf)
buf.seek(0)
export_day = datetime.utcnow().date().isoformat()
name_parts = ["reports", export_day]
if date_from and date_to:
name_parts.append(f"{date_from}_{date_to}")
elif date_from:
name_parts.append(f"from_{date_from}")
elif date_to:
name_parts.append(f"to_{date_to}")
filename = "_".join(name_parts) + ".xlsx"
headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
return StreamingResponse(
buf,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers=headers,
)
@api_router.post(
"",
status_code=status.HTTP_201_CREATED,
response_model=ResponseReportSchema,
)
async def create_report(body: ReportCreateSchema, session: SessionDep):
admin = Administrator(session)
service = ReportService(admin)
return await service.create_report(
event_id=body.event_id,
requester_messenger_id=body.requester_messenger_id,
how_it_went=body.how_it_went,
beneficiaries_count=body.beneficiaries_count,
)
@api_router.get("/{event_id}", status_code=status.HTTP_200_OK, response_model=ResponseReportSchema)
async def get_report(
event_id: Annotated[str, Path()],
requester_messenger_id: Annotated[int, Query()],
session: SessionDep,
):
admin = Administrator(session)
service = ReportService(admin)
return await service.get_report(
event_id=UUID(event_id),
requester_messenger_id=requester_messenger_id,
)
Editor is loading...
Leave a Comment