Untitled
4ae4d
plain_text
a month ago
52 kB
4
Indexable
f=services/bot/core/clients/request.py
f=services/bot/core/services/requests.py
f=services/bot/core/handlers/request.py
f=services/bot/core/handlers/__init__.py
f=services/bot/main.py
f=services/bot/core/markups/inline.py
f=services/bot/core/markups/request.py
--- services/bot/core/clients/request.py ---
from requests import Response
from core.clients.base import BaseApiClient
class RequestClient(BaseApiClient):
"""Клиент для работы с API заявок"""
def get_request_cards(
self,
requester_messenger_id: int,
page: int = 1,
limit: int = 10,
status: str | None = None,
) -> Response:
"""Получает список заявок"""
url = "/requests/card/list"
params = {
"page": page,
"limit": limit,
}
if status:
params["status"] = status
return self.get(
url,
headers={"Authorization": str(requester_messenger_id)},
params=params,
)
def get_request(self, request_id: str, requester_messenger_id: int) -> Response:
"""Получает заявку по ID"""
url = f"/requests/{request_id}/card"
params = {"messenger_id": requester_messenger_id}
return self.get(
url,
headers={"Authorization": str(requester_messenger_id)},
params=params,
)
def approve_request(self, request_id: str, requester_messenger_id: int) -> Response:
"""Одобряет заявку"""
url = f"/requests/{request_id}/approve"
return self.post(url, headers={"Authorization": str(requester_messenger_id)})
def reject_request(self, request_id: str, requester_messenger_id: int, reason: str) -> Response:
"""Отклоняет заявку с указанием причины"""
url = f"/requests/{request_id}/reject"
json_data = {
"reject_reason": reason,
}
return self.post(
url, headers={"Authorization": str(requester_messenger_id)}, json=json_data
)
--- services/bot/core/services/requests.py ---
from core.clients import RequestClient
from core.schemas import RequestCardSchema, RequestCardsPageSchema
from core.services.base import BaseService
from core.services.registry import BaseServiceRegistry
from core.utils import logger
class RequestService(BaseService):
"""Сервис для работы с заявками"""
def __init__(self, registry: BaseServiceRegistry, client: RequestClient):
super().__init__(registry)
self.client = client
def get_request_cards_page(
self,
*,
requester_messenger_id: int,
page: int = 1,
limit: int = 10,
status: str | None = None,
) -> RequestCardsPageSchema:
"""Получает страницу с заявками"""
resp = self.client.get_request_cards(
requester_messenger_id=requester_messenger_id,
page=page,
limit=limit,
status=status,
)
if resp.status_code != 200:
logger.error(f"[RequestService] get_request_cards_page failed: {resp.status_code}")
return RequestCardsPageSchema(request_cards=[], current_page=1, total_pages=1)
try:
return RequestCardsPageSchema.from_response(resp.json())
except Exception as e:
logger.error(f"[RequestService] Failed to parse response: {e}")
return RequestCardsPageSchema(request_cards=[], current_page=1, total_pages=1)
def get_request_by_id(
self, request_id: str, requester_messenger_id: int
) -> RequestCardSchema | None:
"""Получает заявку по ID"""
resp = self.client.get_request(request_id, requester_messenger_id)
if resp.status_code == 404:
return None
if resp.status_code != 200:
logger.error(f"[RequestService] get_request_by_id failed: {resp.status_code}")
return None
try:
return RequestCardSchema.from_dict(resp.json())
except Exception as e:
logger.error(f"[RequestService] Failed to parse request: {e}")
return None
def approve_request(self, request_id: str, requester_messenger_id: int) -> bool:
"""Одобряет заявку"""
resp = self.client.approve_request(request_id, requester_messenger_id)
return resp.status_code == 200
def reject_request(self, request_id: str, requester_messenger_id: int, reason: str) -> bool:
"""Отклоняет заявку с причиной"""
resp = self.client.reject_request(request_id, requester_messenger_id, reason)
return resp.status_code == 200
--- services/bot/core/handlers/request.py ---
from __future__ import annotations
from dataclasses import dataclass
from dialog_bot_sdk.entities.messaging import (
InteractiveMediaGroup,
UpdateInteractiveMediaEvent,
UpdateMessage,
)
from dialog_bot_sdk.interactive_media import Button, MediaGroupBuilder
from core.bot_kit.fsm import FSMContext, State, StatesGroup
from core.bot_kit.router import Router
from core.config import bot
from core.markups import (
format_request_details,
requests_pagination_keyboard,
requests_select_keyboard,
)
from core.schemas import RequestCardsPageSchema
from core.services import RequestService
from core.utils import delete_prev_message, delete_prev_message_by_peer
requests_rt = Router()
class RequestRejectState(StatesGroup):
wait_reason = State()
@dataclass(frozen=True)
class RequestUIContext:
name: str
back_value: str
back_label: str
_UI: dict[str, RequestUIContext] = {
# модерация -> заявки
"requests": RequestUIContext(
name="requests",
back_value="requests",
back_label="⬅️ Назад",
),
"my_requests": RequestUIContext(
name="my_requests",
back_value="my_requests",
back_label="⬅️ Назад",
),
}
def get_ui(ctx_name: str) -> RequestUIContext:
return _UI.get(ctx_name, _UI["requests"])
def build_context_back(*, label: str = "❌ Отмена", value: str = "", ctx: str = ""):
ui = get_ui(ctx) # noqa: F841
def build_back_keyboard(
*, value: str = "", label: str = "❌ Отмена", media_id: str = "request_open"
) -> list[InteractiveMediaGroup]:
return MediaGroupBuilder(
[Button(media_id=media_id, value=value, label=label)],
).build() # type: ignore
def _send_requests_list_screen(
peer,
*,
ctx_name: str,
offset: int,
limit: int,
page_data: RequestCardsPageSchema,
header: str | None = None,
empty_text: str | None = None,
filter_summary: str | None = None,
note: str | None = None,
):
"""Отправляет список заявок"""
request_cards = page_data.request_cards
has_prev = page_data.has_prev
has_next = page_data.has_next
header_part = header if header is not None else "📋 Заявки\n\n"
header_part = f"{note}\n\n{header_part}" if note else header_part
filter_part = f"\n\n{filter_summary}" if filter_summary else ""
if not request_cards:
text = (
header_part
+ (
empty_text
or "⚠️ Заявок не найдено.\n\nПопробуй изменить критерии или вернись назад."
)
+ filter_part
)
im: list[InteractiveMediaGroup] = []
im += requests_pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
leave_value=ctx_name,
leave_label="⬅️ Назад",
)
bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)
return
text = (
header_part
+ f"Страница: {page_data.current_page} / {page_data.total_pages}\n\n"
+ "Выбери заявку для просмотра:"
+ filter_part
)
im: list[InteractiveMediaGroup] = []
im += requests_select_keyboard(
request_cards,
open_media_id="request_open",
start_index=offset + 1,
per_row=1,
)
im += requests_pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
leave_value=ctx_name,
leave_label="🛡️ В меню модерации",
)
bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)
def send_request_cards_page(
peer,
*,
offset: int,
request_service: RequestService,
ctx_name: str,
note: str | None = None,
context: FSMContext | None = None,
):
"""Отправляет страницу с заявками"""
limit = 10
page = offset // limit + 1
page_data = request_service.get_request_cards_page(
requester_messenger_id=peer.id,
page=page,
limit=limit,
)
_send_requests_list_screen(
peer,
ctx_name=ctx_name,
offset=offset,
limit=limit,
page_data=page_data,
note=note,
)
@bot.di
def all_requests_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик для просмотра всех заявок"""
delete_prev_message_by_peer(bot, event.peer)
send_request_cards_page(
event.peer,
offset=0,
request_service=request_service,
ctx_name="requests",
context=context,
)
@bot.di
def requests_page_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик пагинации заявок"""
delete_prev_message_by_peer(bot, event.peer)
try:
offset = int(event.data.value)
if offset < 0:
offset = 0
except Exception:
offset = 0
send_request_cards_page(
event.peer,
offset=offset,
request_service=request_service,
ctx_name="requests",
context=context,
)
@bot.di
def request_open_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик открытия конкретной заявки"""
delete_prev_message_by_peer(bot, event.peer)
context.set_state(None)
request_id = str(event.data.value or "").strip()
# Получаем детали заявки
request_card = request_service.get_request_by_id(request_id, event.peer.id)
if request_card is None:
bot.messaging.send_message(
peer=event.peer,
text="⚠️ Заявка не найдена.",
interactive_media_groups=build_back_keyboard(media_id="requests"),
)
return
# Формируем детальное сообщение
text = format_request_details(request_card)
# Клавиатура для действий с заявкой
keyboard = _build_request_actions_keyboard(request_card, ctx_name="requests")
bot.messaging.send_message(
peer=event.peer,
text=text,
interactive_media_groups=keyboard,
)
def _build_request_actions_keyboard(request_card, ctx_name: str):
"""Строит клавиатуру для действий с заявкой"""
actions = []
req = request_card.request
actions.append(
Button(
media_id="leave",
value=ctx_name,
label="⬅️ Назад",
)
)
if req.status.lower() == "pending":
actions.append(
Button(
media_id="request_approve",
value=req.id,
label="✅ Одобрить",
)
)
actions.append(
Button(
media_id="request_reject",
value=req.id,
label="❌ Отклонить",
)
)
return MediaGroupBuilder(actions).build()
@bot.di
def requests_prev_page_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик предыдущей страницы заявок"""
delete_prev_message_by_peer(bot, event.peer)
try:
offset = int(event.data.value)
if offset < 0:
offset = 0
except Exception:
offset = 0
send_request_cards_page(
event.peer,
offset=offset,
request_service=request_service,
ctx_name="moderation",
context=context,
)
@bot.di
def requests_next_page_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик следующей страницы заявок"""
delete_prev_message_by_peer(bot, event.peer)
try:
offset = int(event.data.value)
if offset < 0:
offset = 0
except Exception:
offset = 0
send_request_cards_page(
event.peer,
offset=offset,
request_service=request_service,
ctx_name="moderation",
context=context,
)
@bot.di
def request_approve_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик одобрения заявки"""
delete_prev_message_by_peer(bot, event.peer)
request_id = str(event.data.value or "").strip()
success = request_service.approve_request(request_id, event.peer.id)
if success:
note = "✅ Заявка успешно одобрена!"
else:
note = "⚠️ Не удалось одобрить заявку. Попробуй позже."
send_request_cards_page(
event.peer,
offset=0,
request_service=request_service,
ctx_name="moderation",
context=context,
note=note,
)
@bot.di
def request_reject_handler(
event: UpdateInteractiveMediaEvent,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик отклонения заявки - запрашивает причину"""
delete_prev_message_by_peer(bot, event.peer)
request_id = str(event.data.value or "").strip()
context.update_data({"reject_request_id": request_id})
context.set_state(RequestRejectState.wait_reason)
bot.messaging.send_message(
peer=event.peer,
text="💬 Укажите причину отклонения заявки:",
interactive_media_groups=build_back_keyboard(value=request_id, media_id="request_open"),
)
@requests_rt.message(state=RequestRejectState.wait_reason)
@bot.di
def request_reject_reason_handler(
message: UpdateMessage,
context: FSMContext,
request_service: RequestService,
):
"""Обработчик получения причины отклонения заявки"""
delete_prev_message(bot, message)
data = context.get_data()
request_id = data.get("reject_request_id")
reason = message.message.text_message.text.strip()
note = ""
if not reason:
note = "⚠️ Причина не может быть пустой."
else:
success = request_service.reject_request(request_id, message.peer.id, reason)
note = "ℹ️ Заявка успешно отклонена." if success else "⚠️ Не удалось отклонить заявку."
context.set_state(None)
context.update_data({"reject_request_id": None})
send_request_cards_page(
message.peer,
offset=0,
request_service=request_service,
ctx_name="moderation",
note=note,
)
--- services/bot/core/handlers/__init__.py ---
from .admin import admin_menu_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 import (
all_events_handler,
all_events_open_handler,
all_events_page_handler,
create_event_start_handler,
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_menu_sign_out_handler,
event_menu_sign_up_handler,
event_tags_toggle_handler,
event_view_by_id_start,
events_rt,
gosb_approved,
gosb_update_selected,
moderation_menu_handler,
my_events_handler,
my_events_open_handler,
my_events_page_handler,
project_input_dash_handler,
tb_approved,
tb_reselect_handler,
tb_update_selected,
)
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,
update_user_menu_handler,
update_user_name_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,
request_approve_handler,
request_open_handler,
request_reject_handler,
requests_next_page_handler,
requests_page_handler,
requests_prev_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
__all__ = [
"admin_menu_handler",
"ai_assistant_handler",
"ai_rt",
"all_events_handler",
"all_events_open_handler",
"all_events_page_handler",
"all_requests_handler",
"create_event_start_handler",
"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_menu_sign_out_handler",
"event_menu_sign_up_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",
"event_view_by_id_start",
"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",
"my_events_handler",
"my_events_open_handler",
"my_events_page_handler",
"my_profile_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_open_handler",
"request_reject_handler",
"requests_next_page_handler",
"requests_page_handler",
"requests_prev_page_handler",
"requests_rt",
"start_handler",
"tb_approved",
"tb_reselect_handler",
"tb_update_selected",
"update_user_interests_start",
"update_user_menu_handler",
"update_user_name_start",
"update_user_rt",
"user_events_open_handler",
"user_events_page_handler",
# Routers
"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,
ai_assistant_handler,
ai_rt,
all_events_handler,
all_events_open_handler,
all_events_page_handler,
all_requests_handler,
create_event_start_handler,
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_menu_sign_out_handler,
event_menu_sign_up_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,
event_view_by_id_start,
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,
my_events_handler,
my_events_open_handler,
my_events_page_handler,
my_profile_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_open_handler,
request_reject_handler,
requests_next_page_handler,
requests_page_handler,
requests_prev_page_handler,
requests_rt,
start_handler,
tb_approved,
tb_reselect_handler,
tb_update_selected,
update_user_interests_start,
update_user_menu_handler,
update_user_name_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(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.subscribe(bot)
bot.messaging.command_handler(
[
CommandHandler(
function=start_handler,
command="start",
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, 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_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=part_events_menu_handler, value="part_events"),
# TODO почистить
EventHandler(function=project_input_dash_handler, value="project_input_dash"),
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, 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_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, value="requests"),
EventHandler(function=requests_page_handler, _id="requests_prev"),
EventHandler(function=requests_page_handler, _id="requests_next"),
EventHandler(function=request_open_handler, _id="request_open"),
EventHandler(function=request_approve_handler, _id="request_approve"),
EventHandler(function=request_reject_handler, _id="request_reject"),
# TODO почистить
EventHandler(function=project_input_dash_handler, value="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=requests_prev_page_handler, _id="requests_prev"),
EventHandler(function=requests_next_page_handler, _id="requests_next"),
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"),
]
)
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/bot/core/markups/inline.py ---
from dialog_bot_sdk.entities.messaging import InteractiveMediaStyle
from dialog_bot_sdk.interactive_media import Button, MediaGroupBuilder
from core.markups.pagination import pagination_keyboard
# Основные клавиатуры
def main_keyboard_admin(): # legacy
group_builder_1 = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"🏡 Дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2",
"ai_assistant",
"✨ ИИ Волонтёр",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button(
"3",
"moderation",
"🛡️ Модерация",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"4",
"administration",
"⚙️ Администрация",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
return group_builder_1.merge([group_builder_2])
def main_keyboard_moder(): # legacy
group_builder_1 = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"🏡 Дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2",
"ai_assistant",
"✨ ИИ Волонтёр",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button(
"3", "moderation", "Модерация", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
)
]
)
return group_builder_1.merge([group_builder_2])
def main_keyboard_user(): # legacy
builder = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"🏡 Дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2",
"ai_assistant",
"✨ ИИ Волонтёр",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
return builder.build()
def back_to_main_menu_keyboard(): # legacy
builder = MediaGroupBuilder(
[Button("20", "main_menu", "⬅️ В меню", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT)]
)
return builder.build()
# Дом волонтёра
def volunteer_home_keyboard(): # legacy
group_builder_1 = MediaGroupBuilder(
[
Button(
"1",
"my_profile",
"👤 Мой профиль",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"2", "events", "🌐 Мероприятия", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button("3", "points", "🏆 Баллы", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT),
Button(
"4",
"part_events",
"🎉 Мои мероприятия",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
group_builder_3 = MediaGroupBuilder(
[
Button(
"5", "main_menu", "⬅️ В меню", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
),
]
)
return group_builder_1.merge([group_builder_2, group_builder_3])
def back_to_moderation_and_skip_keyboard():
builder = MediaGroupBuilder(
[
Button(
"2",
"project_input_dash",
"➡️ Пропустить",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button(
"1",
"moderation",
"⬅️ В меню модерации",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
return builder.build()
def back_to_moderation_keyboard(): # legacy
builder = MediaGroupBuilder(
[
Button(
"1",
"moderation",
"⬅️ В меню модерации",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
)
return builder.build()
def back_to_volunteer_home_keyboard(): # legacy
builder = MediaGroupBuilder(
[
Button(
"1",
"volunteer_home",
"⬅️ В дом волонтёра",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
)
]
)
return builder.build()
# Модерация
def moderation_menu_keyboard(): # legacy
group_builder_1 = MediaGroupBuilder(
[
Button("1", "create_event", "🗓️ Создать мероприятие"),
Button("4", "my_events", "📋 Мои мероприятия"),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button(
"5",
"all_events",
"🌐 Все мероприятия",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
Button("7", "download_reports_list", "📥 Выгрузить отчёты"),
]
)
group_builder_3 = MediaGroupBuilder(
[
Button("2", "requests", "📩 Заявки"),
Button("3", "my_requests", "✉️ Мои заявки"),
]
)
group_builder_4 = MediaGroupBuilder(
[
Button("10", "main_menu", "⬅️ В меню"),
]
)
return group_builder_1.merge([group_builder_2, group_builder_3, group_builder_4])
# Администрация
def admin_menu_keyboard(): # legacy
group_builder_1 = MediaGroupBuilder(
[
Button("1", "add_admin", "Добавить администратора"),
Button("2", "delete_admin", "Убрать администратора"),
]
)
group_builder_2 = MediaGroupBuilder(
[
Button("3", "add_moderator", "Добавить модератора"),
Button("4", "delete_moderator", "Убрать модератора"),
]
)
group_builder_3 = MediaGroupBuilder(
[
Button("5", "ban_user", "Забанить пользователя"),
Button("6", "unban_user", "Разбанить пользователя"),
]
)
group_builder_4 = MediaGroupBuilder([Button("7", "main_menu", "⬅️ В меню")])
return group_builder_1.merge([group_builder_2, group_builder_3, group_builder_4])
def reports_export_keyboard(): # legacy
"""
value везде = 'download_reports_list', обработчик один
выбор различаем по media_id
"""
group_1 = MediaGroupBuilder(
[
Button("rep_day", "download_reports_list", "📅 Последний день"),
Button("rep_week", "download_reports_list", "📆 Неделя"),
]
)
group_2 = MediaGroupBuilder(
[
Button("rep_month", "download_reports_list", "🗓️ Месяц"),
Button("rep_all", "download_reports_list", "🌐 Всё время"),
]
)
group_3 = MediaGroupBuilder(
[
Button("rep_from", "download_reports_list", "📌 От даты до сегодня"),
Button("rep_range", "download_reports_list", "🗓️ От даты до даты"),
]
)
group_4 = MediaGroupBuilder(
[
Button("rep_back", "moderation", "⬅️ В меню модерации"),
]
)
return group_1.merge([group_2, group_3, group_4])
def back_to_reports_export_keyboard(): # legacy
return MediaGroupBuilder(
[
Button(
"9",
"download_reports_list",
"⬅️ В меню выгрузки отчётов",
InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
),
]
).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_value="moderation",
leave_label="🛡️ В меню модерации",
style=InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
keep_layout=True,
add_view_by_uuid_button=True,
view_by_uuid_value="moderation",
)
def all_requests_pagination_keyboard(
*, offset: int, limit: int, has_prev: bool, has_next: bool
): # untested
return pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
prev_media_id="all_requests_prev",
next_media_id="all_requests_next",
leave_value="moderation",
leave_label="🛡️ В меню модерации",
style=InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
keep_layout=True,
add_view_by_uuid_button=True,
view_by_uuid_value="moderation",
)
--- services/bot/core/markups/request.py ---
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder
from core.markups.event import format_event_details
from core.markups.pagination import pagination_keyboard
from core.schemas import RequestCardSchema
from core.utils import format_datetime, wrap_text
def _get_status_emoji(status: str) -> str:
status_emojis = {
"pending": "⏳", # на рассмотрении
"approved": "✅", # одобрено
"rejected": "❌", # отклонено
}
return status_emojis.get(status.lower(), "⚪")
def _get_status_label(status: str) -> str:
status_label = {
"pending": "на рассмотрении",
"approved": "одобрено",
"rejected": "отлконено",
}
return status_label.get(status.lower(), "-")
def _get_request_type_label(request_type: str) -> str:
types = {
"event": "Мероприятие вне проекта",
"project_event": "Мероприятие в рамках",
"project": "Проект",
}
return types.get(request_type.lower(), request_type.capitalize())
def _request_button_label(request_card: RequestCardSchema) -> str:
"""Формирует подпись для кнопки заявки (короткий вариант)"""
req = request_card.request
resource = request_card.resource
request_type = _get_request_type_label(req.request_type)
resource_name = "—"
if resource.event:
resource_name = resource.event.title or "—"
status_emoji = _get_status_emoji(req.status)
status_label = _get_status_label(req.status)
return f"{request_type}\nНазвание: {resource_name}\nСтатус: {status_label} {status_emoji}"
def requests_select_keyboard(
requests_cards: list[RequestCardSchema],
*,
open_media_id: str,
start_index: int = 1,
per_row: int = 1,
) -> list[InteractiveMediaGroup]:
"""
Клавиатура для выбора заявки из списка (аналог events_select_keyboard)
"""
groups: list[InteractiveMediaGroup] = []
row: list[Button] = []
idx = start_index
for request_card in requests_cards:
label = _request_button_label(request_card)
row.append(Button(media_id=open_media_id, value=str(request_card.request.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 requests_pagination_keyboard(
*,
offset: int,
limit: int,
has_prev: bool,
has_next: bool,
prev_media_id: str = "requests_prev",
next_media_id: str = "requests_next",
leave_value: str = "moderation",
leave_label: str = "🛡️ В меню модерации",
keep_layout: bool = True,
) -> list[InteractiveMediaGroup]:
"""
Клавиатура пагинации для списка заявок
"""
return pagination_keyboard(
offset=offset,
limit=limit,
has_prev=has_prev,
has_next=has_next,
prev_media_id=prev_media_id,
next_media_id=next_media_id,
leave_value=leave_value,
leave_label=leave_label,
keep_layout=keep_layout,
)
def requests_filters_keyboard(
ctx_name: str = "moderation",
offset: int = 0,
) -> list[InteractiveMediaGroup]:
"""
Клавиатура фильтров для заявок
"""
return MediaGroupBuilder(
[Button(media_id="requests_filters_open", value=f"{ctx_name}|{offset}", label="🔍 Фильтры")]
).build()
def format_request_details(request_card: RequestCardSchema) -> str:
"""Форматирует детальную информацию о заявке"""
def _format_reject_reason(reject_reason: str) -> str:
reject_reason = wrap_text(reject_reason, 40, "")
return "❌ Причина отказа:\n```plain\n" + reject_reason + "\n```"
req = request_card.request
resource = request_card.resource
status_emoji = _get_status_emoji(req.status)
status_label = _get_status_label(req.status)
lines = [
f"📋 *Заявка* {status_emoji}",
"",
f"🆔 ID: `{req.id}`",
f"📝 Тип: {_get_request_type_label(req.request_type)}",
f"👤 Отправитель: `{req.messenger_id}`",
f"📅 Создана: {format_datetime(req.created_at)}",
f"🔄 Обновлена: {format_datetime(req.updated_at)}",
f"📊 Статус: {status_label} {status_emoji}",
"",
]
if req.status.lower() == "rejected" and req.reject_reason:
lines.append(_format_reject_reason(req.reject_reason))
if req.request_type == "event" or req.request_type == "project_event":
event_formated = format_event_details(
resource,
show_code=True,
show_organizer=True,
make_frame=True,
wrap=True,
)
lines.append(event_formated)
return "\n".join(lines)
def request_actions_keyboard(
request_id: str,
*,
status: str,
back_value: str,
back_label: str,
approve_media_id: str = "request_approve",
reject_media_id: str = "request_reject",
) -> list[InteractiveMediaGroup]:
"""
Клавиатура для действий с заявкой (аналог event_actions_keyboard)
"""
actions = []
if status.lower() == "pending":
actions.append(Button(media_id=approve_media_id, value=request_id, label="✅ Одобрить"))
actions.append(Button(media_id=reject_media_id, value=request_id, label="❌ Отклонить"))
actions.append(Button(media_id="leave", value=back_value, label=back_label))
return MediaGroupBuilder(actions).build()
--- services/bot/core/schemas/request.py ---
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from core.schemas.event import EventCardSchema
from core.utils import parse_datetime_value, require_datetime
class RequestSchema(BaseModel):
"""Схема заявки"""
id: str
messenger_id: str
request_type: str
reason: str
source_id: str
status: str
reject_reason: str
reviewed_by: str
reviewed_at: datetime | None
created_at: datetime
updated_at: datetime
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RequestSchema":
reviewed_at = data.get("reviewed_at")
if reviewed_at and reviewed_at.startswith("0001-01-01"):
reviewed_at = None
return cls(
id=str(data.get("id", "")),
messenger_id=str(data.get("messenger_id", "")),
request_type=str(data.get("request_type", "")),
reason=str(data.get("reason", "")),
source_id=str(data.get("source_id", "")),
status=str(data.get("status", "")),
reject_reason=str(data.get("reject_reason", "")),
reviewed_by=str(data.get("reviewed_by", "")),
reviewed_at=parse_datetime_value(reviewed_at) if reviewed_at else None,
created_at=require_datetime(data.get("created_at"), "participation.created_at"),
updated_at=require_datetime(data.get("updated_at"), "participation.updated_at"),
)
class RequestCardSchema(BaseModel):
"""Схема карточки заявки"""
request: RequestSchema
resource: EventCardSchema # | ProjectCardSchema
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RequestCardSchema":
request_data = data.get("request")
if not isinstance(request_data, dict):
raise ValueError("Invalid request contract: request_data is required")
request = RequestSchema.from_dict(request_data)
resource_data = data.get("resource")
if not isinstance(resource_data, dict):
raise ValueError("Invalid request contract: resource_data is required")
resource = EventCardSchema.from_dict(resource_data)
# TODO: Сделать обработку карточки проекта здесь
return cls(
request=request,
resource=resource,
)
class RequestCardsPageSchema(BaseModel):
"""Схема страницы с карточками заявок"""
request_cards: list[RequestCardSchema]
current_page: int
total_pages: int
@classmethod
def from_response(cls, response_data: dict[str, Any]) -> "RequestCardsPageSchema":
"""Создаёт схему из ответа бэкенда"""
cards_data = response_data.get("request_cards") or []
if not isinstance(cards_data, list):
raise ValueError("Invalid request card contract: cards_data must be list")
page_meta = response_data.get("page_meta") or {}
if not isinstance(page_meta, dict):
raise ValueError("Invalid request card contract: page_meta must be dict")
request_cards: list[RequestCardSchema] = []
for card_data in cards_data:
if not isinstance(card_data, dict):
raise ValueError("Invalid request card contract: item must be dict")
request_cards.append(RequestCardSchema.from_dict(card_data))
return cls(
request_cards=request_cards,
current_page=int(page_meta.get("current_page", 1)),
total_pages=int(page_meta.get("total_pages", 1)),
)
@property
def has_prev(self) -> bool:
return self.current_page > 1
@property
def has_next(self) -> bool:
return self.current_page < self.total_pages
Editor is loading...
Leave a Comment