Untitled
4ae4d
plain_text
13 days ago
102 kB
3
Indexable
f=services/bot/core/clients/event.py
f=services/bot/core/services/event.py
f=services/bot/core/schemas/participation.py
f=services/bot/core/markups/event.py
f=services/bot/core/markups/pagination.py
f=services/bot/core/handlers/events_ui.py
f=services/bot/core/handlers/moderation_menu.py
f=services/bot/core/handlers/__init__.py
f=services/bot/main.py
f=services/backend/internal/drivers/http/v1/volunteering/participation_card/participation_card_controller.go
f=services/backend/internal/drivers/http/v1/volunteering/participation_card/participation_card_request.go
f=services/backend/internal/drivers/http/v1/volunteering/volunteering_routes.go
f=services/backend/internal/domain/volunteering/participation_card/participation_card.go
f=services/backend/internal/domain/volunteering/participation_card/participation_card_page.go
f=services/backend/internal/domain/volunteering/participation_card/participation_card_query.go
f=services/backend/internal/domain/volunteering/participation_card/page_meta.go
f=services/backend/internal/adapters/volunteering/participation_card/participation_card_mapper.go
f=services/backend/internal/adapters/volunteering/participation_card/participation_card_query.go
f=services/backend/internal/adapters/volunteering/participation_card/participation_card_schema.go
f=services/backend/internal/drivers/http/v1/volunteering/participation/participation_controller.go
f=services/backend/internal/drivers/http/v1/volunteering/participation/participation_requests.go
f=services/backend/internal/domain/volunteering/participations/participation.go
f=services/backend/internal/domain/volunteering/participations/participation_service.go
f=services/backend/internal/domain/volunteering/participations/participation_repository.go
f=services/backend/internal/adapters/volunteering/participation/participation_repo.go
--- services/bot/core/clients/event.py ---
from core.clients.base import BaseApiClient
class EventClient(BaseApiClient):
def get_event_cards_page(
self,
*,
requester_messenger_id: int,
page: int = 1,
limit: int = 10,
title_str: str | None = None,
tag_ids: list[str] | None = None,
):
params: dict = {
"page": page,
"limit": limit,
}
if title_str:
params["title_str"] = title_str
if tag_ids:
params["tag_id"] = tag_ids
return self.get(
"/events/card/list",
headers={"Authorization": str(requester_messenger_id)},
params=params,
)
def get_my_event_cards_page(
self,
*,
requester_messenger_id: int,
page: int = 1,
limit: int = 10,
title_str: str | None = None,
tag_ids: list[str] | None = None,
):
params: dict = {
"page": page,
"limit": limit,
}
if title_str:
params["title_str"] = title_str
if tag_ids:
params["tag_id"] = tag_ids
return self.get(
"/events/card/list/mine",
headers={"Authorization": str(requester_messenger_id)},
params=params,
)
def get_event_info(self, event_id: str, requester_messenger_id: int):
"""GET /events/{event_id}/card"""
return self.get(
f"/events/{event_id}/card",
headers={"Authorization": str(requester_messenger_id)},
)
def sign_up(self, *, event_id: str, messenger_id: int):
"""POST /events/{event_id}/participations/signup"""
return self.post(
f"/events/{event_id}/participations/signup",
headers={"Authorization": str(messenger_id)},
)
def sign_out(self, *, event_id: str, messenger_id: int):
"""DELETE /events/{event_id}/participations/cancel"""
return self.delete(
f"/events/{event_id}/participations/cancel",
headers={"Authorization": str(messenger_id)},
)
def confirm_participation(self, *, event_id: str, messenger_id: int, code: str):
"""POST /events/{event_id}/participations/confirm"""
return self.post(
f"/events/{event_id}/participations/confirm",
headers={"Authorization": str(messenger_id)},
json={"code": code},
)
def add_tag(self, *, event_id: str, tag_id: str, messenger_id: int):
"""POST /events/{event_id}/tags"""
return self.post(
f"/events/{event_id}/tags",
headers={"Authorization": str(messenger_id)},
json={"tag_id": tag_id},
)
def remove_tag(self, *, event_id: str, tag_id: str, messenger_id: int):
"""DELETE /events/{event_id}/tags"""
return self.delete(
f"/events/{event_id}/tags",
headers={"Authorization": str(messenger_id)},
json={"tag_id": tag_id},
)
def create_event(
self,
*,
title: str,
description: str,
date_time: str,
gosb_id: str,
project_id: str = "",
verification_code: str,
hours: int,
participation_limit: int,
requester_messenger_id: int,
tags: list[str] | None = None,
):
"""POST /events"""
payload = {
"activation_code": verification_code,
"hours": hours,
"date_time": date_time,
"description": description,
"gosb_id": gosb_id,
"project_id": project_id,
"title": title,
"participation_limit": participation_limit,
}
if tags:
payload["selected_tags"] = tags
return self.post(
"/events",
headers={"Authorization": str(requester_messenger_id)},
json=payload,
)
def update_event(self, event_id: str, messenger_id: int, payload: dict[str, int | str]):
"""PATCH /events/{event_id}"""
return self.patch(
f"/events/{event_id}",
headers={"Authorization": str(messenger_id)},
json=payload,
)
--- services/bot/core/services/event.py ---
from datetime import datetime, timezone
from typing import Any
from core.clients import EventClient
from core.schemas import EventCardSchema, ParticipationSchema
from core.services.base import BaseService
from core.services.registry import BaseServiceRegistry
from core.utils import logger
class EventService(BaseService): # legacy
"""
сервис мероприятий на стороне бота.
на данный момент:
- ходит в backend через EventClient
- умеет получать список мероприятий по городу
- умеет получать одно мероприятие по id
- создает мероприятие через клиент используя мастера создания
"""
def __init__(self, registry: BaseServiceRegistry, client: EventClient): # legacy
# DI может вызвать клиент без аргументов.
super().__init__(registry)
self.client = client
def get_event_cards_page(
self,
*,
requester_messenger_id: int,
page: int = 1,
limit: int = 10,
title_str: str | None = None,
tag_ids: list[str] | None = None,
mine: bool = False,
) -> tuple[list[EventCardSchema], int, int]: # меняем тип возвращаемого списка
resp = (
self.client.get_my_event_cards_page(
requester_messenger_id=requester_messenger_id,
page=page,
limit=limit,
title_str=title_str,
tag_ids=tag_ids,
)
if mine
else self.client.get_event_cards_page(
requester_messenger_id=requester_messenger_id,
page=page,
limit=limit,
title_str=title_str,
tag_ids=tag_ids,
)
)
self._check_response(resp, ctx="getting event card list")
body = resp.json() or {}
items = body.get("event_cards") or []
page_meta = body.get("page_meta") or {}
result: list[EventCardSchema] = []
for item in items:
if not isinstance(item, dict):
raise ValueError("Invalid event card contract: item must be dict")
result.append(EventCardSchema.from_dict(item))
current_page = int(page_meta.get("current_page") or page)
total_pages = int(page_meta.get("total_pages") or current_page)
return result, current_page, total_pages
def sign_up(self, event_id: str, messenger_id: int) -> str | None:
"""messenger_id = peer.id (как на бэкенде: user.get_by_messenger_id)"""
resp = self.client.sign_up(event_id=event_id, messenger_id=messenger_id)
if resp.status_code == 409:
return resp.json()["error"]
self._check_response(resp, ctx="signing up")
return None
def get_event_by_id(
self,
event_id: str,
requester_messenger_id: int,
) -> EventCardSchema | None:
logger.debug(
"[EventService] get_event_by_id: event_id=%s requester=%s",
event_id,
requester_messenger_id,
)
resp = self.client.get_event_info(
event_id=event_id,
requester_messenger_id=requester_messenger_id,
)
if resp.status_code == 400:
return None
self._check_response(resp, ctx="getting event card")
logger.debug(
"[EventService] get_event_by_id: body=%s",
resp.json(),
)
return EventCardSchema.from_dict(resp.json() or {})
def sign_out_by_event(self, event_id: str, messenger_id: int) -> bool:
resp = self.client.sign_out(event_id=event_id, messenger_id=messenger_id)
return resp.status_code == 200
def enter_code_by_event(
self,
event_id: str,
messenger_id: int,
code: str,
) -> ParticipationSchema | None:
resp = self.client.confirm_participation(
event_id=event_id,
messenger_id=messenger_id,
code=code,
)
if resp.status_code in {400, 404}:
return None
self._check_response(resp, ctx="confirming participation")
return self.get_participation_status(
event_id=event_id,
requester_messenger_id=messenger_id,
)
def get_participation_status(self, event_id: str, requester_messenger_id: int):
resp = self.client.get_event_info(event_id, requester_messenger_id)
card = EventCardSchema.from_dict(resp.json() or {})
return card.participation
def add_tag(self, event_id: str, tag_id: str, messenger_id: int) -> bool:
resp = self.client.add_tag(event_id=event_id, tag_id=tag_id, messenger_id=messenger_id)
return resp.status_code == 200
def remove_tag(self, event_id: str, tag_id: str, messenger_id: int) -> bool:
resp = self.client.remove_tag(event_id=event_id, tag_id=tag_id, messenger_id=messenger_id)
return resp.status_code == 200
def create_from_wizard(self, data: dict[str, Any], peer) -> dict[str, Any]:
title = data.get("event_name", "")
description = data.get("event_description", "")
date_obj = data.get("event_date", "")
time_obj = data.get("event_time", "")
gosb_id = data.get("gosb_id", "")
project_id = data.get("project_id", "")
hours = data.get("hours", "")
participation_limit = data.get("event_participation_limit", "")
verification_code = data.get("event_verification_code", "")
# Теги: массив ID, сформированный в хендлере
tags: list[str] = data.get("event_app_tag_ids") or []
# === Формирование RFC3339-совместимой строки ===
event_dt = datetime.combine(date_obj, time_obj).replace(tzinfo=timezone.utc)
date_time = event_dt.isoformat().replace("+00:00", "Z")
resp = self.client.create_event(
title=title,
description=description,
date_time=date_time,
gosb_id=gosb_id,
project_id=project_id,
verification_code=verification_code,
hours=hours,
participation_limit=participation_limit,
requester_messenger_id=peer.id,
tags=tags if tags else None,
)
self._check_response(resp, ctx="creating event")
body = resp.json() or {}
return {
"TYPE": resp.json().get("create_type"),
"event_id": body.get("event_id"),
"request_id": body.get("request_id"),
}
def update_event(self, event_id: str, messenger_id: int, payload: dict[str, int | str]) -> bool:
resp = self.client.update_event(
event_id=event_id,
messenger_id=messenger_id,
payload=payload,
)
self._check_response(resp, ctx="patching event")
return True
--- services/bot/core/schemas/participation.py ---
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from core.utils import require_datetime
class ParticipationSchema(BaseModel):
"""Схема участия пользователя в мероприятии"""
messenger_id: str
event_id: str
state: str
created_at: datetime
updated_at: datetime
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "ParticipationSchema":
return cls(
messenger_id=str(data.get("messenger_id", "")),
event_id=str(data.get("event_id", "")),
state=str(data.get("state", "")),
created_at=require_datetime(data.get("created_at"), "participation.created_at"),
updated_at=require_datetime(data.get("updated_at"), "participation.updated_at"),
)
--- services/bot/core/markups/event.py ---
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder
from core.markups.pagination import pagination_keyboard
from core.schemas import EventCardSchema
from core.utils import format_datetime, wrap_text
def format_event_details(
card: EventCardSchema,
*,
show_code: bool,
show_organizer: bool,
show_report_status: bool = False,
report_exists: bool | None = None,
show_active: bool = False,
make_frame: bool = False,
wrap: bool = False,
) -> str:
"""формирует читаемое описание из EventCardSchema"""
def make_bold(string: str) -> str:
return f"*{string}*" if not make_frame else string
def make_block(string: str) -> str:
return f"`{string}`" if not make_frame else string
def make_title(string: str) -> str:
return f"{string}\n" if make_frame else f"{string}\n\n"
lines: list[str] = [
f"📣 {make_bold('Название')}: {card.event.title}",
f"⏰ {make_bold('Когда')}: {format_datetime(card.event.date_time)}",
f"🏷️ {make_bold('Теги')}: {_format_tags(card.tags)}",
f"🏆 {make_bold('Часы')}: {card.event.hours}",
f"🏛️ {make_bold('ГОСБ')}: {card.gosb.name}",
f"👥 {make_bold('Участники')}: {card.event.participation_count}/{card.event.participation_limit}",
]
if show_code:
lines.append(f"🎟️ {make_bold('Код')}: {card.event.verification_code or '—'}")
lines.extend(
[
f"📝 {make_bold('Описание')}: {wrap_text(card.event.description or '—')}"
if wrap
else f"📝 {make_bold('Описание')}: {card.event.description or '—'}",
]
)
if show_active:
lines.append("")
lines.append("✅ Согласовано" if card.event.active else "⏳ На согласовании")
lines.extend(["", "🔧 Техническая информация:"])
if show_report_status:
status = "✅ Создан" if report_exists else "Ещё не создан"
lines.append(f"Отчёт: {make_block(status)}")
if show_organizer:
lines.append(f"Creator id: {make_block(card.event.creator_id or '—')}")
lines.append(f"Project ID: {make_block(card.event.project_id or '—')}")
lines.append(f"ID: {make_block(card.event.id)}")
result = "\n".join(lines)
return make_title("📅 *Мероприятие*") + (
"```plain\n" + result + "\n```" if make_frame else result
)
def _format_tags(
tags: list | None,
) -> str: # TODO add type to 'tags' and simplify func depending on type
if not tags:
return "—"
try:
titles = [str(getattr(t, "title", "")).strip() for t in tags]
titles = [t for t in titles if t]
return ", ".join(titles) if titles else "—"
except Exception:
return str(tags)
def event_actions_keyboard( # legacy
event_id: str,
*,
is_participant: bool,
can_edit: bool,
back_value: str | None,
back_label: str,
enter_code_media_id: str,
sign_up_media_id: str,
sign_out_media_id: str,
edit_media_id: str = "event_menu_edit",
delete_media_id: str = "event_menu_delete",
can_create_report: bool = False,
can_view_report: bool = False,
can_sign_up: bool = False,
ctx: str | None = None,
) -> list[InteractiveMediaGroup]:
actions = []
v = f"{event_id}|{ctx}" if ctx else event_id
actions.append(Button(media_id=enter_code_media_id, value=v, label="🏅 Ввести код"))
if is_participant:
actions.append(Button(media_id=sign_out_media_id, value=v, label="❌ Отписаться"))
elif can_sign_up:
actions.append(Button(media_id=sign_up_media_id, value=v, label="✅ Записаться"))
else:
actions.append(Button(media_id=sign_up_media_id, value=v, label="⚠️ Записаться"))
if can_edit:
actions.append(Button(media_id=edit_media_id, value=v, label="✏️ Редактировать"))
# TODO: separate permissions
actions.append(Button(media_id="event_participants_open", value=v, label="👥 Участники"))
actions.append(Button(media_id=delete_media_id, value=v, label="🗑 Удалить"))
if can_create_report:
actions.append(Button(media_id="event_report_create", value=v, label="📄 Создать отчёт"))
if can_view_report:
actions.append(Button(media_id="event_report_view", value=v, label="📄 Посмотреть отчёт"))
if back_value is not None:
actions.append(Button(media_id=back_value, value="", label=back_label))
return MediaGroupBuilder(actions).build()
def event_edit_fields_keyboard( # legacy
event_id: str,
*,
leave_media_id: str = "leave",
leave_value: str = "moderation",
leave_label: str = "⬅️ В мастерскую",
) -> list[InteractiveMediaGroup]:
group_builder_1 = MediaGroupBuilder(
[
Button(
media_id="event_menu_edit_name",
value=f"{event_id}|title",
label="📣 Название",
),
Button(
media_id="event_menu_edit_description",
value=f"{event_id}|description",
label="📝 Описание",
),
Button(
media_id="event_menu_edit_date",
value=f"{event_id}|date",
label="📅 Дата",
),
Button(
media_id="event_menu_edit_time",
value=f"{event_id}|time",
label="⏰ Время",
),
Button(
media_id="event_menu_edit_tags",
value=f"{event_id}|tags",
label="🏷️ Теги",
),
Button(
media_id="event_menu_edit_code",
value=f"{event_id}|code",
label="🎟️ Код",
),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button(
media_id="event_menu_edit_organizers",
value=f"{event_id}|event_organizers",
label="🤓 Организаторы",
),
Button(
media_id="event_menu_edit_hours",
value=f"{event_id}|hours",
label="🏆 Часы",
),
Button(
media_id="event_menu_edit_gosb",
value=f"{event_id}|gosb_id",
label="🏛️ ГОСБ",
),
Button(
media_id="event_menu_edit_participation_limit",
value=f"{event_id}|participation_limit",
label="👥 Кол-во волонтеров",
),
Button(
media_id=leave_media_id,
value=leave_value,
label=leave_label,
),
]
)
return group_builder_1.merge([group_builder_2])
def user_events_pagination_keyboard( # legacy
*, offset: int, limit: int, has_prev: bool, has_next: bool
) -> list[InteractiveMediaGroup]:
return pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
prev_media_id="user_events_prev",
next_media_id="user_events_next",
leave_media_id="volunteer_home",
leave_label="🏡 В дом волонтёра",
keep_layout=True,
add_view_by_uuid_button=True,
view_by_uuid_value="events",
)
def part_events_pagination_keyboard( # legacy
*, offset: int, limit: int, has_prev: bool, has_next: bool
) -> list[InteractiveMediaGroup]:
return pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
prev_media_id="part_events_prev",
next_media_id="part_events_next",
leave_media_id="volunteer_home",
leave_label="🏡 В дом волонтёра",
keep_layout=True,
add_view_by_uuid_button=True,
view_by_uuid_value="part_events",
)
def my_events_pagination_keyboard( # legacy
*, offset: int, limit: int, has_prev: bool, has_next: bool
) -> list[InteractiveMediaGroup]:
return pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
prev_media_id="my_events_prev",
next_media_id="my_events_next",
leave_media_id="moderation",
leave_label="🛡️ В мастерскую",
keep_layout=True,
add_view_by_uuid_button=True,
view_by_uuid_value="my_events",
)
def _event_button_label(event_card: EventCardSchema, ctx: str | None = None) -> str:
"""Формирует подпись для кнопки мероприятия"""
ev = event_card.event
participation = event_card.participation
date_str = format_datetime(getattr(ev, "date_time", None))
title = getattr(ev, "title", "—")
if ctx == "my_events":
status = "на согласовании ⏳"
if event_card.event.active:
status = "согласовано ✅"
return f"{title} | {date_str} — {status}"
participation_emoji = "⚪"
if participation:
state = getattr(participation, "state", "")
if state == "signed_up":
participation_emoji = "📝" # записан
elif state == "confirmed":
participation_emoji = "✅" # подтверждено
elif state == "cancelled":
participation_emoji = "❌" # отменено (не пришел)
return f"{title} | {date_str} {participation_emoji}"
def events_select_keyboard(
events_cards: list[EventCardSchema],
*,
open_media_id: str,
start_index: int = 1,
per_row: int = 1,
label_max: int = 120,
ctx: str | None = None,
) -> list[InteractiveMediaGroup]:
"""
строит список кнопок по мероприятиям
нажатие вызывает handler по _id = open_media_id, value = event.id
"""
groups: list[InteractiveMediaGroup] = []
row: list[Button] = []
idx = start_index
for event_card in events_cards:
label = _event_button_label(event_card, ctx)
row.append(
Button(
media_id=open_media_id, value=str(getattr(event_card.event, "id", "")), label=label
)
)
idx += 1
if len(row) >= per_row:
groups.extend(MediaGroupBuilder(row).build())
row = []
if row:
groups.extend(MediaGroupBuilder(row).build())
return groups
def view_by_uuid_keyboard(*, media_id: str, label: str) -> list[InteractiveMediaGroup]: # legacy
return MediaGroupBuilder([Button(media_id=media_id, value="", label=label)]).build()
def all_events_pagination_keyboard(
*, offset: int, limit: int, has_prev: bool, has_next: bool
): # legacy
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_media_id="moderation",
leave_label="🛡️ В мастерскую",
keep_layout=True,
add_view_by_uuid_button=True,
view_by_uuid_value="moderation",
)
--- services/bot/core/markups/pagination.py ---
from __future__ import annotations
from typing import cast
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder
def pagination_keyboard( # legacy
*,
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 | None = None,
leave_value: str | None = "",
leave_label: str | 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 - то возвращаем пустой лист.
"""
if leave_value is None:
leave_value = ""
if leave_label is None:
leave_label = "⬅️ Назад"
def _btn(media_id: str, value: str, label: str) -> Button: # legacy
return Button(media_id=media_id, value=value, label=label)
def _ph(slot: str) -> Button: # legacy
return _btn(
placeholder_media_id + "_" + slot,
placeholder_value,
placeholder_label,
)
def _prev() -> Button: # legacy
return _btn(prev_media_id, str(max(offset - limit, 0)), prev_label)
def _next() -> Button: # legacy
return _btn(next_media_id, str(offset + limit), next_label)
def _view_by_uuid() -> Button: # legacy
return _btn(
view_by_uuid_media_id,
str(view_by_uuid_value),
str(view_by_uuid_label),
)
def _leave() -> Button | None: # legacy
if not include_leave or leave_media_id is None:
return None
return _btn(leave_media_id, leave_value, 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 cast(list[InteractiveMediaGroup], MediaGroupBuilder(buttons).build())
# --- default linear layout (как было)
buttons = []
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 cast(list[InteractiveMediaGroup], MediaGroupBuilder(buttons).build() if buttons else [])
--- services/bot/core/handlers/events_ui.py ---
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder
from core.bot_kit.fsm import FSMContext
from core.config import bot
from core.handlers.events_pages import EVENTS_PAGE_SIZE, fetch_events_page
from core.markups import (
all_events_pagination_keyboard,
event_actions_keyboard,
events_filters_button_keyboard,
events_select_keyboard,
format_event_details,
my_events_pagination_keyboard,
part_events_pagination_keyboard,
points_events_pagination_keyboard,
points_menu_keyboard,
user_events_pagination_keyboard,
)
from core.schemas import EventCardsPageSchema, UserSchema
from core.services import EventService, ReportService
from core.utils import format_events_filter_summary, get_events_filter, logger
if TYPE_CHECKING:
from core.schemas.event import EventCardSchema
@dataclass(frozen=True)
class EventUIContext: # legacy
name: str
# list
list_open_media_id: str
pagination_builder: Callable[..., list[InteractiveMediaGroup]]
extra_groups_builder: Callable[[], list[InteractiveMediaGroup]] | None
# card
back_value: str | None
back_label: str
enter_code_media_id: str
sign_up_media_id: str
sign_out_media_id: str
_UI: dict[str, EventUIContext] = {
# дом волонтёра -> все мероприятия
"events": EventUIContext( # legacy
name="events",
list_open_media_id="user_events_open",
pagination_builder=user_events_pagination_keyboard,
extra_groups_builder=None,
back_value="events",
back_label="⬅️ К списку мероприятий",
enter_code_media_id="event_user_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
# дом волонтёра -> мои меропрития
"part_events": EventUIContext( # legacy
name="part_events",
list_open_media_id="user_events_open",
pagination_builder=part_events_pagination_keyboard,
extra_groups_builder=None,
back_value="part_events",
back_label="⬅️ К списку мероприятий",
enter_code_media_id="event_user_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
# дом волонтёра -> посмотреть по uuid
"volunteer_home": EventUIContext( # legacy
name="volunteer_home",
list_open_media_id="user_events_open", # не используется в этом контексте
pagination_builder=user_events_pagination_keyboard,
extra_groups_builder=None,
back_value="volunteer_home",
back_label="⬅️ В дом волонтёра",
enter_code_media_id="event_user_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
# дом модерации -> все мероприятия
"moderation": EventUIContext( # legacy
name="moderation",
list_open_media_id="all_events_open",
pagination_builder=all_events_pagination_keyboard,
extra_groups_builder=None,
back_value="all_events",
back_label="⬅️ К списку мероприятий",
enter_code_media_id="event_menu_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
# меню баллов -> мероприятия с начисленными баллами
"points": EventUIContext( # legacy
name="points",
list_open_media_id="points_event_open",
pagination_builder=points_events_pagination_keyboard,
extra_groups_builder=points_menu_keyboard, # лидерборд + назад
back_value="points",
back_label="⬅️ К баллам",
enter_code_media_id="event_user_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
# дом модерации -> посмотреть по uuid
"moderation_menu": EventUIContext( # legacy
name="moderation_menu",
list_open_media_id="all_events_open", # не используется в этом контексте
pagination_builder=all_events_pagination_keyboard,
extra_groups_builder=None,
back_value="moderation",
back_label="⬅️ В мастерскую",
enter_code_media_id="event_menu_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
# дом модерации -> мои мероприятия
"my_events": EventUIContext( # legacy
name="my_events",
list_open_media_id="my_events_open",
pagination_builder=my_events_pagination_keyboard,
extra_groups_builder=None,
back_value="my_events",
back_label="⬅️ К моим мероприятиям",
enter_code_media_id="event_menu_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
# ответ нейронки -> карточка мероприятия
"ai_dialog_event": EventUIContext( # legacy
name="ai_dialog_event",
list_open_media_id="user_events_open",
pagination_builder=user_events_pagination_keyboard,
extra_groups_builder=None,
back_value=None,
back_label="",
enter_code_media_id="event_user_enter_code",
sign_up_media_id="event_user_sign_up",
sign_out_media_id="event_user_sign_out",
),
}
def get_ui(ctx_name: str) -> EventUIContext: # legacy
return _UI.get(ctx_name, _UI["events"])
def build_back_keyboard(ctx_name: str) -> list[InteractiveMediaGroup]:
ui = get_ui(ctx_name)
if not ui.back_value:
return []
return MediaGroupBuilder(
[Button(media_id=ui.back_value, value="", label=ui.back_label)]
).build()
def split_event_value(raw: Any) -> tuple[str, str | None]: # legacy
"""
ожидаем:
- "<event_id>"
- "<event_id>|<ctx>"
"""
s = str(raw or "").strip()
if "|" in s:
ev_id, ctx = s.split("|", 1)
return ev_id.strip(), (ctx.strip() or None)
return s, None
def _send_events_list_screen( # legacy
peer,
*,
ui: EventUIContext,
ctx_name: str,
offset: int,
limit: int,
page_data: EventCardsPageSchema,
note: str | None = None,
header: str | None = None,
empty_text: str | None = None,
empty_groups: list[InteractiveMediaGroup] | None = None,
filter_summary: str | None = None,
show_active: bool = False,
):
# ИСПРАВЛЕНО: используем event_cards вместо events
event_cards = page_data.event_cards # list[EventCardSchema]
has_prev = page_data.has_prev
has_next = page_data.has_next
prefix = f"{note}\n\n" if note else ""
header_part = (header if header is not None else "📅 Мероприятия") + "\n\n"
filter_part = f"\n\n{filter_summary}" if filter_summary else ""
if not event_cards: # проверяем карточки
text = (
prefix
+ header_part
+ (
empty_text
or "⚠️ Мероприятий не найдено.\n\nПопробуй изменить критерии или вернись назад."
)
+ filter_part
)
im: list[InteractiveMediaGroup] = []
footer: list[InteractiveMediaGroup] = []
if empty_groups is not None:
footer = empty_groups
elif ui.extra_groups_builder:
footer = ui.extra_groups_builder()
im += events_filters_button_keyboard(ctx_name=ctx_name, offset=offset)
im += ui.pagination_builder(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
)
im += footer
bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)
return
text = (
prefix
+ header_part
+ f"Страница: {page_data.current_page} / {page_data.total_pages}\n\n"
+ "Выбери мероприятие:"
+ filter_part
)
im: list[InteractiveMediaGroup] = []
im += events_select_keyboard(
event_cards,
ctx=ctx_name,
open_media_id=ui.list_open_media_id,
start_index=offset + 1,
per_row=1,
)
im += events_filters_button_keyboard(ctx_name=ctx_name, offset=offset)
im += ui.pagination_builder(offset=offset, limit=limit, has_prev=has_prev, has_next=has_next)
if ui.extra_groups_builder:
im += ui.extra_groups_builder()
bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)
def send_event_cards_page(
peer,
*,
offset: int,
event_service: EventService,
ctx_name: str,
note: str | None = None,
context: FSMContext | None = None,
):
ui = get_ui(ctx_name)
limit = EVENTS_PAGE_SIZE
flt = get_events_filter(context, ctx_name=ctx_name) if context is not None else None
summary = format_events_filter_summary(flt) if context is not None else None
page_data = fetch_events_page(
event_service=event_service,
offset=offset,
limit=limit,
requester_messenger_id=peer.id,
filters=flt,
)
_send_events_list_screen(
peer,
ui=ui,
ctx_name=ctx_name,
offset=offset,
limit=limit,
page_data=page_data,
note=note,
filter_summary=summary,
)
def send_part_events_page( # legacy
peer,
*,
offset: int,
event_service: EventService,
context: FSMContext | None = None,
note: str | None = None,
):
bot.messaging.send_message(
peer=peer,
text=("f{note}\n\n" if note else "Мои мероприятия\n\nПока не реализовано"),
interactive_media_groups=build_back_keyboard("part_events"),
)
def send_points_page( # legacy
peer,
*,
offset: int,
event_service: EventService,
context: FSMContext | None = None,
):
bot.messaging.send_message(
peer=peer,
text=("Меорприятия за которые я получил баллы\n\nПока не реализовано"),
interactive_media_groups=build_back_keyboard("part_events"),
)
def send_my_events_page( # legacy
peer,
*,
offset: int,
event_service: EventService,
requester_messenger_id: int,
ctx_name: str,
note: str | None = None,
context: FSMContext | None = None,
):
ui = get_ui(ctx_name)
limit = EVENTS_PAGE_SIZE
flt = get_events_filter(context, ctx_name=ctx_name) if context is not None else None
summary = format_events_filter_summary(flt) if context is not None else None
page_data = fetch_events_page(
event_service=event_service,
offset=offset,
limit=limit,
requester_messenger_id=peer.id,
filters=flt,
mine=True,
)
_send_events_list_screen(
peer,
ui=ui,
ctx_name=ctx_name,
offset=offset,
limit=limit,
page_data=page_data,
note=note,
header="👤 Мои мероприятия",
filter_summary=summary,
show_active=True,
)
def send_event_card(
peer,
*,
event_id: str,
event_service: EventService,
report_service: ReportService | None = None,
user: UserSchema | None,
ctx_name: str,
note: str | None = None,
show_organizer: bool = True,
):
ui = get_ui(ctx_name)
card: EventCardSchema | None = event_service.get_event_by_id(event_id, peer.id)
if card is None:
text = f"{note}\n\n⚠️ Мероприятие не найдено." if note else "⚠️ Мероприятие не найдено."
bot.messaging.send_message(
peer=peer,
text=text,
interactive_media_groups=build_back_keyboard(ctx_name),
)
return
# TODO: 1. Убрать проверку права редактирования в сервис
can_edit = card.allowed_actions.can_edit if card.allowed_actions else False
is_participant = card.participation is not None
show_code = card.allowed_actions.can_view_code if card.allowed_actions else False
# TODO: 2. Отдельная проверка на право доступа к отчёту
can_access_reports = can_edit
can_sign_up = card.allowed_actions.can_sign_up if card.allowed_actions else False
logger.debug(f"""
for event with id {event_id}:
can_sign_up = {can_sign_up}
card.event.participation_count < card.event.participation_limit is {card.event.participation_count < card.event.participation_limit}
card.event.archived_at is not None is {card.event.archived_at is not None}
not card.event.deleted is {not card.event.deleted}
card.event.active is {card.event.active}
""")
report_exists: bool | None = None
if can_access_reports and report_service is not None:
report_exists = report_service.report_exists(
event_id=str(card.event.id),
requester_messenger_id=peer.id,
)
else:
report_exists = None
# TODO: 3 Separete permissions
can_create_report = bool(can_edit and (report_exists is False))
can_view_report = bool(can_access_reports and (report_exists is True))
show_active = ctx_name == "my_events"
base = format_event_details(
card,
show_active=show_active,
show_code=show_code,
show_organizer=show_organizer,
show_report_status=bool(can_access_reports and report_exists is not None),
report_exists=bool(report_exists) if report_exists is not None else None,
)
text = f"{note}\n\n{base}" if note else base
bot.messaging.send_message(
peer=peer,
text=text,
interactive_media_groups=event_actions_keyboard(
card.event.id,
is_participant=is_participant,
can_edit=can_edit,
back_value=ui.back_value,
back_label=ui.back_label,
enter_code_media_id=ui.enter_code_media_id,
sign_up_media_id=ui.sign_up_media_id,
sign_out_media_id=ui.sign_out_media_id,
ctx=ui.name, # важно: чтобы value был "<event_id>|<ctx>"
can_create_report=can_create_report,
can_view_report=can_view_report,
can_sign_up=can_sign_up,
),
)
# TODO перенести клавиатуру туда где ей место
def build_back_to_event_keyboard(
event_id: str, ctx_name: str
) -> list[InteractiveMediaGroup]: # legacy
ui = get_ui(ctx_name)
return MediaGroupBuilder(
[
Button(
media_id=ui.list_open_media_id,
value=f"{event_id}|{ui.name}",
label="⬅️ К мероприятию",
)
]
).build()
# TODO перенести клавиатуру туда где ей место
def build_back_to_event_editing_keyboard(
event_id: str, ctx_name: str
) -> list[InteractiveMediaGroup]: # legacy
return MediaGroupBuilder(
[
Button(
media_id="event_menu_edit",
value=f"{event_id}|{ctx_name}",
label="⬅️ К редактированию мероприятия",
)
]
).build()
--- services/bot/core/handlers/moderation_menu.py ---
from dialog_bot_sdk.entities.messaging import UpdateInteractiveMediaEvent
from core.bot_kit.fsm import FSMContext
from core.bot_kit.router import Router
from core.config import bot
from core.handlers.events_ui import (
send_event_card,
send_event_cards_page,
send_my_events_page,
split_event_value,
)
from core.markups import moderation_menu_keyboard
from core.schemas import UserSchema
from core.services import EventService, ReportService
from core.utils import clear_context_keep_events_filters, delete_prev_message_by_peer
moderation_menu_rt = Router()
@bot.di
def moderation_menu_handler(event: UpdateInteractiveMediaEvent, context: FSMContext): # legacy
clear_context_keep_events_filters(context)
delete_prev_message_by_peer(bot, event.peer)
bot.messaging.send_message(
peer=event.peer,
text="Ты в мастерской!\n\nВыбери, что хочешь сделать 👇",
interactive_media_groups=moderation_menu_keyboard(),
)
@bot.di
def all_events_handler( # legacy
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
):
delete_prev_message_by_peer(bot, event.peer)
clear_context_keep_events_filters(context)
send_event_cards_page(
event.peer,
offset=0,
event_service=event_service,
ctx_name="moderation",
context=context,
)
@bot.di
def my_events_handler( # legacy
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
):
delete_prev_message_by_peer(bot, event.peer)
clear_context_keep_events_filters(context)
send_my_events_page(
event.peer,
offset=0,
event_service=event_service,
requester_messenger_id=event.peer.id,
ctx_name="my_events",
context=context,
)
@bot.di
def my_events_page_handler( # legacy
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
):
delete_prev_message_by_peer(bot, event.peer)
clear_context_keep_events_filters(context)
try:
offset = int(event.data.value)
if offset < 0:
offset = 0
except Exception:
offset = 0
send_my_events_page(
event.peer,
offset=offset,
event_service=event_service,
requester_messenger_id=event.peer.id,
ctx_name="my_events",
context=context,
)
@bot.di
def my_events_open_handler( # legacy
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
report_service: ReportService,
user: UserSchema | None,
):
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
event_id, ctx_name = split_event_value(event.data.value)
send_event_card(
event.peer,
event_id=event_id,
event_service=event_service,
user=user,
ctx_name=ctx_name or "my_events",
report_service=report_service,
)
@bot.di
def all_events_page_handler( # legacy
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
):
delete_prev_message_by_peer(bot, event.peer)
clear_context_keep_events_filters(context)
try:
offset = int(event.data.value)
if offset < 0:
offset = 0
except Exception:
offset = 0
send_event_cards_page(
event.peer,
offset=offset,
event_service=event_service,
ctx_name="moderation",
context=context,
)
@bot.di
def all_events_open_handler( # legacy
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
report_service: ReportService,
user: UserSchema | None,
):
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
event_id, ctx = split_event_value(event.data.value)
ctx_name = ctx or "moderation"
send_event_card(
event.peer,
event_id=event_id,
event_service=event_service,
user=user,
ctx_name=ctx_name,
report_service=report_service,
)
--- services/bot/core/handlers/__init__.py ---
from core.bot_kit.router import Router
from .admin import admin_menu_handler, admin_rt, del_moderator_handler, set_moderator_handler
from .ai import ai_assistant_handler, ai_rt, event_source_callback_handler
from .event import (
event_participants_open_handler,
event_user_enter_code_start_handler,
event_user_sign_out_handler,
event_user_sign_up_handler,
events_menu_handler,
part_events_menu_handler,
part_events_page_handler,
user_events_open_handler,
user_events_page_handler,
user_events_rt,
)
from .events_filters import (
events_filters_apply_handler,
events_filters_back_handler,
events_filters_mode_open_handler,
events_filters_mode_select_handler,
events_filters_not_implemented_handler,
events_filters_open_handler,
events_filters_reset_handler,
events_filters_tags_open_handler,
events_filters_tags_toggle_handler,
)
from .main import main_menu_handler, start_handler
from .moderation_create import (
create_event_start_handler,
create_rt,
event_create_back_to_time_handler,
project_input_dash_handler,
)
from .moderation_edit import (
edit_rt,
event_menu_delete_handler,
event_menu_edit_field_handler,
event_menu_edit_handler,
event_menu_enter_code_start_handler,
event_tags_toggle_handler,
gosb_approved,
gosb_update_selected,
tb_approved,
tb_reselect_handler,
tb_update_selected,
)
from .moderation_menu import (
all_events_handler,
all_events_open_handler,
all_events_page_handler,
moderation_menu_handler,
moderation_menu_rt,
my_events_handler,
my_events_open_handler,
my_events_page_handler,
)
from .noop import noop_handler
from .points import points_event_open_handler, points_menu_handler, points_page_handler, points_rt
from .profile import my_profile_handler, profile_tags_toggle_handler, update_user_interests_start
from .profile import rt as update_user_rt
from .report import (
report_create_start_handler,
report_rt,
report_view_handler,
reports_export_handler,
)
from .request import (
all_requests_handler,
my_requests_handler,
my_requests_page_handler,
request_approve_handler,
request_cancel_handler,
request_open_handler,
request_reject_handler,
request_repeat_handler,
requests_page_handler,
requests_rt,
)
from .view_by_uuid import rt as view_by_uuid_rt
from .view_by_uuid import view_by_uuid_start_handler
from .volunteer_home import points_leaderboard_handler, volunteer_home_handler
events_rt = Router()
events_rt.register(moderation_menu_rt)
events_rt.register(create_rt)
events_rt.register(edit_rt)
__all__ = [
"admin_menu_handler",
"admin_rt",
"ai_assistant_handler",
"ai_rt",
"all_events_handler",
"all_events_open_handler",
"all_events_page_handler",
"all_requests_handler",
"create_event_start_handler",
"create_rt",
"del_moderator_handler",
"edit_rt",
"event_create_back_to_time_handler",
"event_menu_delete_handler",
"event_menu_edit_field_handler",
"event_menu_edit_handler",
"event_menu_enter_code_start_handler",
"event_participants_open_handler",
"event_source_callback_handler",
"event_tags_toggle_handler",
"event_user_enter_code_start_handler",
"event_user_sign_out_handler",
"event_user_sign_up_handler",
"events_filters_apply_handler",
"events_filters_back_handler",
"events_filters_mode_open_handler",
"events_filters_mode_select_handler",
"events_filters_not_implemented_handler",
"events_filters_open_handler",
"events_filters_reset_handler",
"events_filters_tags_open_handler",
"events_filters_tags_toggle_handler",
"events_menu_handler",
"events_rt",
"gosb_approved",
"gosb_update_selected",
"main_menu_handler",
"moderation_menu_handler",
"moderation_menu_rt",
"my_events_handler",
"my_events_open_handler",
"my_events_page_handler",
"my_profile_handler",
"my_requests_handler",
"my_requests_page_handler",
"noop_handler",
"part_events_menu_handler",
"part_events_page_handler",
"points_event_open_handler",
"points_leaderboard_handler",
"points_menu_handler",
"points_page_handler",
"points_rt",
"profile_tags_toggle_handler",
"project_input_dash_handler",
"report_create_start_handler",
"report_rt",
"report_view_handler",
"reports_export_handler",
"request_approve_handler",
"request_cancel_handler",
"request_open_handler",
"request_reject_handler",
"request_repeat_handler",
"requests_page_handler",
"requests_rt",
"set_moderator_handler",
"start_handler",
"tb_approved",
"tb_reselect_handler",
"tb_update_selected",
"update_user_interests_start",
"update_user_rt",
"user_events_open_handler",
"user_events_page_handler",
"user_events_rt",
"view_by_uuid_rt",
"view_by_uuid_start_handler",
"volunteer_home_handler",
]
--- services/bot/main.py ---
from core.bot_kit.router import Router
from core.config import bot
from core.handlers import (
admin_menu_handler,
admin_rt,
ai_assistant_handler,
ai_rt,
all_events_handler,
all_events_open_handler,
all_events_page_handler,
all_requests_handler,
create_event_start_handler,
create_rt,
del_moderator_handler,
edit_rt,
event_create_back_to_time_handler,
event_menu_delete_handler,
event_menu_edit_field_handler,
event_menu_edit_handler,
event_menu_enter_code_start_handler,
event_participants_open_handler,
event_source_callback_handler,
event_tags_toggle_handler,
event_user_enter_code_start_handler,
event_user_sign_out_handler,
event_user_sign_up_handler,
events_filters_apply_handler,
events_filters_back_handler,
events_filters_mode_open_handler,
events_filters_mode_select_handler,
events_filters_not_implemented_handler,
events_filters_open_handler,
events_filters_reset_handler,
events_filters_tags_open_handler,
events_filters_tags_toggle_handler,
events_menu_handler,
events_rt,
gosb_approved,
gosb_update_selected,
main_menu_handler,
moderation_menu_handler,
moderation_menu_rt,
my_events_handler,
my_events_open_handler,
my_events_page_handler,
my_profile_handler,
my_requests_handler,
my_requests_page_handler,
noop_handler,
part_events_menu_handler,
part_events_page_handler,
points_event_open_handler,
points_leaderboard_handler,
points_menu_handler,
points_page_handler,
points_rt,
profile_tags_toggle_handler,
project_input_dash_handler,
report_create_start_handler,
report_rt,
report_view_handler,
reports_export_handler,
request_approve_handler,
request_cancel_handler,
request_open_handler,
request_reject_handler,
request_repeat_handler,
requests_page_handler,
requests_rt,
set_moderator_handler,
start_handler,
tb_approved,
tb_reselect_handler,
tb_update_selected,
update_user_interests_start,
update_user_rt,
user_events_open_handler,
user_events_page_handler,
user_events_rt,
view_by_uuid_rt,
view_by_uuid_start_handler,
volunteer_home_handler,
)
from dialog_bot_sdk.entities.messaging import CommandHandler, EventHandler
def handlers_setting() -> None: # legacy
router = Router()
router.register(edit_rt)
router.register(moderation_menu_rt)
router.register(create_rt)
router.register(update_user_rt)
router.register(events_rt)
router.register(user_events_rt)
router.register(points_rt)
router.register(view_by_uuid_rt)
router.register(report_rt)
router.register(ai_rt)
router.register(requests_rt)
router.register(admin_rt)
router.subscribe(bot)
bot.messaging.command_handler(
[
CommandHandler(
function=start_handler,
command="start",
description="Расскажу о себе",
),
CommandHandler(
function=moderation_menu_handler,
command="workship",
description="Мастерская",
),
CommandHandler(
function=create_event_start_handler,
command="create_event",
description="Создать мероприятие",
),
CommandHandler(
function=all_events_handler,
command="all_events",
description="Показать все мероприятия",
),
CommandHandler(
function=reports_export_handler,
command="reports_export",
description="Выгрузка отчётов",
),
]
)
bot.messaging.event_handler(
[
EventHandler(function=volunteer_home_handler, _id="volunteer_home"),
EventHandler(function=main_menu_handler, _id="main_menu"),
EventHandler(function=ai_assistant_handler, _id="ai_assistant"),
EventHandler(function=my_profile_handler, _id="my_profile"),
EventHandler(function=update_user_interests_start, _id="update_user_interests"),
EventHandler(function=profile_tags_toggle_handler, _id="profile_tags_toggle"),
EventHandler(function=points_menu_handler, _id="points"),
EventHandler(function=points_leaderboard_handler, _id="points_leaderboard"),
EventHandler(function=moderation_menu_handler, _id="moderation"),
EventHandler(function=reports_export_handler, _id="download_reports_list"),
EventHandler(function=all_events_handler, _id="all_events"),
EventHandler(function=events_menu_handler, _id="events"),
EventHandler(function=part_events_menu_handler, _id="part_events"),
EventHandler(function=user_events_page_handler, _id="user_events_prev"),
EventHandler(function=user_events_page_handler, _id="user_events_next"),
EventHandler(function=part_events_page_handler, _id="part_events_prev"),
EventHandler(function=part_events_page_handler, _id="part_events_next"),
EventHandler(function=event_user_sign_up_handler, _id="event_user_sign_up"),
EventHandler(function=event_user_sign_out_handler, _id="event_user_sign_out"),
EventHandler(function=event_user_enter_code_start_handler, _id="event_user_enter_code"),
EventHandler(function=event_tags_toggle_handler, _id="event_tags_toggle"),
EventHandler(function=event_participants_open_handler, _id="event_participants_open"),
EventHandler(function=report_create_start_handler, _id="event_report_create"),
EventHandler(function=report_view_handler, _id="event_report_view"),
EventHandler(function=all_events_page_handler, _id="all_events_prev"),
EventHandler(function=all_events_page_handler, _id="all_events_next"),
EventHandler(function=my_events_handler, _id="my_events"),
EventHandler(function=my_events_page_handler, _id="my_events_prev"),
EventHandler(function=my_events_page_handler, _id="my_events_next"),
EventHandler(function=my_events_open_handler, _id="my_events_open"),
EventHandler(function=create_event_start_handler, _id="create_event"),
EventHandler(function=admin_menu_handler, _id="administration"),
EventHandler(function=event_menu_enter_code_start_handler, _id="event_menu_enter_code"),
EventHandler(function=event_menu_delete_handler, _id="event_menu_delete"),
EventHandler(function=event_menu_edit_handler, _id="event_menu_edit"),
EventHandler(function=view_by_uuid_start_handler, _id="view_by_uuid"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_name"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_description"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_date"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_time"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_tags"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_organizers"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_hours"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_code"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_gosb"),
EventHandler(
function=event_menu_edit_field_handler, _id="event_menu_edit_participation_limit"
),
EventHandler(function=user_events_open_handler, _id="user_events_open"),
EventHandler(function=all_events_open_handler, _id="all_events_open"),
EventHandler(function=points_page_handler, _id="points_prev"),
EventHandler(function=points_page_handler, _id="points_next"),
EventHandler(function=points_event_open_handler, _id="points_event_open"),
EventHandler(function=points_event_open_handler, _id="points_events_open"),
EventHandler(function=events_filters_open_handler, _id="events_filters_open"),
EventHandler(function=events_filters_apply_handler, _id="events_filters_apply"),
EventHandler(function=events_filters_reset_handler, _id="events_filters_reset"),
EventHandler(function=events_filters_back_handler, _id="events_filters_back"),
EventHandler(function=events_filters_tags_open_handler, _id="events_filters_tags"),
EventHandler(
function=events_filters_tags_toggle_handler, _id="events_filters_tags_toggle"
),
EventHandler(
function=events_filters_not_implemented_handler, _id="events_filters_date"
),
EventHandler(function=events_filters_mode_open_handler, _id="events_filters_mode"),
EventHandler(
function=events_filters_mode_select_handler, _id="events_filters_mode_select"
),
EventHandler(function=all_requests_handler, _id="requests"),
EventHandler(function=my_requests_handler, _id="my_requests"),
EventHandler(function=requests_page_handler, _id="requests_prev"),
EventHandler(function=my_requests_page_handler, _id="my_requests_prev"),
EventHandler(function=requests_page_handler, _id="requests_next"),
EventHandler(function=my_requests_page_handler, _id="my_requests_next"),
EventHandler(function=request_open_handler, _id="request_open"),
EventHandler(function=request_open_handler, _id="my_request_open"),
EventHandler(function=request_approve_handler, _id="request_approve"),
EventHandler(function=request_reject_handler, _id="request_reject"),
EventHandler(function=request_repeat_handler, _id="request_repeat"),
EventHandler(function=request_cancel_handler, _id="request_cancel"),
# TODO почистить
EventHandler(function=project_input_dash_handler, _id="project_input_dash"),
EventHandler(function=noop_handler, _id="noop_prev"),
EventHandler(function=noop_handler, _id="noop_mid"),
EventHandler(function=noop_handler, _id="noop_next"),
EventHandler(function=noop_handler, _id="noop_uuid"),
EventHandler(function=tb_approved, _id="tb_approved"),
EventHandler(function=gosb_approved, _id="gosb_approved"),
EventHandler(function=tb_update_selected, _id="tb_select"),
EventHandler(
function=event_create_back_to_time_handler, _id="event_create_back_to_time"
),
EventHandler(function=tb_reselect_handler, _id="tb_reselect"),
EventHandler(function=gosb_update_selected, _id="gosb_select"),
EventHandler(function=event_source_callback_handler, _id="ai_event_card"),
EventHandler(function=set_moderator_handler, _id="add_moderator"),
EventHandler(function=del_moderator_handler, _id="delete_moderator"),
]
)
def main() -> None: # legacy
handlers_setting()
bot.profile.edit_about_sync("Волонтёрский бот, который помогает сотрудникам Сбера нести добро!")
bot.updates.on_updates(do_read_message=True, do_register_commands=True)
if __name__ == "__main__":
main()
--- services/backend/internal/drivers/http/v1/volunteering/participation_card/participation_card_controller.go ---
package participation_card_http
import (
"main/internal/domain/volunteering/participation_card"
"main/pkg"
"net/http"
"github.com/gin-gonic/gin"
)
type ParticipationCardController struct {
query participation_card.ParticipationCardQuery
logger pkg.Logger
}
func NewParticipationCardController(
query participation_card.ParticipationCardQuery,
logger pkg.Logger,
) *ParticipationCardController {
return &ParticipationCardController{
query: query,
logger: logger,
}
}
// GetParticipationList godoc
// @Summary Get participation card list
// @Description Returns paginated list of event participations
// @Tags participations
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param event_id path string true "Event ID"
// @Param request query GetParticipationsRequest false "Participations parameters"
// @Success 200 {object} participation_card.ParticipationCardPage "Participations cards retrieved successfully"
// @Failure 400 {object} map[string]interface{} "Invalid parameters"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /events/{event_id}/participations [get]
func (controller *ParticipationCardController) GetParticipationList(ctx *gin.Context) {
event_id := ctx.Param("event_id")
var req GetParticipationsRequest
if err := ctx.ShouldBindQuery(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid query parameters: " + err.Error()})
return
}
if req.Page < 1 {
req.Page = 1
}
if req.Limit < 1 {
req.Limit = 20
}
list, err := controller.query.GetParticipationList(event_id, req.Page, req.Limit)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, list)
}
--- services/backend/internal/drivers/http/v1/volunteering/participation_card/participation_card_request.go ---
package participation_card_http
type GetParticipationsRequest struct {
Page int `form:"page" binding:"omitempty,min=1"`
Limit int `form:"limit" binding:"omitempty,min=1,max=100"`
}
--- services/backend/internal/drivers/http/v1/volunteering/volunteering_routes.go ---
package volunteering_http
import (
"main/internal/drivers/http/middlewares"
event_card_http "main/internal/drivers/http/v1/volunteering/event_card"
event_organizers_http "main/internal/drivers/http/v1/volunteering/event_organizers"
event_tags_http "main/internal/drivers/http/v1/volunteering/event_tags"
events_http "main/internal/drivers/http/v1/volunteering/events"
gosbs_http "main/internal/drivers/http/v1/volunteering/gosbs"
participation_http "main/internal/drivers/http/v1/volunteering/participation"
participation_card_http "main/internal/drivers/http/v1/volunteering/participation_card"
registry_http "main/internal/drivers/http/v1/volunteering/registry"
request_card_http "main/internal/drivers/http/v1/volunteering/request_card"
requests_http "main/internal/drivers/http/v1/volunteering/requests"
tags_http "main/internal/drivers/http/v1/volunteering/tags"
terbank_moderators_http "main/internal/drivers/http/v1/volunteering/terbank_moderators"
terbanks_http "main/internal/drivers/http/v1/volunteering/terbanks"
volunteer_http "main/internal/drivers/http/v1/volunteering/volunteer"
volunteer_card_http "main/internal/drivers/http/v1/volunteering/volunteer_card"
volunteer_tag_http "main/internal/drivers/http/v1/volunteering/volunteer_tag"
"main/pkg"
"go.uber.org/fx"
)
type VolunteeringRoutes struct {
handler pkg.RequestHandler
authMiddleware *middlewares.AuthMiddleware
testAuthMiddleware *middlewares.TestAuthMiddleware
contextMiddleware *middlewares.ContextMiddleware
roleMiddleware *middlewares.RoleMiddleware
policyMiddleware *middlewares.PolicyMiddleware
tagsController *tags_http.TagsController
volunteerController *volunteer_http.VolunteerController
volunteerTagsController *volunteer_tag_http.VolunteerTagController
volunteerCardController *volunteer_card_http.VolunteerCardController
eventController *events_http.EventsController
eventTagController *event_tags_http.EventTagController
eventCardController *event_card_http.EventCardController
eventOrganizerController *event_organizers_http.EventOrganizerController
participationController *participation_http.ParticipationController
participationCardController *participation_card_http.ParticipationCardController
requestController *requests_http.RequestController
requestCardController *request_card_http.RequestCardController
terbankController *terbanks_http.TerbankController
terbankModeratorController *terbank_moderators_http.TerbankModeratorController
gosbController *gosbs_http.GosbController
registryController *registry_http.RegistryController
}
func NewVolunteeringRoutes(
handler pkg.RequestHandler,
authMiddleware *middlewares.AuthMiddleware,
roleMiddleware *middlewares.RoleMiddleware,
contextMiddleware *middlewares.ContextMiddleware,
policyMiddleware *middlewares.PolicyMiddleware,
testAuthMiddleware *middlewares.TestAuthMiddleware,
volunteerController *volunteer_http.VolunteerController,
tagsController *tags_http.TagsController,
volunteerTagsController *volunteer_tag_http.VolunteerTagController,
volunteerCardController *volunteer_card_http.VolunteerCardController,
eventController *events_http.EventsController,
eventTagController *event_tags_http.EventTagController,
eventCardController *event_card_http.EventCardController,
requestController *requests_http.RequestController,
requestCardController *request_card_http.RequestCardController,
terbankController *terbanks_http.TerbankController,
terbankModeratorController *terbank_moderators_http.TerbankModeratorController,
gosbController *gosbs_http.GosbController,
eventOrganizerController *event_organizers_http.EventOrganizerController,
participationController *participation_http.ParticipationController,
participationCardController *participation_card_http.ParticipationCardController,
registryController *registry_http.RegistryController,
) *VolunteeringRoutes {
return &VolunteeringRoutes{
handler: handler,
authMiddleware: authMiddleware,
contextMiddleware: contextMiddleware,
roleMiddleware: roleMiddleware,
policyMiddleware: policyMiddleware,
testAuthMiddleware: testAuthMiddleware,
volunteerController: volunteerController,
tagsController: tagsController,
volunteerTagsController: volunteerTagsController,
volunteerCardController: volunteerCardController,
eventController: eventController,
eventTagController: eventTagController,
eventCardController: eventCardController,
eventOrganizerController: eventOrganizerController,
requestController: requestController,
requestCardController: requestCardController,
terbankController: terbankController,
terbankModeratorController: terbankModeratorController,
gosbController: gosbController,
participationController: participationController,
participationCardController: participationCardController,
registryController: registryController,
}
}
func (r *VolunteeringRoutes) Setup() {
api := r.handler.Gin.Group("/api/v1")
// Volunteer
volunteerGroup := api.Group("/volunteers").Use(r.testAuthMiddleware.Handler())
{
volunteerGroup.GET("/card", r.volunteerCardController.GetVolunteerCard)
volunteerGroup.POST("/tags", r.volunteerTagsController.SetTag)
volunteerGroup.DELETE("/tags", r.volunteerTagsController.RemoveTag)
volunteerGroup.GET("/", r.volunteerController.GetVolunteer)
volunteerGroup.PUT("/", r.volunteerController.EnsureVolunteerExists)
}
// Tags
tagsGroup := api.Group("tags")
{
tagsGroup.GET("/", r.tagsController.GetAllActualTags)
}
// Events
eventsGroup := api.Group("/events").
Use(r.testAuthMiddleware.Handler()).
Use(r.contextMiddleware.Handler()).
Use(r.roleMiddleware.Handler())
{
eventsGroup.GET("/card/list/mine", r.eventCardController.GetOrganizerEventCardList)
eventsGroup.GET("/card/list/participations", r.eventCardController.GetParticiapatedEventCardList)
eventsGroup.GET("/card/list", r.eventCardController.GetEventCardList)
eventsGroup.GET("/:event_id/card",
r.policyMiddleware.OptionalPermissions("event:viewCode", "event:edit"),
r.eventCardController.GetEventCard)
eventsGroup.POST("/:event_id/tags",
r.policyMiddleware.RequirePermission("event:edit"),
r.eventTagController.SetTag)
eventsGroup.DELETE("/:event_id/tags",
r.policyMiddleware.RequirePermission("event:edit"),
r.eventTagController.RemoveTag)
eventsGroup.POST("/:event_id/organizers",
r.policyMiddleware.RequirePermission("event:handleOrganizers"),
r.eventOrganizerController.AddOrganizer)
eventsGroup.DELETE("/:event_id/organizers",
r.policyMiddleware.RequirePermission("event:handleOrganizers"),
r.eventOrganizerController.RemoveOrganizer)
eventsGroup.PATCH("/:event_id",
r.policyMiddleware.RequirePermission("event:edit"),
r.eventController.UpdateEvent)
eventsGroup.POST("/",
r.policyMiddleware.OptionalPermission("event:autoActivate"),
r.registryController.RegisterEvent)
}
// Requests
requestsGroup := api.Group("/requests").
Use(r.testAuthMiddleware.Handler()).
Use(r.contextMiddleware.Handler()).
Use(r.roleMiddleware.Handler())
{
requestsGroup.GET("/card/list/mine", r.requestCardController.GetVolunteerRequestCardList)
requestsGroup.GET("/card/list", r.requestCardController.GetRequestCardList)
requestsGroup.GET("/:request_id/card",
r.policyMiddleware.RequirePermission("request:view"),
r.policyMiddleware.OptionalPermissions(
"request:handle",
"request:repeat",
"request:edit",
"request:cancel"),
r.requestCardController.GetRequestCard)
requestsGroup.POST("/:request_id/approve",
r.policyMiddleware.RequirePermission("request:handle"),
r.requestController.Approve)
requestsGroup.POST("/:request_id/reject",
r.policyMiddleware.RequirePermission("request:handle"),
r.requestController.Reject)
requestsGroup.POST("/:request_id/repeat",
r.policyMiddleware.RequirePermission("request:repeat"),
r.requestController.Repeat)
requestsGroup.POST("/:request_id/cancel",
r.policyMiddleware.RequirePermission("request:cancel"),
r.requestController.Cancel)
}
// Terbanks
terbanksGroup := api.Group("/terbanks").
Use(r.testAuthMiddleware.Handler()).
Use(r.contextMiddleware.Handler()).
Use(r.roleMiddleware.Handler())
{
terbanksGroup.POST("/:terbank_id/moderators",
r.policyMiddleware.RequirePermission("terbank:editModerators"),
r.terbankModeratorController.SetModerator)
terbanksGroup.DELETE("/:terbank_id/moderators",
r.policyMiddleware.RequirePermission("terbank:editModerators"),
r.terbankModeratorController.RemoveModerator)
terbanksGroup.GET("/:terbank_id/gosbs", r.gosbController.GetAllActualGosbs)
terbanksGroup.GET("/", r.terbankController.GetAllActualTerbanks)
}
// Participations
participationGroup := api.Group("/events/:event_id/participations").
Use(r.testAuthMiddleware.Handler()).
Use(r.contextMiddleware.Handler()).
Use(r.roleMiddleware.Handler())
{
participationGroup.POST("/signup", r.participationController.SignUp)
participationGroup.POST("/confirm", r.participationController.Confirm)
participationGroup.DELETE("/cancel", r.participationController.Cancel)
participationGroup.POST("/hours",
r.policyMiddleware.RequirePermission("participation:addHours"),
r.participationController.AddHours)
participationGroup.GET("/",
r.policyMiddleware.RequirePermission("participation:viewList"),
r.participationCardController.GetParticipationList)
}
}
var Module = fx.Options(
fx.Provide(NewVolunteeringRoutes),
fx.Provide(volunteer_http.NewVolunteerController),
fx.Provide(tags_http.NewTagsController),
fx.Provide(volunteer_tag_http.NewVolunteerTagController),
fx.Provide(volunteer_card_http.NewVolunteerCardController),
fx.Provide(events_http.NewEventController),
fx.Provide(event_tags_http.NewEventTagController),
fx.Provide(event_card_http.NewEventCardController),
fx.Provide(requests_http.NewRequestController),
fx.Provide(request_card_http.NewRequestCardController),
fx.Provide(terbanks_http.NewTerbankController),
fx.Provide(terbank_moderators_http.NewTerbankModeratorController),
fx.Provide(gosbs_http.NewGosbController),
fx.Provide(event_organizers_http.NewEventOrganizerController),
fx.Provide(participation_http.NewParticipationController),
fx.Provide(participation_card_http.NewParticipationCardController),
fx.Provide(registry_http.NewRegistryController),
)
--- services/backend/internal/domain/volunteering/participation_card/participation_card.go ---
package participation_card
import (
"main/internal/domain/volunteering/participations"
"main/internal/domain/volunteering/volunteer_card"
)
type ParticipationCard struct {
Participation *participations.Participation `json:"participation"`
VolunteerCard *volunteer_card.VolunteerCard `json:"volunteer_card"`
}
--- services/backend/internal/domain/volunteering/participation_card/participation_card_page.go ---
package participation_card
type ParticipationCardPage struct {
ParticipationCards []*ParticipationCard `json:"participation_cards"`
PageMeta *PageMeta `json:"page_meta"`
}
--- services/backend/internal/domain/volunteering/participation_card/participation_card_query.go ---
package participation_card
type ParticipationCardQuery interface {
GetParticipationList(eventId string, page int, limit int) (*ParticipationCardPage, error)
}
--- services/backend/internal/domain/volunteering/participation_card/page_meta.go ---
package participation_card
type PageMeta struct {
CurrentPage int `json:"current_page"`
TotalPages int `json:"total_pages"`
}
--- services/backend/internal/adapters/volunteering/participation_card/participation_card_mapper.go ---
package participation_card_adapters
import (
participation_adapters "main/internal/adapters/volunteering/participation"
volunteer_card_adapters "main/internal/adapters/volunteering/volunteer_card"
"main/internal/domain/volunteering/participation_card"
)
func ToDomainParticipationCard(schema *ParticipationCardSchema) *participation_card.ParticipationCard {
card := &participation_card.ParticipationCard{}
if schema.ParticipationSchema != (participation_adapters.ParticipationSchema{}) {
card.Participation = participation_adapters.ToDomainParticipation(&schema.ParticipationSchema)
}
if schema.VolunteerCard != nil {
card.VolunteerCard = volunteer_card_adapters.ToVolunteerCardDomain(schema.VolunteerCard)
}
return card
}
--- services/backend/internal/adapters/volunteering/participation_card/participation_card_query.go ---
package participation_card_adapters
import (
"main/internal/domain/volunteering/participation_card"
"main/pkg"
)
type ParticipationCardQuery struct {
gormDb pkg.GormDB
}
func NewParticipationCardQuery(gormDb pkg.GormDB) participation_card.ParticipationCardQuery {
return &ParticipationCardQuery{
gormDb: gormDb,
}
}
func (p *ParticipationCardQuery) GetParticipationList(eventId string, page int, limit int) (*participation_card.ParticipationCardPage, error) {
query := p.gormDb.
Preload("VolunteerCard.Tags").
Where("event_id = ?", eventId)
var schemas []ParticipationCardSchema
paginationResult, err := pkg.PaginateSimple(query, &schemas, page, limit)
if err != nil {
return nil, err
}
participationCards := make([]*participation_card.ParticipationCard, 0, len(schemas))
for i := range schemas {
participationCards = append(participationCards, ToDomainParticipationCard(&schemas[i]))
}
return &participation_card.ParticipationCardPage{
ParticipationCards: participationCards,
PageMeta: &participation_card.PageMeta{
CurrentPage: paginationResult.Page,
TotalPages: paginationResult.TotalPages,
},
}, nil
}
--- services/backend/internal/adapters/volunteering/participation_card/participation_card_schema.go ---
package participation_card_adapters
import (
participation_adapters "main/internal/adapters/volunteering/participation"
volunteer_card_adapters "main/internal/adapters/volunteering/volunteer_card"
)
type ParticipationCardSchema struct {
participation_adapters.ParticipationSchema
VolunteerCard *volunteer_card_adapters.VolunteerCardSchema `gorm:"foreignKey:MessengerId;references:MessengerId"`
}
func (ParticipationCardSchema) TableName() string {
return "participations"
}
--- services/backend/internal/drivers/http/v1/volunteering/participation/participation_controller.go ---
package participation_http
import (
"errors"
"fmt"
"main/internal/domain/volunteering/events"
"main/internal/domain/volunteering/participations"
"main/pkg"
"net/http"
"github.com/gin-gonic/gin"
)
type ParticipationController struct {
participationService *participations.ParticipationService
logger pkg.Logger
}
func NewParticipationController(
participationService *participations.ParticipationService,
logger pkg.Logger,
) *ParticipationController {
return &ParticipationController{
participationService: participationService,
logger: logger,
}
}
// SignUp godoc
// @Summary Sign up for event
// @Description User signs up for a specific event
// @Tags participations
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param event_id path string true "Event ID"
// @Success 200 {object} map[string]interface{} "Successfully signed up"
// @Failure 409 {object} map[string]interface{} "Conflict"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Failure 404 {object} map[string]interface{} "Event not found"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /events/{event_id}/participations/signup [post]
func (controller *ParticipationController) SignUp(ctx *gin.Context) {
userId := ctx.GetString("user_id")
eventId := ctx.Param("event_id")
if err := controller.participationService.SignUp(userId, eventId); err != nil {
if errors.Is(err, participations.ErrInvalidState) ||
errors.Is(err, participations.ErrAlreadyParticipating) ||
errors.Is(err, events.ErrInvalidParticipantsCount) ||
errors.Is(err, events.ErrArchived) ||
errors.Is(err, events.ErrNotActive) {
ctx.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
if errors.Is(err, events.ErrEventNotFound) {
ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "signed up",
})
}
// Cancel godoc
// @Summary Cancel participation
// @Description User cancels their participation in an event
// @Tags participations
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param event_id path string true "Event ID"
// @Success 200 {object} map[string]interface{} "Successfully canceled"
// @Failure 400 {object} map[string]interface{} "Bad request"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Failure 404 {object} map[string]interface{} "Event not found or participation not found"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /events/{event_id}/participations/cancel [delete]
func (controller *ParticipationController) Cancel(ctx *gin.Context) {
userId := ctx.GetString("user_id")
eventId := ctx.Param("event_id")
if err := controller.participationService.Cancel(userId, eventId); err != nil {
if errors.Is(err, participations.ErrInvalidState) ||
errors.Is(err, events.ErrNotActive) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if errors.Is(err, events.ErrEventNotFound) ||
errors.Is(err, participations.ErrParticipationNotFound) {
ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "canceled",
})
}
// Confirm godoc
// @Summary Confirm participation
// @Description User confirms their participation in an event using activation code
// @Tags participations
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param event_id path string true "Event ID"
// @Param request body ConfirmRequest true "Confirmation code"
// @Success 200 {object} map[string]interface{} "Successfully confirmed"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Failure 400 {object} map[string]interface{} "Bad request"
// @Failure 404 {object} map[string]interface{} "Event not found or participation not found"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /events/{event_id}/participations/confirm [post]
func (controller *ParticipationController) Confirm(ctx *gin.Context) {
userId := ctx.GetString("user_id")
eventId := ctx.Param("event_id")
var req *ConfirmRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
controller.logger.Error(err)
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := controller.participationService.Confirm(userId, eventId, req.Code); err != nil {
if errors.Is(err, participations.ErrInvalidState) ||
errors.Is(err, events.ErrInvalidCode) ||
errors.Is(err, events.ErrNotActive) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if errors.Is(err, events.ErrEventNotFound) ||
errors.Is(err, participations.ErrParticipationNotFound) {
ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "confirmed",
})
}
// AddHours godoc
// @Summary Add hours to participation
// @Description Add volunteer hours to confirmed participation. Hours cannot exceed event limit.
// @Tags participations
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param event_id path string true "Event ID"
// @Param request body AddHoursRequest true "Hours to add"
// @Success 200 {object} map[string]interface{} "Hours added successfully"
// @Failure 400 {object} map[string]interface{} "Invalid request format"
// @Failure 401 {object} map[string]interface{} "Unauthorized"
// @Failure 403 {object} map[string]interface{} "Forbidden"
// @Failure 404 {object} map[string]interface{} "Event or participation not found"
// @Failure 409 {object} map[string]interface{} "Conflict "
// @Failure 422 {object} map[string]interface{} "Unprocessable entity"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /events/{event_id}/participations/hours [post]
func (controller *ParticipationController) AddHours(ctx *gin.Context) {
eventId := ctx.Param("event_id")
var req AddHoursRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
controller.logger.Error(err)
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := controller.participationService.AddHours(req.MessengerId, eventId, req.Hours); err != nil {
if errors.Is(err, participations.ErrInvalidState) {
ctx.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
if errors.Is(err, participations.ErrInvalidHoursCount) {
ctx.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
if errors.Is(err, events.ErrEventNotFound) ||
errors.Is(err, participations.ErrParticipationNotFound) {
ctx.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("add %d hours", req.Hours),
})
}
--- services/backend/internal/drivers/http/v1/volunteering/participation/participation_requests.go ---
package participation_http
type ConfirmRequest struct {
Code string `json:"code"`
}
type AddHoursRequest struct {
Hours int `json:"hours"`
MessengerId string `json:"messenger_id"`
}
--- services/backend/internal/domain/volunteering/participations/participation.go ---
package participations
import "time"
type ParticipiationState string
const (
StateNone ParticipiationState = "none"
StateSignedUp ParticipiationState = "signed_up"
StateConfirmed ParticipiationState = "confirmed"
StateCanceled ParticipiationState = "canceled"
)
type Participation struct {
MessengerId string `json:"messenger_id"`
EventId string `json:"event_id"`
State ParticipiationState `json:"state"`
Hours int `json:"hours"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (p *Participation) Confirm() error {
if p.State != StateSignedUp {
return ErrInvalidState
}
p.State = StateConfirmed
return nil
}
func (p *Participation) Cancel() error {
if p.State != StateSignedUp {
return ErrInvalidState
}
p.State = StateCanceled
return nil
}
func (p *Participation) SetHours(hours int) error {
if p.State != StateConfirmed {
return ErrInvalidState
}
p.Hours += hours
return nil
}
func (p *Participation) AddHours(hours int, maxHours int) error {
if p.State != StateConfirmed {
return ErrInvalidState
}
if p.Hours+hours > maxHours {
return ErrInvalidHoursCount
}
p.Hours += hours
return nil
}
--- services/backend/internal/domain/volunteering/participations/participation_service.go ---
package participations
import (
"main/internal/domain/volunteering/events"
"main/internal/domain/volunteering/volunteers"
)
type ParticipationService struct {
participationRepo ParticipationRepository
eventService *events.EventService
volunteerService *volunteers.VolunteerService
eventCfg events.EventConfig
}
func NewParticipationService(
participationRepo ParticipationRepository,
eventService *events.EventService,
volunteerService *volunteers.VolunteerService,
eventCfgProvider events.EventConfigProvider,
) *ParticipationService {
return &ParticipationService{
participationRepo: participationRepo,
eventService: eventService,
volunteerService: volunteerService,
eventCfg: eventCfgProvider.Provide(),
}
}
func (s *ParticipationService) SignUp(messengerId string, eventId string) error {
count, err := s.participationRepo.Count(eventId)
if err != nil {
return err
}
newCount := count + 1
if err := s.eventService.UpdateParticipantCount(eventId, newCount); err != nil {
return err
}
if err := s.participationRepo.Create(eventId, messengerId); err != nil {
return err
}
return nil
}
func (s *ParticipationService) Cancel(messengerId string, eventId string) error {
count, err := s.participationRepo.Count(eventId)
if err != nil {
return err
}
newCount := count - 1
if err := s.eventService.UpdateParticipantCount(eventId, newCount); err != nil {
return err
}
participation, err := s.participationRepo.Get(eventId, messengerId)
if err != nil {
return err
}
if err := participation.Cancel(); err != nil {
return err
}
if err := s.participationRepo.Delete(eventId, messengerId); err != nil {
return err
}
return nil
}
func (s *ParticipationService) Confirm(messengerId string, eventId string, verificationCode string) error {
result, err := s.eventService.VerificationCode(eventId, verificationCode)
if err != nil {
return err
}
participation, err := s.participationRepo.Get(eventId, messengerId)
if err != nil {
return err
}
if err := participation.Confirm(); err != nil {
return err
}
if err := participation.SetHours(result.Hours); err != nil {
return err
}
if err := s.volunteerService.AccureHours(messengerId, result.Hours); err != nil {
return err
}
if err := s.participationRepo.Update(participation); err != nil {
return err
}
return nil
}
func (s *ParticipationService) AddHours(messengerId string, eventId string, hours int) error {
participation, err := s.participationRepo.Get(eventId, messengerId)
if err != nil {
return err
}
if err := participation.AddHours(hours, s.eventCfg.Hours.Max); err != nil {
return err
}
if err := s.volunteerService.AccureHours(messengerId, hours); err != nil {
return err
}
if err := s.participationRepo.Update(participation); err != nil {
return err
}
return nil
}
--- services/backend/internal/domain/volunteering/participations/participation_repository.go ---
package participations
type ParticipationRepository interface {
Create(eventId string, messengerId string) error
Delete(eventId string, messengerId string) error
Update(participiation *Participation) error
Get(eventId string, messengerId string) (*Participation, error)
Exists(eventId string, messengerId string) (bool, error)
Count(eventId string) (int, error)
}
--- services/backend/internal/adapters/volunteering/participation/participation_repo.go ---
package participation_adapters
import (
"context"
"errors"
participations_domain "main/internal/domain/volunteering/participations"
"main/pkg"
"time"
"gorm.io/gorm"
)
type GormParticipationRepository struct {
db pkg.GormDB
}
func NewGormParticipationRepository(db pkg.GormDB) participations_domain.ParticipationRepository {
return &GormParticipationRepository{
db: db,
}
}
func (g *GormParticipationRepository) Create(eventId string, messengerId string) error {
return g.db.WithTransaction(context.Background(), func(tx *gorm.DB) error {
participation := &ParticipationSchema{
EventId: eventId,
MessengerId: messengerId,
State: string(participations_domain.StateSignedUp),
Hours: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := tx.Create(participation).Error
if err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return participations_domain.ErrAlreadyParticipating
}
return err
}
return nil
})
}
func (g *GormParticipationRepository) Delete(eventId string, messengerId string) error {
return g.db.WithTransaction(context.Background(), func(tx *gorm.DB) error {
result := tx.Where("event_id = ? AND messenger_id = ?", eventId, messengerId).
Delete(&ParticipationSchema{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return participations_domain.ErrParticipationNotFound
}
return nil
})
}
func (g *GormParticipationRepository) Update(participation *participations_domain.Participation) error {
return g.db.WithTransaction(context.Background(), func(tx *gorm.DB) error {
participation.UpdatedAt = time.Now()
schema := ToParticipationSchema(participation)
result := tx.Model(&ParticipationSchema{}).
Where("event_id = ? AND messenger_id = ?", schema.EventId, schema.MessengerId).
Updates(map[string]interface{}{
"state": schema.State,
"participation_hours": schema.Hours,
"updated_at": schema.UpdatedAt,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return participations_domain.ErrParticipationNotFound
}
return nil
})
}
func (g *GormParticipationRepository) Get(eventId string, messengerId string) (*participations_domain.Participation, error) {
var schema ParticipationSchema
err := g.db.DB.
Where("event_id = ? AND messenger_id = ?", eventId, messengerId).
First(&schema).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, participations_domain.ErrParticipationNotFound
}
return nil, err
}
return ToDomainParticipation(&schema), nil
}
func (g *GormParticipationRepository) Exists(eventId string, messengerId string) (bool, error) {
var count int64
err := g.db.DB.
Model(&ParticipationSchema{}).
Where("event_id = ? AND messenger_id = ?", eventId, messengerId).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (g *GormParticipationRepository) Count(eventId string) (int, error) {
var count int64
err := g.db.DB.
Model(&ParticipationSchema{}).
Where("event_id = ?", eventId).
Count(&count).Error
if err != nil {
return 0, err
}
return int(count), nil
}
--- services/backend/internal/adapters/volunteering/participation/participation_schema.go ---
package participation_adapters
import (
participations_domain "main/internal/domain/volunteering/participations"
"time"
)
type ParticipationSchema struct {
MessengerId string `gorm:"column:messenger_id;primaryKey"`
EventId string `gorm:"column:event_id;primaryKey"`
State string `gorm:"column:state;type:varchar(50);not null;default:'signed_up'"`
Hours int `gorm:"column:participation_hours;type:int;default:0"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
}
func (ParticipationSchema) TableName() string {
return "participations"
}
func ToParticipationSchema(participation *participations_domain.Participation) *ParticipationSchema {
return &ParticipationSchema{
MessengerId: participation.MessengerId,
EventId: participation.EventId,
State: string(participation.State),
CreatedAt: participation.CreatedAt,
UpdatedAt: participation.UpdatedAt,
}
}
func ToDomainParticipation(schema *ParticipationSchema) *participations_domain.Participation {
return &participations_domain.Participation{
MessengerId: schema.MessengerId,
EventId: schema.EventId,
State: participations_domain.ParticipiationState(schema.State),
CreatedAt: schema.CreatedAt,
UpdatedAt: schema.UpdatedAt,
}
}
Editor is loading...
Leave a Comment