Untitled
4ae4d
plain_text
02/10/2026 6:40 AM
56.0 KB
9
Indexable
--- src/bot/core/handlers/events_filters.py ---
from dialog_bot_sdk.entities.messaging import UpdateInteractiveMediaEvent
from core.bot_kit.fsm import FSMContext, State, StatesGroup
from core.config import bot
from core.handlers.events_ui import send_city_events_page, send_my_events_page, send_points_page
from core.markups.events_filters import events_filters_menu_keyboard
from core.markups.tags import TagChoice
from core.services.event import EventService
from core.services.tag import TagService
from core.utils import delete_prev_message_by_peer
from core.utils.events_filter import (
format_events_filter_summary,
get_events_filter,
reset_events_filter,
set_events_filter,
)
from core.utils.tags_toggle_ui import TagsToggleCfg, is_done, parse_toggle_key, send_tags_toggle_ui
class EventsFiltersTagsState(StatesGroup):
tags = State()
FILTERS_TAGS_CFG = TagsToggleCfg(
media_id="events_filters_tags_toggle",
toggle_prefix="toggle:",
done_value="done",
)
_CTX_KEY = "events_filters_edit_ctx"
_OFFSET_KEY = "events_filters_edit_offset"
_TAG_IDS_KEY = "events_filters_edit_tags_selected"
def _split_ctx_offset(raw: object) -> tuple[str, int]:
s = str(raw or "").strip()
if "|" not in s:
return (s or "events", 0)
a, b = s.split("|", 1)
ctx = (a or "events").strip()
try:
off = int(str(b).strip())
if off < 0:
off = 0
except Exception:
off = 0
return ctx, off
def _send_list(
*, peer, ctx_name: str, offset: int, context: FSMContext, event_service: EventService
):
# apply/back используют эту маршрутизацию
if ctx_name == "points":
return send_points_page(peer, offset=offset, event_service=event_service, context=context)
if ctx_name == "my_events":
return send_my_events_page(
peer,
offset=offset,
event_service=event_service,
requester_messenger_id=peer.id,
context=context,
)
# events / moderation
return send_city_events_page(
peer,
offset=offset,
event_service=event_service,
ctx_name=ctx_name,
context=context,
)
@bot.di
def events_filters_open_handler(
event: UpdateInteractiveMediaEvent, context: FSMContext, event_service: EventService
):
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
ctx_name, offset = _split_ctx_offset(event.data.value)
flt = get_events_filter(context, ctx_name=ctx_name)
summary = format_events_filter_summary(flt)
bot.messaging.send_message(
peer=event.peer,
text=f"🧰 *Фильтры*\n\n{summary}\n\nВыбери, что изменить:",
interactive_media_groups=events_filters_menu_keyboard(ctx_name=ctx_name, offset=offset),
)
@bot.di
def events_filters_reset_handler(
event: UpdateInteractiveMediaEvent, context: FSMContext, event_service: EventService
):
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
ctx_name, offset = _split_ctx_offset(event.data.value)
reset_events_filter(context, ctx_name=ctx_name)
# остаёмся в меню, чтобы пользователь видел что “сбросилось”
flt = get_events_filter(context, ctx_name=ctx_name)
summary = format_events_filter_summary(flt)
bot.messaging.send_message(
peer=event.peer,
text=f"🧰 *Фильтры*\n\n{summary}\n\nВыбери, что изменить:",
interactive_media_groups=events_filters_menu_keyboard(ctx_name=ctx_name, offset=offset),
)
@bot.di
def events_filters_apply_handler(
event: UpdateInteractiveMediaEvent, context: FSMContext, event_service: EventService
):
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
ctx_name, _offset = _split_ctx_offset(event.data.value)
# безопаснее возвращать на первую страницу (фильтр мог сузить выдачу)
_send_list(
peer=event.peer, ctx_name=ctx_name, offset=0, context=context, event_service=event_service
)
@bot.di
def events_filters_back_handler(
event: UpdateInteractiveMediaEvent, context: FSMContext, event_service: EventService
):
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
ctx_name, offset = _split_ctx_offset(event.data.value)
_send_list(
peer=event.peer,
ctx_name=ctx_name,
offset=offset,
context=context,
event_service=event_service,
)
@bot.di
def events_filters_not_implemented_handler(event: UpdateInteractiveMediaEvent, context: FSMContext):
# handlers для tags/city/date/mode до следующих коммитов
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
ctx_name, offset = _split_ctx_offset(event.data.value)
flt = get_events_filter(context, ctx_name=ctx_name)
summary = format_events_filter_summary(flt)
bot.messaging.send_message(
peer=event.peer,
text=f"⚠️ Пока не реализовано в этом коммите.\n\n{summary}\n\nВыбери, что изменить:",
interactive_media_groups=events_filters_menu_keyboard(ctx_name=ctx_name, offset=offset),
)
def _tag_choices_from_backend(all_tags) -> list[TagChoice]:
out: list[TagChoice] = []
for t in all_tags or []:
tid = getattr(t, "id", None)
name = getattr(t, "name", None)
if tid and name:
out.append(TagChoice(key=str(tid), label=str(name)))
return out
@bot.di
def events_filters_tags_open_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
tag_service: TagService,
):
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
ctx_name, offset = _split_ctx_offset(event.data.value)
flt = get_events_filter(context, ctx_name=ctx_name)
selected_ids = set(flt.tag_ids or [])
all_tags = tag_service.get_all_tags()
choices = _tag_choices_from_backend(all_tags)
# сохраняем "куда вернуться"
context.update_data(
{
_CTX_KEY: ctx_name,
_OFFSET_KEY: int(offset),
_TAG_IDS_KEY: list(selected_ids),
}
)
context.set_state(EventsFiltersTagsState.tags)
send_tags_toggle_ui(
peer=event.peer,
title="🏷️ *Фильтры: теги*\n\nВыбери теги, затем нажми «Готово»:",
choices=choices,
selected_keys=set(selected_ids),
cfg=FILTERS_TAGS_CFG,
done_label="✅ Готово",
)
@bot.di
def events_filters_tags_toggle_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
event_service: EventService,
tag_service: TagService,
):
state = context.get_state()
if not (state == EventsFiltersTagsState.tags or str(state) == str(EventsFiltersTagsState.tags)):
return
delete_prev_message_by_peer(bot, event.peer)
raw = str(event.data.value or "")
data = context.get_data() or {}
ctx_name = str(data.get(_CTX_KEY) or "events")
offset = int(data.get(_OFFSET_KEY) or 0)
selected = set(data.get(_TAG_IDS_KEY) or [])
# DONE -> сохранить filter.tag_ids и вернуться в меню фильтров
if is_done(raw, FILTERS_TAGS_CFG):
flt = get_events_filter(context, ctx_name=ctx_name)
flt.tag_ids = sorted([str(x) for x in selected if str(x).strip()])
all_tags = tag_service.get_all_tags()
id_to_name = {
str(getattr(t, "id", "")).strip(): str(getattr(t, "name", "")).strip()
for t in (all_tags or [])
if str(getattr(t, "id", "")).strip() and str(getattr(t, "name", "")).strip()
}
flt.tag_names = [id_to_name[i] for i in flt.tag_ids if i in id_to_name]
set_events_filter(context, ctx_name=ctx_name, flt=flt)
context.set_state(None)
# чистим только "служебные" ключи экрана выбора тегов
context.update_data({_CTX_KEY: None, _OFFSET_KEY: None, _TAG_IDS_KEY: None})
summary = format_events_filter_summary(get_events_filter(context, ctx_name=ctx_name))
bot.messaging.send_message(
peer=event.peer,
text=f"🧰 *Фильтры*\n\n{summary}\n\nВыбери, что изменить:",
interactive_media_groups=events_filters_menu_keyboard(ctx_name=ctx_name, offset=offset),
)
return
# TOGGLE
key = parse_toggle_key(raw, FILTERS_TAGS_CFG)
if not key:
return
if key in selected:
selected.remove(key)
else:
selected.add(key)
context.update_data({_TAG_IDS_KEY: list(selected)})
all_tags = tag_service.get_all_tags()
choices = _tag_choices_from_backend(all_tags)
send_tags_toggle_ui(
peer=event.peer,
title="🏷️ *Фильтры: теги*\n\nВыбери теги, затем нажми «Готово»:",
choices=choices,
selected_keys=set(selected),
cfg=FILTERS_TAGS_CFG,
done_label="✅ Готово",
)
--- src/bot/core/markups/events_filters.py ---
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder
def events_filters_button_keyboard(*, ctx_name: str, offset: int) -> list[InteractiveMediaGroup]:
return MediaGroupBuilder(
[Button(media_id="events_filters_open", value=f"{ctx_name}|{offset}", label="🧰 Фильтры")]
).build()
def events_filters_menu_keyboard(*, ctx_name: str, offset: int) -> list[InteractiveMediaGroup]:
# value всегда "ctx|offset" чтобы назад возвращаться туда же
v = f"{ctx_name}|{offset}"
top = MediaGroupBuilder(
[
Button(media_id="events_filters_tags", value=v, label="🏷️ Теги"),
Button(media_id="events_filters_city", value=v, label="🏙️ Город"),
]
).build()
mid = MediaGroupBuilder(
[
Button(media_id="events_filters_date", value=v, label="🗓️ Дата/время"),
Button(media_id="events_filters_mode", value=v, label="🎛️ Режим"),
]
).build()
bottom = MediaGroupBuilder(
[
Button(media_id="events_filters_apply", value=v, label="✅ Применить"),
Button(media_id="events_filters_reset", value=v, label="♻️ Сбросить"),
Button(media_id="events_filters_back", value=v, label="⬅️ Назад"),
]
).build()
return top + mid + bottom
--- src/bot/core/utils/events_filter.py ---
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
EventsMode = Literal["all", "participant", "organizer"]
FILTERS_CTX_KEY = "events_filters_v1"
@dataclass(slots=True)
class EventsFilter:
"""
фильтры на стороне бота (храним в fsmcontext.data).
- date_from/date_to сейчас не поддержаны backend (/events/search), поэтому
они участвуют только в UI/summary, но не уходят в query params.
"""
city_id: str | None = None
tag_ids: list[str] = field(default_factory=list)
tag_names: list[str] = field(default_factory=list)
mode: EventsMode = "all"
# UI-only (пока не поддержано backend)
date_from: str | None = None
date_to: str | None = None
@classmethod
def from_dict(cls, raw: Any) -> EventsFilter:
if not isinstance(raw, dict):
return cls()
mode = raw.get("mode") or "all"
if mode not in ("all", "participant", "organizer"):
mode = "all"
city_id = raw.get("city_id")
city_id = str(city_id) if city_id else None
tag_ids_raw = raw.get("tag_ids") or []
if not isinstance(tag_ids_raw, list):
tag_ids_raw = []
tag_ids = [str(x) for x in tag_ids_raw if str(x).strip()]
tag_names_raw = raw.get("tag_names") or []
if not isinstance(tag_names_raw, list):
tag_names_raw = []
tag_names = [str(x) for x in tag_names_raw if str(x).strip()]
date_from = raw.get("date_from")
date_from = str(date_from) if date_from else None
date_to = raw.get("date_to")
date_to = str(date_to) if date_to else None
return cls(
city_id=city_id,
tag_ids=tag_ids,
tag_names=tag_names,
mode=mode, # type: ignore[assignment]
date_from=date_from,
date_to=date_to,
)
def to_dict(self) -> dict[str, Any]:
return {
"city_id": self.city_id,
"tag_ids": list(self.tag_ids or []),
"tag_names": list(self.tag_names or []),
"mode": self.mode,
"date_from": self.date_from,
"date_to": self.date_to,
}
@dataclass(frozen=True, slots=True)
class EventsSearchBase:
"""
базовые параметры конкретного списка (контекст):
- например points всегда only_claimed=True
- my_events (организатор) может фиксировать режим
"""
city_id: str | None
requester_messenger_id: int | None
only_claimed: bool = False
only_organizer: bool = False
only_participant: bool = False
has_report: bool | None = None
tag_ids: list[str] = field(default_factory=list)
# если True — mode из EventsFilter игнорируется
lock_mode: bool = False
def build_events_search_params(
*,
base: EventsSearchBase,
flt: EventsFilter | None,
offset: int,
limit: int,
) -> dict[str, Any]:
"""собираем kwargs для EventService.search_events_with_total(...)"""
f = flt or EventsFilter()
# city/tags: если в фильтре задано — переопределяет базу
city_id = f.city_id if f.city_id else base.city_id
tag_ids = f.tag_ids if f.tag_ids else (base.tag_ids or [])
only_claimed = bool(base.only_claimed)
only_organizer = bool(base.only_organizer)
only_participant = bool(base.only_participant)
# mode: работаем только с organizer/participant
if not base.lock_mode:
if f.mode == "organizer":
only_organizer = True
only_participant = False
elif f.mode == "participant":
only_participant = True
only_organizer = False
else:
# all
# оставляем только то, что задано базой
pass
params: dict[str, Any] = {
"offset": int(offset),
"limit": int(limit),
"only_claimed": bool(only_claimed),
"only_organizer": bool(only_organizer),
"only_participant": bool(only_participant),
}
if city_id is not None:
params["city_id"] = str(city_id)
if base.requester_messenger_id is not None:
params["requester_messenger_id"] = int(base.requester_messenger_id)
if base.has_report is not None:
params["has_report"] = bool(base.has_report)
if tag_ids:
params["tag_ids"] = list(tag_ids)
return params
def format_events_filter_summary(flt: EventsFilter | None) -> str:
f = flt or EventsFilter()
parts: list[str] = []
if f.tag_names:
names = [str(x).strip() for x in f.tag_names if str(x).strip()]
parts.append("теги: " + ", ".join(names))
elif f.tag_ids:
parts.append(f"теги: {len(f.tag_ids)}")
if f.mode == "participant":
parts.append("режим: участвую")
elif f.mode == "organizer":
parts.append("режим: организую")
if f.city_id:
parts.append(f"город: {str(f.city_id)[:8]}…")
if f.date_from or f.date_to:
a = f.date_from or "…"
b = f.date_to or "…"
parts.append(f"даты: {a} — {b}")
if not parts:
return "Фильтр\nбез доп. условий"
return "Фильтр\n" + "\n".join(parts)
def get_events_filter(context, *, ctx_name: str) -> EventsFilter:
data = (context.get_data() or {}).get(FILTERS_CTX_KEY) or {}
if not isinstance(data, dict):
return EventsFilter()
return EventsFilter.from_dict(data.get(str(ctx_name)))
def set_events_filter(context, *, ctx_name: str, flt: EventsFilter) -> None:
raw = (context.get_data() or {}).get(FILTERS_CTX_KEY) or {}
data = dict(raw) if isinstance(raw, dict) else {}
data[str(ctx_name)] = flt.to_dict()
context.update_data({FILTERS_CTX_KEY: data})
def reset_events_filter(context, *, ctx_name: str) -> None:
raw = (context.get_data() or {}).get(FILTERS_CTX_KEY) or {}
data = dict(raw) if isinstance(raw, dict) else {}
data.pop(str(ctx_name), None)
context.update_data({FILTERS_CTX_KEY: data})
def clear_context_keep_events_filters(context) -> None:
"""
нужен для мест, где раньше делали context.clear(),
но теперь фильтры должны переживать пагинацию
"""
data = context.get_data() or {}
keep = data.get(FILTERS_CTX_KEY)
context.clear()
if keep is not None:
context.update_data({FILTERS_CTX_KEY: keep})
--- src/bot/core/services/city.py ---
from logging import getLogger
from core.clients.city import CityClient
from core.schemas.city import CitySchema
from core.services.base import BaseService
from core.services.registry import BaseServiceRegistry
logger = getLogger(__name__)
class CityService(BaseService):
def __init__(self, registry: BaseServiceRegistry, client: CityClient):
super().__init__(registry)
self.client = client
def get_all(self, offset: int = 0, limit: int = 10) -> list[CitySchema]:
resp = self.client.get_all(offset, limit)
if resp.status_code != 200:
raise Exception(f"Error receiving cities: {resp.text}")
return [CitySchema(name=item.get("name"), id=item.get("id")) for item in resp.json()]
def get_by_name(self, name: str):
resp = self.client.get_by_name(name)
if resp.status_code == 404:
return None
if resp.status_code != 200:
raise Exception(f"Error receiving city: {resp.text}")
body = resp.json()
return CitySchema(name=body.get("name"), id=body.get("id"))
def get_by_id(self, id: str):
resp = self.client.get_by_id(id)
if resp.status_code == 404:
return None
if resp.status_code != 200:
raise Exception(f"Error receiving cities: {resp.text}")
body = resp.json()
return CitySchema(name=body.get("name"), id=body.get("id"))
--- src/bot/core/services/__init__.py ---
from .city import CityService
from .event import EventService
from .report import ReportService
from .tag import TagService
from .user import UserService
__all__ = [
"UserService",
"EventService",
"CityService",
"TagService",
"ReportService",
]
--- src/bot/core/schemas/city.py ---
from pydantic import BaseModel
class CitySchema(BaseModel):
name: str
id: str
--- src/bot/core/handlers/events_ui.py ---
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import 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.event import (
event_actions_keyboard,
events_select_keyboard,
format_event_details,
my_events_pagination_keyboard,
user_events_pagination_keyboard,
)
from core.markups.events_filters import events_filters_button_keyboard
from core.markups.inline import all_events_pagination_keyboard
from core.markups.points import points_events_pagination_keyboard, points_menu_keyboard
from core.schemas.event import EventSchema
from core.schemas.user import UserSchema
from core.services.event import EventService
from core.services.report import ReportService
from core.utils.events_filter import format_events_filter_summary, get_events_filter
DEFAULT_CITY_UUID = "be9da3ba-a0e4-42f9-9ae2-9875c8625083" # SPB
ADMIN_CAN_MANAGE_OTHERS = True # имеет ли администратор доступ ко всем мероприятиям?
@dataclass(frozen=True)
class EventUIContext:
name: str
# list
list_open_media_id: str
pagination_builder: Callable[..., list[InteractiveMediaGroup]]
extra_groups_builder: Callable[[], list[InteractiveMediaGroup]] | None
# card
back_value: str
back_label: str
enter_code_media_id: str
sign_up_media_id: str
sign_out_media_id: str
_UI: dict[str, EventUIContext] = {
# дом волонтёра -> мероприятия
"events": EventUIContext(
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",
),
# дом волонтёра -> посмотреть по uuid
"volunteer_home": EventUIContext(
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(
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_menu_sign_up",
sign_out_media_id="event_menu_sign_out",
),
# меню баллов -> мероприятия с начисленными баллами
"points": EventUIContext(
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(
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_menu_sign_up",
sign_out_media_id="event_menu_sign_out",
),
# дом модерации -> мои мероприятия
"my_events": EventUIContext(
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_menu_sign_up",
sign_out_media_id="event_menu_sign_out",
),
}
def get_ui(ctx_name: str) -> EventUIContext:
return _UI.get(ctx_name, _UI["events"])
def build_back_keyboard(ctx_name: str) -> list[InteractiveMediaGroup]:
ui = get_ui(ctx_name)
return MediaGroupBuilder(
[Button(media_id="leave", value=ui.back_value, label=ui.back_label)]
).build()
def split_event_value(raw: Any) -> tuple[str, str | None]:
"""
ожидаем:
- "<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 can_manage_event(ev: EventSchema, user: UserSchema | None) -> bool:
if getattr(user, "role", None) == "admin" and ADMIN_CAN_MANAGE_OTHERS:
return True
uid = getattr(user, "id", None)
if not uid:
return False
return bool(getattr(ev, "creator_id", None)) and (str(ev.creator_id) == str(uid))
def can_view_code(ev: EventSchema, user: UserSchema | None) -> bool:
# кто может управлять - тот видит код
return can_manage_event(ev, user)
def _send_events_list_screen(
peer,
*,
ui: EventUIContext,
ctx_name: str,
offset: int,
limit: int,
page_events: list[EventSchema],
total: int | None,
has_prev: bool,
has_next: bool,
note: str | None = None,
header: str | None = None,
empty_text: str | None = None,
empty_groups: list[InteractiveMediaGroup] | None = None,
filter_summary: str | None = None,
):
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 page_events:
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
total_str = str(total) if total is not None else "?"
text = (
prefix
+ header_part
+ f"Показаны: {offset + 1} — {offset + len(page_events)} из {total_str}\n\n"
+ "Выбери мероприятие:"
+ filter_part
)
im: list[InteractiveMediaGroup] = []
im += events_select_keyboard(
page_events, 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_city_events_page(
peer,
*,
offset: int,
event_service: EventService,
ctx_name: str,
city_id: str = DEFAULT_CITY_UUID,
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_events, has_prev, has_next, total = fetch_events_page(
event_service=event_service,
city_id=city_id,
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_events=page_events,
total=total,
has_prev=has_prev,
has_next=has_next,
note=note,
filter_summary=summary,
)
def send_points_page(
peer,
*,
offset: int,
event_service: EventService,
context: FSMContext | None = None,
):
limit = EVENTS_PAGE_SIZE
points_total = "?"
flt = get_events_filter(context, ctx_name="points") if context is not None else None
summary = format_events_filter_summary(flt) if context is not None else None
page_events, has_prev, has_next, total = fetch_events_page(
event_service=event_service,
city_id=DEFAULT_CITY_UUID,
offset=offset,
limit=limit,
requester_messenger_id=peer.id,
only_claimed=True,
filters=flt,
)
_send_events_list_screen(
peer,
ui=get_ui("points"),
ctx_name="points",
offset=offset,
limit=limit,
page_events=page_events,
total=total,
has_prev=has_prev,
has_next=has_next,
header=f"🏆 Твои баллы: {points_total}\n\n",
empty_text=(
"Пока нет мероприятий, за которые уже начислены баллы.\n\n"
"Как получить баллы:\n"
"1) Запишитесь на мероприятие\n"
"2) После участия введи код\n"
),
empty_groups=points_menu_keyboard(),
filter_summary=summary,
)
def send_my_events_page(
peer,
*,
offset: int,
event_service: EventService,
requester_messenger_id: int,
city_id: str = DEFAULT_CITY_UUID,
note: str | None = None,
context: FSMContext | None = None,
):
ui = get_ui("my_events")
limit = EVENTS_PAGE_SIZE
flt = get_events_filter(context, ctx_name="my_events") if context is not None else None
summary = format_events_filter_summary(flt) if context is not None else None
page_events, has_prev, has_next, total = fetch_events_page(
event_service=event_service,
city_id=city_id,
offset=offset,
limit=limit,
requester_messenger_id=requester_messenger_id,
only_organizer=True,
filters=flt,
lock_mode=True,
)
_send_events_list_screen(
peer,
ui=ui,
ctx_name="my_events",
offset=offset,
limit=limit,
page_events=page_events,
total=total,
has_prev=has_prev,
has_next=has_next,
note=note,
header="📋 Мои мероприятия\n\n",
empty_text="Пока у тебя нет созданных мероприятий.",
filter_summary=summary,
)
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)
ev = event_service.get_event_by_id(event_id)
if ev 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
part = event_service.get_participation_status(ev.id, peer.id)
is_participant = part is not None
can_manage = can_manage_event(ev, user)
show_code = can_view_code(ev, user)
role = getattr(user, "role", None) if user else None
can_access_reports = (role in ("admin", "moderator")) or can_manage
report_exists: bool | None = None
if can_access_reports and report_service is not None:
report_exists = report_service.report_exists(
event_id=str(ev.id),
requester_messenger_id=peer.id,
)
else:
report_exists = None
can_create_report = bool(can_manage and (report_exists is False))
can_view_report = bool(can_access_reports and (report_exists is True))
base = format_event_details(
ev,
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(
ev.id,
is_participant=is_participant,
can_manage=can_manage,
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,
),
)
def build_back_to_event_keyboard(event_id: str, ctx_name: str) -> list[InteractiveMediaGroup]:
ui = get_ui(ctx_name)
return MediaGroupBuilder(
[
Button(
media_id=ui.list_open_media_id,
value=f"{event_id}|{ui.name}",
label="⬅️ К мероприятию",
)
]
).build()
--- src/bot/main.py ---
from core.bot_kit.router import Router
from core.config import bot
from core.handlers import (
admin_menu_handler,
ai_assistant_handler,
all_events_handler,
all_events_open_handler,
all_events_page_handler,
create_event_start_handler,
event_menu_delete_handler,
event_menu_edit_field_handler,
event_menu_edit_handler,
event_menu_enter_code_start_handler,
event_menu_sign_out_handler,
event_menu_sign_up_handler,
event_participants_open_handler,
event_tags_toggle_handler,
event_user_enter_code_start_handler,
event_user_sign_out_handler,
event_user_sign_up_handler,
event_view_by_id_start,
events_filters_apply_handler,
events_filters_back_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,
main_menu_handler,
moderation_menu_handler,
my_events_handler,
my_events_open_handler,
my_events_page_handler,
my_profile_handler,
noop_handler,
points_event_open_handler,
points_leaderboard_handler,
points_menu_handler,
points_page_handler,
points_rt,
profile_tags_toggle_handler,
report_create_start_handler,
report_rt,
report_view_handler,
reports_export_handler,
start_handler,
update_user_interests_start,
update_user_menu_handler,
update_user_name_start,
update_user_region_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:
router = Router()
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.subscribe(bot)
bot.messaging.command_handler(
[
CommandHandler(
function=start_handler,
command="start",
description="Расскажу о себе",
),
CommandHandler(
function=reports_export_handler,
command="reports_export",
description="Выгрузка отчётов",
),
]
)
bot.messaging.event_handler(
[
EventHandler(function=volunteer_home_handler, value="volunteer_home"),
EventHandler(function=main_menu_handler, value="main_menu"),
EventHandler(function=ai_assistant_handler, value="ai_assistant"),
EventHandler(function=my_profile_handler, value="my_profile"),
EventHandler(function=update_user_menu_handler, value="update_profile_menu"),
EventHandler(function=update_user_name_start, _id="update_user_name"),
EventHandler(function=update_user_region_start, _id="update_user_region"),
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, value="points"),
EventHandler(function=points_leaderboard_handler, value="points_leaderboard"),
EventHandler(function=moderation_menu_handler, value="moderation"),
EventHandler(function=reports_export_handler, value="download_reports_list"),
EventHandler(function=all_events_handler, value="all_events"),
EventHandler(function=events_menu_handler, value="events"),
EventHandler(function=user_events_page_handler, _id="user_events_prev"),
EventHandler(function=user_events_page_handler, _id="user_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, value="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, value="create_event"),
EventHandler(function=event_view_by_id_start, value="update_event"),
EventHandler(function=admin_menu_handler, value="administration"),
EventHandler(function=event_menu_sign_up_handler, _id="event_menu_sign_up"),
EventHandler(function=event_menu_sign_out_handler, _id="event_menu_sign_out"),
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_date"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_time"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_location"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_tags"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_creator_id"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_points"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_description"),
EventHandler(function=event_menu_edit_field_handler, _id="event_menu_edit_bonus_code"),
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_not_implemented_handler, _id="events_filters_mode"
),
# TODO почистить
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"),
]
)
def main() -> None:
handlers_setting()
bot.profile.edit_about_sync("Волонтёрский бот, который помогает сотрудникам Сбера нести добро!")
bot.updates.on_updates(do_read_message=True, do_register_commands=True)
if __name__ == "__main__":
main()
Editor is loading...
Leave a Comment