mirror of
https://github.com/snoups/remnashop.git
synced 2026-04-18 08:53:57 +00:00
Merge branch 'pr/68' into 0.8.0
This commit is contained in:
@@ -27,6 +27,19 @@ def get_default_notifications() -> dict[str, bool]:
|
||||
return {**system_keys, **user_keys}
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class SystemNotifyRouteDto:
|
||||
"""Routing target for a specific system notification type."""
|
||||
|
||||
chat_id: int
|
||||
thread_id: Optional[int] = None
|
||||
|
||||
@property
|
||||
def effective_thread_id(self) -> Optional[int]:
|
||||
"""thread_id=1 is the General topic — treat as no-topic."""
|
||||
return None if self.thread_id == 1 else self.thread_id
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class AccessSettingsDto(TrackableMixin):
|
||||
mode: AccessMode = AccessMode.PUBLIC
|
||||
@@ -67,6 +80,7 @@ class RequirementSettingsDto(TrackableMixin):
|
||||
@dataclass(kw_only=True)
|
||||
class NotificationsSettingsDto(TrackableMixin):
|
||||
settings: dict[str, bool] = field(default_factory=get_default_notifications)
|
||||
routes: dict[str, SystemNotifyRouteDto] = field(default_factory=dict)
|
||||
|
||||
def is_enabled(self, ntf_type: NotificationType) -> bool:
|
||||
return self.settings.get(ntf_type, True)
|
||||
@@ -76,6 +90,19 @@ class NotificationsSettingsDto(TrackableMixin):
|
||||
new_settings[ntf_type] = not self.is_enabled(ntf_type)
|
||||
self.settings = new_settings
|
||||
|
||||
def get_route(self, ntf_type: NotificationType) -> Optional[SystemNotifyRouteDto]:
|
||||
return self.routes.get(str(ntf_type))
|
||||
|
||||
def set_route(self, ntf_type: NotificationType, chat_id: int, thread_id: Optional[int]) -> None:
|
||||
new_routes = self.routes.copy()
|
||||
new_routes[str(ntf_type)] = SystemNotifyRouteDto(chat_id=chat_id, thread_id=thread_id)
|
||||
self.routes = new_routes
|
||||
|
||||
def clear_route(self, ntf_type: NotificationType) -> None:
|
||||
new_routes = self.routes.copy()
|
||||
new_routes.pop(str(ntf_type), None)
|
||||
self.routes = new_routes
|
||||
|
||||
@property
|
||||
def system(self) -> list[tuple[str, bool]]:
|
||||
return [
|
||||
|
||||
@@ -29,6 +29,8 @@ from src.application.dto import (
|
||||
TempUserDto,
|
||||
UserDto,
|
||||
)
|
||||
from src.application.dto.message_payload import MediaDescriptorDto
|
||||
from src.application.dto.settings import SystemNotifyRouteDto
|
||||
from src.application.events import ErrorEvent, RemnawaveVersionWarningEvent, SystemEvent
|
||||
from src.application.events.base import UserEvent
|
||||
from src.application.events.system import RemnashopWelcomeEvent
|
||||
@@ -101,7 +103,11 @@ class NotificationService(Notifier):
|
||||
logger.info(f"Notification for '{event.notification_type}' is disabled, skipping")
|
||||
return
|
||||
|
||||
await self.notify_admins(event.as_payload(), roles=[Role.OWNER, Role.DEV])
|
||||
await self._notify_system(
|
||||
event.as_payload(),
|
||||
roles=[Role.OWNER, Role.DEV],
|
||||
notification_type=event.notification_type,
|
||||
)
|
||||
|
||||
@on_event(UserEvent)
|
||||
async def on_user_event(self, event: UserEvent) -> None:
|
||||
@@ -123,7 +129,7 @@ class NotificationService(Notifier):
|
||||
logger.info(f"Notification for '{event.notification_type}' is disabled, skipping")
|
||||
return
|
||||
|
||||
await self.notify_admins(event.as_payload())
|
||||
await self._notify_system(event.as_payload(), notification_type=event.notification_type)
|
||||
|
||||
@on_event(ErrorEvent)
|
||||
async def on_error_event(self, event: ErrorEvent) -> None:
|
||||
@@ -146,11 +152,70 @@ class NotificationService(Notifier):
|
||||
filename=f"error_{event.event_id}.txt",
|
||||
)
|
||||
|
||||
await self.notify_admins(
|
||||
await self._notify_system(
|
||||
event.as_payload(media, error_type, error_message),
|
||||
roles=[Role.OWNER, Role.DEV],
|
||||
notification_type=event.notification_type,
|
||||
)
|
||||
|
||||
async def _notify_system(
|
||||
self,
|
||||
payload: MessagePayloadDto,
|
||||
roles: list[Role] = [Role.OWNER, Role.DEV, Role.ADMIN],
|
||||
notification_type: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Route system notification: to group/topic if configured in settings, else to admin chats."""
|
||||
route = None
|
||||
if notification_type:
|
||||
settings: SettingsDto = await self.settings_dao.get()
|
||||
route = settings.notifications.get_route(notification_type)
|
||||
|
||||
if route:
|
||||
await self._send_to_topic(payload, route)
|
||||
else:
|
||||
await self.notify_admins(payload, roles=roles)
|
||||
|
||||
async def _send_to_topic(self, payload: MessagePayloadDto, route: SystemNotifyRouteDto) -> None:
|
||||
"""Send a system notification to the configured group chat / topic."""
|
||||
chat_id = route.chat_id
|
||||
thread_id = route.effective_thread_id
|
||||
|
||||
locale = self.config.default_locale
|
||||
text = self._get_translated_text(
|
||||
locale=locale,
|
||||
i18n_key=payload.i18n_key,
|
||||
i18n_kwargs=payload.i18n_kwargs,
|
||||
)
|
||||
|
||||
try:
|
||||
if payload.is_text:
|
||||
await self.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=text,
|
||||
message_thread_id=thread_id,
|
||||
disable_web_page_preview=True,
|
||||
disable_notification=payload.disable_notification,
|
||||
)
|
||||
elif payload.media:
|
||||
method = self._get_media_method(payload)
|
||||
if not method:
|
||||
logger.warning(f"Unknown media type for topic payload '{payload}'")
|
||||
return
|
||||
media = self._build_media(payload.media)
|
||||
await method(
|
||||
chat_id,
|
||||
media,
|
||||
caption=text,
|
||||
message_thread_id=thread_id,
|
||||
disable_notification=payload.disable_notification,
|
||||
)
|
||||
else:
|
||||
logger.error("Topic payload must contain text or media")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send system notification to topic {chat_id}/{thread_id}: {e}")
|
||||
await self._send_topic_config_error(chat_id, thread_id, str(e))
|
||||
|
||||
async def delete_notification(self, chat_id: int, message_id: int) -> None:
|
||||
try:
|
||||
await self.bot.delete_message(chat_id=chat_id, message_id=message_id)
|
||||
@@ -253,6 +318,27 @@ class NotificationService(Notifier):
|
||||
logger.exception(f"Failed to send notification to '{user.telegram_id}': {e}")
|
||||
raise
|
||||
|
||||
async def _send_topic_config_error(
|
||||
self, chat_id: Optional[int], thread_id: Optional[int], reason: str
|
||||
) -> None:
|
||||
"""Fallback: notify owner in personal chat when topic delivery fails."""
|
||||
target = f"chat_id={chat_id}" + (f", thread_id={thread_id}" if thread_id else "")
|
||||
error_text = (
|
||||
f"⚠️ <b>System notification delivery failed</b>\n\n"
|
||||
f"<b>Target:</b> <code>{target}</code>\n"
|
||||
f"<b>Reason:</b> <code>{reason[:300]}</code>\n\n"
|
||||
f"Check the notification route in the dashboard and make sure "
|
||||
f"the bot is a member of the group with send-message permissions."
|
||||
)
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
chat_id=self.config.bot.owner_id,
|
||||
text=error_text,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to deliver topic config error to owner: {e}")
|
||||
|
||||
def _get_media_method(self, payload: MessagePayloadDto) -> Optional[Callable[..., Any]]:
|
||||
if payload.is_photo:
|
||||
return self.bot.send_photo
|
||||
|
||||
@@ -4,7 +4,7 @@ from src.application.common import Interactor
|
||||
|
||||
from .commands.access import ChangeAccessMode, TogglePayments, ToggleRegistration
|
||||
from .commands.currency import UpdateDefaultCurrency
|
||||
from .commands.notifications import ToggleNotification
|
||||
from .commands.notifications import ClearSystemNotifyRoute, ToggleNotification, UpdateSystemNotifyRoute
|
||||
from .commands.referral import (
|
||||
ToggleReferralSystem,
|
||||
UpdateReferralAccrualStrategy,
|
||||
@@ -21,6 +21,7 @@ from .commands.requirements import (
|
||||
|
||||
SETTINGS_USE_CASES: Final[tuple[type[Interactor], ...]] = (
|
||||
ChangeAccessMode,
|
||||
ClearSystemNotifyRoute,
|
||||
ToggleConditionRequirement,
|
||||
ToggleNotification,
|
||||
TogglePayments,
|
||||
@@ -34,4 +35,5 @@ SETTINGS_USE_CASES: Final[tuple[type[Interactor], ...]] = (
|
||||
UpdateReferralRewardType,
|
||||
UpdateRulesRequirement,
|
||||
UpdateDefaultCurrency,
|
||||
UpdateSystemNotifyRoute,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
@@ -29,3 +30,53 @@ class ToggleNotification(Interactor[NotificationType, Optional[SettingsDto]]):
|
||||
|
||||
logger.info(f"{actor.log} Toggled notification '{notification_type}'")
|
||||
return updated
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpdateSystemNotifyRouteDto:
|
||||
notification_type: NotificationType
|
||||
chat_id: int
|
||||
thread_id: Optional[int]
|
||||
|
||||
|
||||
class UpdateSystemNotifyRoute(Interactor[UpdateSystemNotifyRouteDto, Optional[SettingsDto]]):
|
||||
required_permission = Permission.SETTINGS_NOTIFICATIONS
|
||||
|
||||
def __init__(self, uow: UnitOfWork, settings_dao: SettingsDao) -> None:
|
||||
self.uow = uow
|
||||
self.settings_dao = settings_dao
|
||||
|
||||
async def _execute(
|
||||
self, actor: UserDto, data: UpdateSystemNotifyRouteDto
|
||||
) -> Optional[SettingsDto]:
|
||||
async with self.uow:
|
||||
settings = await self.settings_dao.get()
|
||||
settings.notifications.set_route(data.notification_type, data.chat_id, data.thread_id)
|
||||
updated = await self.settings_dao.update(settings)
|
||||
await self.uow.commit()
|
||||
|
||||
logger.info(
|
||||
f"{actor.log} Updated notify route for '{data.notification_type}': "
|
||||
f"chat={data.chat_id}, thread={data.thread_id}"
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
class ClearSystemNotifyRoute(Interactor[NotificationType, Optional[SettingsDto]]):
|
||||
required_permission = Permission.SETTINGS_NOTIFICATIONS
|
||||
|
||||
def __init__(self, uow: UnitOfWork, settings_dao: SettingsDao) -> None:
|
||||
self.uow = uow
|
||||
self.settings_dao = settings_dao
|
||||
|
||||
async def _execute(
|
||||
self, actor: UserDto, notification_type: NotificationType
|
||||
) -> Optional[SettingsDto]:
|
||||
async with self.uow:
|
||||
settings = await self.settings_dao.get()
|
||||
settings.notifications.clear_route(notification_type)
|
||||
updated = await self.settings_dao.update(settings)
|
||||
await self.uow.commit()
|
||||
|
||||
logger.info(f"{actor.log} Cleared notify route for '{notification_type}'")
|
||||
return updated
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Add system notification routes to settings.notifications JSONB
|
||||
|
||||
Revision ID: 0019
|
||||
Revises: 0018
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0019"
|
||||
down_revision: Union[str, None] = "0018"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add 'routes' key to existing notifications JSONB if not already present.
|
||||
# The column stores {"settings": {...}, "routes": {...}} after this migration.
|
||||
op.execute("""
|
||||
UPDATE settings
|
||||
SET notifications = notifications || '{"routes": {}}'::jsonb
|
||||
WHERE NOT (notifications ? 'routes')
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("""
|
||||
UPDATE settings
|
||||
SET notifications = notifications - 'routes'
|
||||
""")
|
||||
@@ -1,5 +1,7 @@
|
||||
from aiogram_dialog import Dialog, Window
|
||||
from aiogram_dialog.widgets.kbd import Column, Row, Select, Start, SwitchTo
|
||||
from aiogram_dialog.widgets.kbd import Button, Column, Row, Select, Start, SwitchTo
|
||||
from aiogram_dialog.widgets.input import TextInput
|
||||
from aiogram_dialog.widgets.text import Format
|
||||
from magic_filter import F
|
||||
|
||||
from src.core.enums import BannerName, SystemNotificationType, UserNotificationType
|
||||
@@ -7,8 +9,18 @@ from src.telegram.keyboards import main_menu_button
|
||||
from src.telegram.states import DashboardRemnashop, RemnashopNotifications
|
||||
from src.telegram.widgets import Banner, I18nFormat, IgnoreUpdate
|
||||
|
||||
from .getters import system_types_getter, user_types_getter
|
||||
from .handlers import on_system_type_select, on_user_type_select
|
||||
from .getters import system_route_getter, system_type_getter, system_types_getter, user_types_getter
|
||||
from .handlers import (
|
||||
on_chat_id_entered,
|
||||
on_invalid_int,
|
||||
on_route_clear,
|
||||
on_system_route_edit_chat,
|
||||
on_system_route_edit_thread,
|
||||
on_system_type_select,
|
||||
on_system_type_toggle,
|
||||
on_thread_id_entered,
|
||||
on_user_type_select,
|
||||
)
|
||||
|
||||
notifications = Window(
|
||||
Banner(BannerName.DASHBOARD),
|
||||
@@ -68,20 +80,16 @@ user = Window(
|
||||
getter=user_types_getter,
|
||||
)
|
||||
|
||||
# Список системных типов — клик открывает подменю типа
|
||||
system = Window(
|
||||
Banner(BannerName.DASHBOARD),
|
||||
I18nFormat("msg-notifications-system"),
|
||||
Column(
|
||||
Select(
|
||||
text=I18nFormat(
|
||||
"btn-notifications.system-choice",
|
||||
type=F["item"]["type"],
|
||||
enabled=F["item"]["enabled"],
|
||||
),
|
||||
text=Format("{item[label]}"),
|
||||
id="type_select",
|
||||
item_id_getter=lambda item: item["type"],
|
||||
items="types",
|
||||
type_factory=SystemNotificationType,
|
||||
on_click=on_system_type_select,
|
||||
),
|
||||
),
|
||||
@@ -97,8 +105,131 @@ system = Window(
|
||||
getter=system_types_getter,
|
||||
)
|
||||
|
||||
# Подменю конкретного типа: тоггл + маршрут
|
||||
system_type = Window(
|
||||
Banner(BannerName.DASHBOARD),
|
||||
Format(
|
||||
"<b>{ntf_type}</b>\n\n"
|
||||
"Статус: {status}\n"
|
||||
"Маршрут: {route_info}"
|
||||
),
|
||||
Row(
|
||||
Button(
|
||||
text=Format("{toggle_btn}"),
|
||||
id="toggle",
|
||||
on_click=on_system_type_toggle,
|
||||
),
|
||||
SwitchTo(
|
||||
text=Format("📡 Маршрут"),
|
||||
id="route",
|
||||
state=RemnashopNotifications.SYSTEM_ROUTE,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
SwitchTo(
|
||||
text=I18nFormat("btn-back.general"),
|
||||
id="back",
|
||||
state=RemnashopNotifications.SYSTEM,
|
||||
),
|
||||
),
|
||||
IgnoreUpdate(),
|
||||
state=RemnashopNotifications.SYSTEM_TYPE,
|
||||
getter=system_type_getter,
|
||||
)
|
||||
|
||||
# Настройка маршрута
|
||||
system_route = Window(
|
||||
Banner(BannerName.DASHBOARD),
|
||||
Format(
|
||||
"📡 <b>Маршрут: {ntf_type}</b>\n\n"
|
||||
"Chat ID: <code>{chat_id}</code>\n"
|
||||
"Thread ID: <code>{thread_id}</code>\n\n"
|
||||
"<i>Если маршрут не задан — уведомление идёт в личку админам</i>"
|
||||
),
|
||||
Row(
|
||||
Button(
|
||||
text=Format("✏️ Chat ID"),
|
||||
id="edit_chat",
|
||||
on_click=on_system_route_edit_chat,
|
||||
),
|
||||
Button(
|
||||
text=Format("✏️ Thread ID"),
|
||||
id="edit_thread",
|
||||
on_click=on_system_route_edit_thread,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Button(
|
||||
text=Format("🗑 Удалить маршрут"),
|
||||
id="clear_route",
|
||||
on_click=on_route_clear,
|
||||
when=F["has_route"],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
SwitchTo(
|
||||
text=I18nFormat("btn-back.general"),
|
||||
id="back",
|
||||
state=RemnashopNotifications.SYSTEM_TYPE,
|
||||
),
|
||||
),
|
||||
IgnoreUpdate(),
|
||||
state=RemnashopNotifications.SYSTEM_ROUTE,
|
||||
getter=system_route_getter,
|
||||
)
|
||||
|
||||
system_route_chat_id = Window(
|
||||
Banner(BannerName.DASHBOARD),
|
||||
Format(
|
||||
"Введи <b>Chat ID</b> группы\n"
|
||||
"(отрицательное число, например <code>-1001234567890</code>):"
|
||||
),
|
||||
TextInput(
|
||||
id="chat_id_input",
|
||||
type_factory=int,
|
||||
on_success=on_chat_id_entered,
|
||||
on_error=on_invalid_int,
|
||||
),
|
||||
Row(
|
||||
SwitchTo(
|
||||
text=I18nFormat("btn-back.general"),
|
||||
id="back",
|
||||
state=RemnashopNotifications.SYSTEM_ROUTE,
|
||||
),
|
||||
),
|
||||
state=RemnashopNotifications.SYSTEM_ROUTE_CHAT_ID,
|
||||
getter=system_route_getter,
|
||||
)
|
||||
|
||||
system_route_thread_id = Window(
|
||||
Banner(BannerName.DASHBOARD),
|
||||
Format(
|
||||
"Введи <b>Thread ID</b> топика\n"
|
||||
"(число, введи <code>0</code> чтобы сбросить):"
|
||||
),
|
||||
TextInput(
|
||||
id="thread_id_input",
|
||||
type_factory=int,
|
||||
on_success=on_thread_id_entered,
|
||||
on_error=on_invalid_int,
|
||||
),
|
||||
Row(
|
||||
SwitchTo(
|
||||
text=I18nFormat("btn-back.general"),
|
||||
id="back",
|
||||
state=RemnashopNotifications.SYSTEM_ROUTE,
|
||||
),
|
||||
),
|
||||
state=RemnashopNotifications.SYSTEM_ROUTE_THREAD_ID,
|
||||
getter=system_route_getter,
|
||||
)
|
||||
|
||||
router = Dialog(
|
||||
notifications,
|
||||
user,
|
||||
system,
|
||||
system_type,
|
||||
system_route,
|
||||
system_route_chat_id,
|
||||
system_route_thread_id,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from src.application.common.dao import SettingsDao
|
||||
from src.application.common import TranslatorRunner
|
||||
from src.core.enums import SystemNotificationType
|
||||
|
||||
|
||||
@@ -31,17 +32,82 @@ async def user_types_getter(
|
||||
async def system_types_getter(
|
||||
dialog_manager: DialogManager,
|
||||
settings_dao: FromDishka[SettingsDao],
|
||||
i18n: FromDishka[TranslatorRunner],
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
settings = await settings_dao.get()
|
||||
|
||||
types = [
|
||||
{
|
||||
types = []
|
||||
for field, value in settings.notifications.system:
|
||||
if field == SystemNotificationType.SYSTEM:
|
||||
continue
|
||||
has_route = settings.notifications.get_route(field) is not None
|
||||
label = i18n.get("btn-notifications.system-choice", type=field.upper(), enabled=value)
|
||||
if has_route:
|
||||
label = label + " 📡"
|
||||
types.append({
|
||||
"type": field.upper(),
|
||||
"enabled": value,
|
||||
}
|
||||
for field, value in settings.notifications.system
|
||||
if field != SystemNotificationType.SYSTEM
|
||||
]
|
||||
"has_route": has_route,
|
||||
"label": label,
|
||||
})
|
||||
|
||||
return {"types": types}
|
||||
|
||||
|
||||
@inject
|
||||
async def system_type_getter(
|
||||
dialog_manager: DialogManager,
|
||||
settings_dao: FromDishka[SettingsDao],
|
||||
i18n: FromDishka[TranslatorRunner],
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
settings = await settings_dao.get()
|
||||
ntf_type = dialog_manager.dialog_data.get("route_ntf_type", "")
|
||||
enabled = settings.notifications.is_enabled(ntf_type)
|
||||
route = settings.notifications.get_route(ntf_type)
|
||||
|
||||
ntf_label = i18n.get(
|
||||
"btn-notifications.system-choice",
|
||||
type=ntf_type.upper(),
|
||||
enabled=enabled,
|
||||
)
|
||||
|
||||
route_info = "не задан (личка админам)"
|
||||
if route:
|
||||
route_info = f"chat={route.chat_id}"
|
||||
if route.thread_id:
|
||||
route_info += f", thread={route.thread_id}"
|
||||
|
||||
return {
|
||||
"ntf_type": ntf_label,
|
||||
"status": "🟢 Включено" if enabled else "🔴 Выключено",
|
||||
"toggle_btn": "🔴 Выключить" if enabled else "🟢 Включить",
|
||||
"route_info": route_info,
|
||||
}
|
||||
|
||||
|
||||
@inject
|
||||
async def system_route_getter(
|
||||
dialog_manager: DialogManager,
|
||||
settings_dao: FromDishka[SettingsDao],
|
||||
i18n: FromDishka[TranslatorRunner],
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
settings = await settings_dao.get()
|
||||
ntf_type = dialog_manager.dialog_data.get("route_ntf_type", "")
|
||||
route = settings.notifications.get_route(ntf_type)
|
||||
enabled = settings.notifications.is_enabled(ntf_type)
|
||||
|
||||
ntf_label = i18n.get(
|
||||
"btn-notifications.system-choice",
|
||||
type=ntf_type.upper(),
|
||||
enabled=enabled,
|
||||
)
|
||||
|
||||
return {
|
||||
"ntf_type": ntf_label,
|
||||
"has_route": route is not None,
|
||||
"chat_id": route.chat_id if route else "—",
|
||||
"thread_id": route.thread_id if (route and route.thread_id) else "—",
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
from aiogram.types import CallbackQuery
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram_dialog import DialogManager
|
||||
from aiogram_dialog.widgets.kbd import Select
|
||||
from aiogram_dialog.widgets.kbd import Button, Select
|
||||
from aiogram_dialog.widgets.input import ManagedTextInput
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from src.application.dto import UserDto
|
||||
from src.application.use_cases.settings.commands.notifications import ToggleNotification
|
||||
from src.application.use_cases.settings.commands.notifications import (
|
||||
ClearSystemNotifyRoute,
|
||||
ToggleNotification,
|
||||
UpdateSystemNotifyRoute,
|
||||
UpdateSystemNotifyRouteDto,
|
||||
)
|
||||
from src.core.constants import USER_KEY
|
||||
from src.core.enums import SystemNotificationType, UserNotificationType
|
||||
from src.telegram.states import RemnashopNotifications
|
||||
|
||||
|
||||
@inject
|
||||
@@ -22,13 +29,126 @@ async def on_user_type_select(
|
||||
await toggle_notification(user, selected_type)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_system_type_select(
|
||||
callback: CallbackQuery,
|
||||
widget: Select,
|
||||
dialog_manager: DialogManager,
|
||||
selected_type: SystemNotificationType,
|
||||
selected_type: str,
|
||||
) -> None:
|
||||
# strip route indicator suffix if present
|
||||
clean_type = selected_type.replace(" 📡", "").strip()
|
||||
dialog_manager.dialog_data["route_ntf_type"] = clean_type
|
||||
await dialog_manager.switch_to(RemnashopNotifications.SYSTEM_TYPE)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_system_type_toggle(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
toggle_notification: FromDishka[ToggleNotification],
|
||||
) -> None:
|
||||
user: UserDto = dialog_manager.middleware_data[USER_KEY]
|
||||
await toggle_notification(user, selected_type)
|
||||
ntf_type = dialog_manager.dialog_data.get("route_ntf_type")
|
||||
if ntf_type:
|
||||
await toggle_notification(user, ntf_type)
|
||||
|
||||
|
||||
async def on_system_route_edit_chat(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.switch_to(RemnashopNotifications.SYSTEM_ROUTE_CHAT_ID)
|
||||
|
||||
|
||||
async def on_system_route_edit_thread(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.switch_to(RemnashopNotifications.SYSTEM_ROUTE_THREAD_ID)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_route_clear(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
clear_route: FromDishka[ClearSystemNotifyRoute],
|
||||
) -> None:
|
||||
user: UserDto = dialog_manager.middleware_data[USER_KEY]
|
||||
ntf_type = dialog_manager.dialog_data.get("route_ntf_type")
|
||||
if ntf_type:
|
||||
await clear_route(user, ntf_type)
|
||||
await dialog_manager.switch_to(RemnashopNotifications.SYSTEM_ROUTE)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_chat_id_entered(
|
||||
message: Message,
|
||||
widget: ManagedTextInput,
|
||||
dialog_manager: DialogManager,
|
||||
value: int,
|
||||
update_route: FromDishka[UpdateSystemNotifyRoute],
|
||||
) -> None:
|
||||
user: UserDto = dialog_manager.middleware_data[USER_KEY]
|
||||
ntf_type = dialog_manager.dialog_data.get("route_ntf_type")
|
||||
if not ntf_type:
|
||||
return
|
||||
|
||||
settings_dao = await dialog_manager.middleware_data["dishka_container"].get(
|
||||
__import__("src.application.common.dao", fromlist=["SettingsDao"]).SettingsDao
|
||||
)
|
||||
settings = await settings_dao.get()
|
||||
current_route = settings.notifications.get_route(ntf_type)
|
||||
thread_id = current_route.thread_id if current_route else None
|
||||
|
||||
await update_route(user, UpdateSystemNotifyRouteDto(
|
||||
notification_type=ntf_type,
|
||||
chat_id=value,
|
||||
thread_id=thread_id,
|
||||
))
|
||||
await message.delete()
|
||||
await dialog_manager.switch_to(RemnashopNotifications.SYSTEM_ROUTE)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_thread_id_entered(
|
||||
message: Message,
|
||||
widget: ManagedTextInput,
|
||||
dialog_manager: DialogManager,
|
||||
value: int,
|
||||
update_route: FromDishka[UpdateSystemNotifyRoute],
|
||||
) -> None:
|
||||
user: UserDto = dialog_manager.middleware_data[USER_KEY]
|
||||
ntf_type = dialog_manager.dialog_data.get("route_ntf_type")
|
||||
if not ntf_type:
|
||||
return
|
||||
|
||||
settings_dao = await dialog_manager.middleware_data["dishka_container"].get(
|
||||
__import__("src.application.common.dao", fromlist=["SettingsDao"]).SettingsDao
|
||||
)
|
||||
settings = await settings_dao.get()
|
||||
current_route = settings.notifications.get_route(ntf_type)
|
||||
|
||||
if not current_route:
|
||||
await message.answer("⚠️ Сначала укажи Chat ID")
|
||||
return
|
||||
|
||||
await update_route(user, UpdateSystemNotifyRouteDto(
|
||||
notification_type=ntf_type,
|
||||
chat_id=current_route.chat_id,
|
||||
thread_id=value if value != 0 else None,
|
||||
))
|
||||
await message.delete()
|
||||
await dialog_manager.switch_to(RemnashopNotifications.SYSTEM_ROUTE)
|
||||
|
||||
|
||||
async def on_invalid_int(
|
||||
message: Message,
|
||||
widget: ManagedTextInput,
|
||||
dialog_manager: DialogManager,
|
||||
error: ValueError,
|
||||
) -> None:
|
||||
await message.answer("⚠️ Введи целое число")
|
||||
|
||||
@@ -134,6 +134,10 @@ class RemnashopNotifications(StatesGroup):
|
||||
MAIN = State()
|
||||
USER = State()
|
||||
SYSTEM = State()
|
||||
SYSTEM_TYPE = State()
|
||||
SYSTEM_ROUTE = State()
|
||||
SYSTEM_ROUTE_CHAT_ID = State()
|
||||
SYSTEM_ROUTE_THREAD_ID = State()
|
||||
|
||||
|
||||
class RemnashopPlans(StatesGroup):
|
||||
|
||||
Reference in New Issue
Block a user