Untitled

 avatar
4ae4d
plain_text
a month ago
117 kB
3
Indexable
вот дифф за коммиты, которые я хочу проверить:


diff --git a/services/bot/core/config/validation_config.py b/services/bot/core/config/validation_config.py
index f9c3b23..8baa957 100644
--- a/services/bot/core/config/validation_config.py
+++ b/services/bot/core/config/validation_config.py
@@ -1,28 +1,79 @@
 import os
+from core.utils.logger import logger
 import yaml
 
+
 class ValidationConfig:
-    
+
     def __init__(self, path: str = "/bot/validation.yml"):
         if not os.path.exists(path):
             raise FileNotFoundError(f"Файл не найден: {path}")
         try:
-            with open(path, "r", encoding="utf-8") as f:
-                self._data = yaml.safe_load(f) or {}
+            with open(path, encoding="utf-8") as f:
+                raw = yaml.safe_load(f) or {}
         except yaml.YAMLError as e:
             raise ValueError(f"Ошибка парсинга YAML: {e}") from e
 
+        self._data = self._cast_numeric_rules(raw)
+
+    def _cast_numeric_rules(self, data: dict) -> dict:
+        result = {}
+        for k, v in data.items():
+            if isinstance(v, dict):
+                result[k] = self._cast_numeric_rules(v)
+            elif k in ("min", "max", "min_length", "max_length"):
+                try:
+                    result[k] = int(v)
+                except (TypeError, ValueError):
+                    result[k] = v
+            else:
+                result[k] = v
+        return result
+
     def get(self, key: str, default=None):
+        # Если ключ не начинается с известного корневого раздела — добавляем event
+        if not any(key.startswith(prefix) for prefix in ("event.", "event")):
+            key = f"event.{key}"
+
         val = self._data
-        for k in key.split("."):
-            if isinstance(val, dict) and k in val:
-                val = val[k]
+        for part in key.split("."):
+            if isinstance(val, dict) and part in val:
+                logger.info("%s\n", val)
+                val = val[part]
             else:
                 return default
         return val
 
-    def __getitem__(self, key):
-        return self._data[key]
+    def __getitem__(self, key: str):
+        """
+        - cfg["title.min_length"] → event.title.min_length
+        - cfg["event.hours.min"] → event.hours.min
+        - cfg["event"]["title"] → прямой доступ к вложенному словарю
+        """
+        if "." in key:
+            path = key
+            if not any(path.startswith(p) for p in ("event.", "event")):
+                path = f"event.{path}"
+
+            val = self._data
+            for part in path.split("."):
+                if isinstance(val, dict) and part in val:
+                    val = val[part]
+                else:
+                    raise KeyError(f"Ключ не найден: {key}")
+            return val
+
+        if key in self._data:
+            return self._data[key]
+        raise KeyError(f"Ключ не найден: {key}")
+
+    def __contains__(self, key: str) -> bool:
+        try:
+            self[key]
+            return True
+        except KeyError:
+            return False
+
 
-    def __contains__(self, key):
-        return key in self._data
\ No newline at end of file
+# Глобальный инстанс
+validation_cfg = ValidationConfig()
diff --git a/services/bot/core/handlers/moderation.py b/services/bot/core/handlers/moderation.py
index b8a32e1..6b3158f 100644
--- a/services/bot/core/handlers/moderation.py
+++ b/services/bot/core/handlers/moderation.py
@@ -7,6 +7,7 @@ from dialog_bot_sdk.interactive_media import Button
 from core.bot_kit.fsm import FSMContext, State, StatesGroup
 from core.bot_kit.router import Router
 from core.config import bot
+from core.config.validation_config import validation_cfg as cfg
 from core.handlers.events_ui import (
     build_back_to_event_editing_keyboard,
     build_back_to_event_keyboard,
@@ -17,6 +18,15 @@ from core.handlers.events_ui import (
     split_event_value,
 )
 from core.handlers.request import build_request_actions_keyboard
+from core.handlers.validators import (
+    validate_date,
+    validate_description,
+    validate_hours,
+    validate_participation_limit,
+    validate_time,
+    validate_title,
+    validate_verification_code,
+)
 from core.markups import (
     back_to_moderation_and_skip_keyboard,
     back_to_moderation_keyboard,
@@ -41,7 +51,30 @@ from core.utils import (
 TAGS_MEDIA_ID = "event_tags_toggle"
 TAGS_TOGGLE_PREFIX = "toggle:"
 TAGS_DONE_VALUE = "done"
+
+
 # TODO убрать хардкод
+# ─────────────────────────────────────────────────────────────
+# 🔧 Хелперы для адаптивных сообщений (используют cfg["..."])
+# ─────────────────────────────────────────────────────────────
+def _cfg(field: str, rule: str, default=None):
+    """Короткий доступ: _cfg('title', 'min_length', 4) → cfg["title.min_length"] или default"""
+    try:
+        return cfg[f"{field}.{rule}"]
+    except KeyError:
+        return default
+
+
+def _err(field: str, message: str, **kwargs):
+    """Форматирует ошибку, подставляя лимиты из YAML: {min}, {max}, {min_len}, {max_len}"""
+    limits = {
+        "min": _cfg(field, "min"),
+        "max": _cfg(field, "max"),
+        "min_len": _cfg(field, "min_length"),
+        "max_len": _cfg(field, "max_length"),
+    }
+    limits = {k: v for k, v in limits.items() if v is not None}
+    return message.format(**kwargs, **limits)
 
 
 events_rt = Router()
@@ -239,10 +272,20 @@ def create_event_start_handler(
 @bot.di
 def event_create_name_step(message: UpdateMessage, context: FSMContext):
     delete_prev_message(bot, message)
-
-    name = message.message.text_message.text.strip()
+    raw_name = message.message.text_message.text.strip()
+    try:
+        name = validate_title(raw_name)
+    except ValueError:
+        bot.messaging.send_message(
+            peer=message.peer,
+            text=_err(
+                "title",
+                "⚠️ Название должно содержать от {min_len} до {max_len} символов. Попробуй ещё раз:",
+            ),
+            interactive_media_groups=back_to_moderation_keyboard(),
+        )
+        return
     context.update_data({"event_name": name})
-
     bot.messaging.send_message(
         peer=message.peer,
         text="Шаг 2/11: введи, пожалуйста, дату мероприятия например, в формате `20.02.2002`:",
@@ -251,58 +294,28 @@ def event_create_name_step(message: UpdateMessage, context: FSMContext):
     context.set_state(EventCreateState.date)
 
 
-def _validate_date(text):  # throws ValueError
-    formats = []
-    for first_gap in ". -_":
-        for second_gap in ". -_":
-            formats.append(f"%d{first_gap}%m{second_gap}%Y")
-
-    date_obj = None
-    for format in formats:
-        try:
-            date_obj = datetime.strptime(text, format).date()
-        except ValueError:
-            continue
-
-    if date_obj is None:
-        raise ValueError(f"unsupported date format in date {text!r}")
-
-    return date_obj
-
-
 @events_rt.message(state=EventCreateState.date)
 @bot.di
 def event_create_date_step(message: UpdateMessage, context: FSMContext):
     delete_prev_message(bot, message)
-
     date = message.message.text_message.text.strip()
-
     try:
-        date_obj = _validate_date(date)
+        date_obj = validate_date(date)
     except ValueError:
         bot.messaging.send_message(
             peer=message.peer,
-            text=(
-                "⚠️ Я не смог распознать дату.\n"
-                "Пожалуйста, введи дату, например, в формате `20.02.2002`:"
-            ),
+            text="⚠️ Я не смог распознать дату.\nПожалуйста, введи дату, например, в формате `20.02.2002`:",
             interactive_media_groups=back_to_moderation_keyboard(),
         )
         return
-
     if date_obj <= datetime.now().date():
         bot.messaging.send_message(
             peer=message.peer,
-            text=(
-                "⚠️ Ты не можешь создать мероприятие раньше завтрашнего дня.\n"
-                "Пожалуйста, введи дату, например, в формате `20.02.2002`:"
-            ),
+            text="⚠️ Ты не можешь создать мероприятие раньше завтрашнего дня.\nПожалуйста, введи дату, например, в формате `20.02.2002`:",
             interactive_media_groups=back_to_moderation_keyboard(),
         )
         return
-
     context.update_data({"event_date": date_obj})
-
     bot.messaging.send_message(
         peer=message.peer,
         text="Шаг 3/11: введи, пожалуйста, время мероприятия например, в формате `10:30`:",
@@ -311,49 +324,25 @@ def event_create_date_step(message: UpdateMessage, context: FSMContext):
     context.set_state(EventCreateState.time)
 
 
-def _validate_time(text):  # throws ValueError
-    time_obj = None
-    for format in ["%H:%M", "%H %M", "%H-%M", "%H_%M"]:
-        try:
-            time_obj = datetime.strptime(text, format).time()
-        except ValueError:
-            continue
-
-    if time_obj is None:
-        raise ValueError(f"unsupported time format in time {text!r}", text)
-
-    return time_obj
-
-
 @events_rt.message(state=EventCreateState.time)
 @bot.di
 def event_create_time_step(
     message: UpdateMessage, context: FSMContext, terbank_service: TerbankService
 ):
     delete_prev_message(bot, message)
-
     time = message.message.text_message.text.strip()
-
     try:
-        time_obj = _validate_time(time)
+        time_obj = validate_time(time)
     except ValueError:
         bot.messaging.send_message(
             peer=message.peer,
-            text=(
-                "⚠️ Я не смог распознать время.\n"
-                "Пожалуйста, введи в формате `HH:MM`, например: `10:30`:"
-            ),
+            text="⚠️ Я не смог распознать время.\nПожалуйста, введи в формате `HH:MM`, например: `10:30`:",
             interactive_media_groups=back_to_moderation_keyboard(),
         )
         return
-
     context.update_data({"event_time": time_obj})
-
     send_tb_select_menu(
-        peer=message.peer,
-        terbank_service=terbank_service,
-        context=context,
-        is_edit=False,
+        peer=message.peer, terbank_service=terbank_service, context=context, is_edit=False
     )
 
 
@@ -381,12 +370,8 @@ def _tb_select_footer_buttons(
 
 
 @bot.di
-def event_create_back_to_time_handler(
-    event: UpdateInteractiveMediaEvent,
-    context: FSMContext,
-):
+def event_create_back_to_time_handler(event: UpdateInteractiveMediaEvent, context: FSMContext):
     delete_prev_message_by_peer(bot, event.peer)
-
     bot.messaging.send_message(
         peer=event.peer,
         text="Шаг 3/11: введи, пожалуйста, время мероприятия например, в формате `10:30`:",
@@ -621,6 +606,17 @@ def event_create_description_step(message: UpdateMessage, context: FSMContext):
     delete_prev_message(bot, message)
 
     description = message.message.text_message.text.strip()
+
+    try:
+        description = validate_description(description)
+    except ValueError as e:
+        bot.messaging.send_message(
+            peer=message.peer,
+            text=f"⚠️ {e}. Попробуй ещё раз:",
+            interactive_media_groups=back_to_moderation_keyboard(),
+        )
+        return
+
     context.update_data({"event_description": description})
     bot.messaging.send_message(
         peer=message.peer,
@@ -630,20 +626,6 @@ def event_create_description_step(message: UpdateMessage, context: FSMContext):
     context.set_state(EventCreateState.hours)
 
 
-def _validate_hours_amount(text) -> int:  # throws ValueError
-    if not isinstance(text, int) and not isinstance(text, str):
-        logger.error("expected type str or int for hours amount, got '%s'", type(text))
-        raise ValueError(f"expected type str or int for hours amount, got {type(text)!r}")
-    text = str(text)
-    if not re.fullmatch(r"^[0-9]{1,2}$", text):
-        logger.error("expected <= 2 long digit sequence for hours amount, got '%s'", text)
-        raise ValueError(f"expected <= 2 long digit sequence for hours amount, got {text!r}")
-    hours = int(text)
-    if hours <= 0 or hours >= 19:
-        raise ValueError(f"hours value must be positive integer <= 18, got {hours!r}")
-    return hours
-
-
 @events_rt.message(state=EventCreateState.hours)
 @bot.di
 def event_create_hours_step(
@@ -655,11 +637,11 @@ def event_create_hours_step(
 
     text = message.message.text_message.text.strip()
     try:
-        hours = _validate_hours_amount(text)
-    except ValueError:
+        hours = validate_hours(text)
+    except ValueError as e:
         bot.messaging.send_message(
             peer=message.peer,
-            text="⚠️ Количество часов должно быть натуральным числом, меньшим 19. Попробуй ещё раз.",
+            text=f"⚠️ {e}. Попробуй ещё раз.",
             interactive_media_groups=back_to_moderation_keyboard(),
         )
         return
@@ -719,7 +701,10 @@ def event_tags_toggle_handler(
             context.update_data({"event_app_tag_ids": app_tag_ids})
             bot.messaging.send_message(
                 peer=event.peer,
-                text="Шаг 10/11: введи код мероприятия:\nБез пробелов, не больше 16 символов. Например: WELCOME2025",
+                text=_err(
+                    "verification_code",
+                    "Шаг 10/11: введи код мероприятия:\nБез пробелов, от {min_len} до {max_len} символов. Например: WELCOME2025",
+                ),
                 interactive_media_groups=back_to_moderation_keyboard(),
             )
             context.set_state(EventCreateState.code)
@@ -887,14 +872,13 @@ def event_create_code_step(
     delete_prev_message(bot, message)
 
     try:
-        code = _validate_verification_code(message.message.text_message.text.strip())
-    except ValueError:
+        code = validate_verification_code(
+            message.message.text_message.text.strip()
+        )  # 🔹 импортированный валидатор
+    except ValueError as e:
         bot.messaging.send_message(
             peer=message.peer,
-            text=(
-                "⚠️ Код должен быть комбинацией русских и латинских букв и цифр, "
-                "длиной не больше 16 символов. Попробуй еще раз:"
-            ),
+            text=f"⚠️ {e}. Попробуй еще раз:",  # 🔹 сообщение из валидатора
             interactive_media_groups=back_to_moderation_keyboard(),
         )
         return
@@ -903,7 +887,10 @@ def event_create_code_step(
 
     bot.messaging.send_message(
         peer=message.peer,
-        text="Шаг 11/11: введи требуемое количество участников:\nПусть это будет число от 1 до 10000",
+        text=_err(  # 🔹 адаптивный текст через _err
+            "participation_limit",
+            "Шаг 11/11: введи требуемое количество участников:\nПусть это будет число от {min} до {max}",
+        ),
         interactive_media_groups=back_to_moderation_keyboard(),
     )
 
@@ -923,11 +910,11 @@ def event_create_participation_limit_step(
     delete_prev_message(bot, message)
 
     try:
-        limit = _validate_participation_limit(message.message.text_message.text.strip())
-    except ValueError:
+        limit = validate_participation_limit(message.message.text_message.text.strip())
+    except ValueError as e:
         bot.messaging.send_message(
             peer=message.peer,
-            text=("⚠️ Количество участников должно быть числом от 1 до 10000. Попробуй еще раз:"),
+            text=f"⚠️ {e}. Попробуй еще раз:",
             interactive_media_groups=back_to_moderation_keyboard(),
         )
         return
@@ -1415,7 +1402,15 @@ def event_menu_edit_field_handler(
     elif field == "time":
         hint = "*Введи время в формате* `HH:MM`, например: `10:30`"
     elif field == "participation_limit":
-        hint = "*Введи максимальное количество участников* от 1 до 10000"
+        hint = _err("participation_limit", "*Введи число от {min} до {max}*")
+    elif field == "hours":
+        hint = _err("hours", "*Введи число от {min} до {max}*")
+    elif field == "title":
+        hint = _err("title", "*Введи от {min_len} до {max_len} символов*")
+    elif field == "code":
+        hint = _err("verification_code", "*От {min_len} до {max_len} символов, буквы и цифры*")
+    elif field == "description":
+        hint = _err("description", "*Максимум {max_len} символов*")
     else:
         hint = "*Введи новое значение.* Чтобы очистить поле - отправь: «-»"
 
@@ -1469,11 +1464,11 @@ def event_edit_value_step(
 
     if field == "hours":
         try:
-            value = _validate_hours_amount(value)
-        except ValueError:
+            value = validate_hours(value)
+        except ValueError as e:
             bot.messaging.send_message(
                 peer=message.peer,
-                text="⚠️ Количество часов должно быть натуральным числом, меньшим 19. Попробуй ещё раз.",
+                text=f"⚠️ {e}",
                 interactive_media_groups=build_back_to_event_editing_keyboard(
                     event_id=event_id,
                     ctx_name=ctx_name,
@@ -1483,13 +1478,11 @@ def event_edit_value_step(
 
     if field == "participation_limit":
         try:
-            value = _validate_participation_limit(value)
-        except ValueError:
+            value = validate_participation_limit(value)
+        except ValueError as e:
             bot.messaging.send_message(
                 peer=message.peer,
-                text=(
-                    "⚠️ Количество участников должно быть числом от 1 до 10000. Попробуй еще раз:"
-                ),
+                text=f"⚠️ {e}",
                 interactive_media_groups=build_back_to_event_editing_keyboard(
                     event_id=event_id,
                     ctx_name=ctx_name,
@@ -1499,14 +1492,25 @@ def event_edit_value_step(
 
     if field == "code":
         try:
-            value = _validate_verification_code(value)
-        except ValueError:
+            value = validate_verification_code(value)
+        except ValueError as e:
             bot.messaging.send_message(
                 peer=message.peer,
-                text=(
-                    "⚠️ Код должен быть комбинацией русских и латинских букв и цифр, "
-                    "длиной не больше 16 символов. Попробуй еще раз:"
+                text=f"⚠️ {e}",
+                interactive_media_groups=build_back_to_event_editing_keyboard(
+                    event_id=event_id,
+                    ctx_name=ctx_name,
                 ),
+            )
+            return
+
+    if field == "title":
+        try:
+            value = validate_title(value)
+        except ValueError as e:
+            bot.messaging.send_message(
+                peer=message.peer,
+                text=f"⚠️ {e}",
                 interactive_media_groups=build_back_to_event_editing_keyboard(
                     event_id=event_id,
                     ctx_name=ctx_name,
@@ -1516,7 +1520,7 @@ def event_edit_value_step(
 
     if field == "date":
         try:
-            date_obj = _validate_date(str(value))
+            date_obj = validate_date(str(value))
         except ValueError:
             bot.messaging.send_message(
                 peer=message.peer,
@@ -1556,7 +1560,7 @@ def event_edit_value_step(
 
     if field == "time":
         try:
-            time_obj = _validate_time(str(value))
+            time_obj = validate_time(str(value))
         except ValueError:
             bot.messaging.send_message(
                 peer=message.peer,
diff --git a/services/bot/core/handlers/validators.py b/services/bot/core/handlers/validators.py
new file mode 100644
index 0000000..de5e78b
--- /dev/null
+++ b/services/bot/core/handlers/validators.py
@@ -0,0 +1,110 @@
+import re
+from datetime import datetime
+
+from core.config.validation_config import validation_cfg
+
+def _get_rule(field: str, rule: str, default=None):
+    return validation_cfg.get(f"event.{field}.{rule}", default)
+
+
+def validate_title(text: str):
+    min_len = _get_rule("title", "min_length", 4)
+    max_len = _get_rule("title", "max_length", 20)
+
+    if len(text) < min_len or len(text) > max_len:
+        raise ValueError(f"event_name must be in a {min_len} and {max_len}, got {len(text)}")
+    return text
+
+
+def validate_description(text: str):
+    max_len = _get_rule("description", "max_length", 20)
+
+    if len(text) > max_len:
+        raise ValueError(f"event_name must be lower than {max_len}, got {len(text)} symbols")
+    return text
+
+
+def validate_date(text: str):
+    """Парсинг даты с поддержкой разделителей . - _ . Возвращает date."""
+    formats = []
+    for first_gap in ". -_":
+        for second_gap in ". -_":
+            formats.append(f"%d{first_gap}%m{second_gap}%Y")
+
+    date_obj = None
+    for fmt in formats:
+        try:
+            date_obj = datetime.strptime(text.strip(), fmt).date()
+            break
+        except ValueError:
+            continue
+
+    if date_obj is None:
+        raise ValueError(f"unsupported date format in date {text!r}")
+    return date_obj
+
+
+def validate_time(text: str):
+    time_obj = None
+    for fmt in ["%H:%M", "%H %M", "%H-%M", "%H_%M"]:
+        try:
+            time_obj = datetime.strptime(text.strip(), fmt).time()
+            break
+        except ValueError:
+            continue
+
+    if time_obj is None:
+        raise ValueError(f"unsupported time format in time {text!r}")
+    return time_obj
+
+
+def validate_hours(text):
+    """Валидация часов: 1-2 цифры, диапазон 1-18 (из YAML)."""
+    if not isinstance(text, (int, str)):
+        raise ValueError(f"expected type str or int for hours amount, got {type(text)!r}")
+
+    text = str(text).strip()
+    if not re.fullmatch(r"^[0-9]{1,2}$", text):
+        raise ValueError(f"expected <= 2 long digit sequence for hours amount, got {text!r}")
+
+    hours = int(text)
+    min_val = _get_rule("hours", "min", 1)
+    max_val = _get_rule("hours", "max", 18)
+
+    if hours < min_val or hours > max_val:
+        raise ValueError(
+            f"hours value must be positive integer between {min_val} and {max_val}, got {hours!r}"
+        )
+    return hours
+
+
+def validate_verification_code(text: str):
+    if not re.fullmatch(r"^[A-Za-zА-Яа-я0-9]+$", text):
+        raise ValueError(f"unsupported verification code format: {text!r}")
+
+    min_len = _get_rule("verification_code", "min_length", 4)
+    max_len = _get_rule("verification_code", "max_length", 20)
+
+    if len(text) < min_len or len(text) > max_len:
+        raise ValueError(
+            f"verification code length must be between {min_len} and {max_len}, got {len(text)}"
+        )
+    return text
+
+
+def validate_participation_limit(value):
+    try:
+        limit = int(value)
+    except (TypeError, ValueError) as e:
+        raise ValueError(
+            f"expected int or numeric string for participation limit, got {value!r}, which has type {type(value)!r}"
+        ) from e
+
+    min_val = _get_rule("participation_limit", "min", 1)
+    max_val = _get_rule("participation_limit", "max", 1000)
+
+    if limit < min_val:
+        raise ValueError(f"participation limit must be positive integer, got {limit!r}")
+    if limit > max_val:
+        raise ValueError(f"participation limit is too high: {limit}")
+    return limit

вот содержимое изменённых файлов:

f=services/bot/core/config/validation_config.py
f=services/bot/core/handlers/moderation.py
--- services/bot/core/config/validation_config.py ---
import os
from core.utils.logger import logger
import yaml


class ValidationConfig:

    def __init__(self, path: str = "/bot/validation.yml"):
        if not os.path.exists(path):
            raise FileNotFoundError(f"Файл не найден: {path}")
        try:
            with open(path, encoding="utf-8") as f:
                raw = yaml.safe_load(f) or {}
        except yaml.YAMLError as e:
            raise ValueError(f"Ошибка парсинга YAML: {e}") from e

        self._data = self._cast_numeric_rules(raw)

    def _cast_numeric_rules(self, data: dict) -> dict:
        result = {}
        for k, v in data.items():
            if isinstance(v, dict):
                result[k] = self._cast_numeric_rules(v)
            elif k in ("min", "max", "min_length", "max_length"):
                try:
                    result[k] = int(v)
                except (TypeError, ValueError):
                    result[k] = v
            else:
                result[k] = v
        return result

    def get(self, key: str, default=None):
        # Если ключ не начинается с известного корневого раздела — добавляем event
        if not any(key.startswith(prefix) for prefix in ("event.", "event")):
            key = f"event.{key}"

        val = self._data
        for part in key.split("."):
            if isinstance(val, dict) and part in val:
                logger.info("%s\n", val)
                val = val[part]
            else:
                return default
        return val

    def __getitem__(self, key: str):
        """
        - cfg["title.min_length"] → event.title.min_length
        - cfg["event.hours.min"] → event.hours.min
        - cfg["event"]["title"] → прямой доступ к вложенному словарю
        """
        if "." in key:
            path = key
            if not any(path.startswith(p) for p in ("event.", "event")):
                path = f"event.{path}"

            val = self._data
            for part in path.split("."):
                if isinstance(val, dict) and part in val:
                    val = val[part]
                else:
                    raise KeyError(f"Ключ не найден: {key}")
            return val

        if key in self._data:
            return self._data[key]
        raise KeyError(f"Ключ не найден: {key}")

    def __contains__(self, key: str) -> bool:
        try:
            self[key]
            return True
        except KeyError:
            return False


# Глобальный инстанс
validation_cfg = ValidationConfig()

--- services/bot/core/handlers/moderation.py ---
import re
from datetime import datetime

from dialog_bot_sdk.entities.messaging import UpdateInteractiveMediaEvent, UpdateMessage
from dialog_bot_sdk.interactive_media import Button

from core.bot_kit.fsm import FSMContext, State, StatesGroup
from core.bot_kit.router import Router
from core.config import bot
from core.config.validation_config import validation_cfg as cfg
from core.handlers.events_ui import (
    build_back_to_event_editing_keyboard,
    build_back_to_event_keyboard,
    get_ui,
    send_event_card,
    send_event_cards_page,
    send_my_events_page,
    split_event_value,
)
from core.handlers.request import build_request_actions_keyboard
from core.handlers.validators import (
    validate_date,
    validate_description,
    validate_hours,
    validate_participation_limit,
    validate_time,
    validate_title,
    validate_verification_code,
)
from core.markups import (
    back_to_moderation_and_skip_keyboard,
    back_to_moderation_keyboard,
    choices_from_backend_tags,
    event_edit_fields_keyboard,
    format_event_details,
    format_request_details,
    gosb_select_keyboard,
    moderation_menu_keyboard,
    tags_toggle_keyboard,
    tb_select_keyboard,
)
from core.schemas import EventCardSchema, TagSchema, UserSchema
from core.services import EventService, ReportService, RequestService, TagService, TerbankService
from core.utils import (
    clear_context_keep_events_filters,
    delete_prev_message,
    delete_prev_message_by_peer,
    logger,
)

TAGS_MEDIA_ID = "event_tags_toggle"
TAGS_TOGGLE_PREFIX = "toggle:"
TAGS_DONE_VALUE = "done"


# TODO убрать хардкод
# ─────────────────────────────────────────────────────────────
# 🔧 Хелперы для адаптивных сообщений (используют cfg["..."])
# ─────────────────────────────────────────────────────────────
def _cfg(field: str, rule: str, default=None):
    """Короткий доступ: _cfg('title', 'min_length', 4) → cfg["title.min_length"] или default"""
    try:
        return cfg[f"{field}.{rule}"]
    except KeyError:
        return default


def _err(field: str, message: str, **kwargs):
    """Форматирует ошибку, подставляя лимиты из YAML: {min}, {max}, {min_len}, {max_len}"""
    limits = {
        "min": _cfg(field, "min"),
        "max": _cfg(field, "max"),
        "min_len": _cfg(field, "min_length"),
        "max_len": _cfg(field, "max_length"),
    }
    limits = {k: v for k, v in limits.items() if v is not None}
    return message.format(**kwargs, **limits)


events_rt = Router()


class EventEditState(StatesGroup):  # legacy
    wait_value = State()
    gosb = State()
    tags = State()


class EventEnterCodeState(StatesGroup):  # legacy
    wait_code = State()


class EventViewState(StatesGroup):  # legacy
    wait_event_id = State()


class EventCreateState(StatesGroup):  # legacy
    name = State()
    date = State()
    time = State()
    gosb = State()
    project = State()
    description = State()
    hours = State()
    tags = State()
    code = State()
    participation_limit = State()


@bot.di
def moderation_menu_handler(event: UpdateInteractiveMediaEvent, context: FSMContext):  # legacy
    clear_context_keep_events_filters(context)
    delete_prev_message_by_peer(bot, event.peer)

    bot.messaging.send_message(
        peer=event.peer,
        text=("Ты в доме модерации!\n\nПожалуйста, выбери, что хочешь сделать 👇"),
        interactive_media_groups=moderation_menu_keyboard(),
    )


@bot.di
def all_events_handler(  # legacy
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
):
    delete_prev_message_by_peer(bot, event.peer)
    clear_context_keep_events_filters(context)
    send_event_cards_page(
        event.peer,
        offset=0,
        event_service=event_service,
        ctx_name="moderation",
        context=context,
    )


@bot.di
def my_events_handler(  # legacy
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
):
    delete_prev_message_by_peer(bot, event.peer)
    clear_context_keep_events_filters(context)

    send_my_events_page(
        event.peer,
        offset=0,
        event_service=event_service,
        requester_messenger_id=event.peer.id,
        ctx_name="my_events",
        context=context,
    )


@bot.di
def my_events_page_handler(  # legacy
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
):
    delete_prev_message_by_peer(bot, event.peer)
    clear_context_keep_events_filters(context)

    try:
        offset = int(event.data.value)
        if offset < 0:
            offset = 0
    except Exception:
        offset = 0

    send_my_events_page(
        event.peer,
        offset=offset,
        event_service=event_service,
        requester_messenger_id=event.peer.id,
        ctx_name="my_events",
        context=context,
    )


@bot.di
def my_events_open_handler(  # legacy
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
    report_service: ReportService,
    user: UserSchema | None,
):
    delete_prev_message_by_peer(bot, event.peer)
    context.set_state(None)

    event_id, ctx_name = split_event_value(event.data.value)
    send_event_card(
        event.peer,
        event_id=event_id,
        event_service=event_service,
        user=user,
        ctx_name=ctx_name or "my_events",
        report_service=report_service,
    )


@bot.di
def all_events_page_handler(  # legacy
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
):
    delete_prev_message_by_peer(bot, event.peer)
    clear_context_keep_events_filters(context)

    try:
        offset = int(event.data.value)
        if offset < 0:
            offset = 0
    except Exception:
        offset = 0

    send_event_cards_page(
        event.peer,
        offset=offset,
        event_service=event_service,
        ctx_name="moderation",
        context=context,
    )


@bot.di
def all_events_open_handler(  # legacy
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
    report_service: ReportService,
    user: UserSchema | None,
):
    delete_prev_message_by_peer(bot, event.peer)
    context.set_state(None)

    event_id, ctx = split_event_value(event.data.value)
    ctx_name = ctx or "moderation"
    send_event_card(
        event.peer,
        event_id=event_id,
        event_service=event_service,
        user=user,
        ctx_name=ctx_name,
        report_service=report_service,
    )


@bot.di
def create_event_start_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
):
    """старт мастера создания мероприятия"""
    delete_prev_message_by_peer(bot, event.peer)

    context.clear()
    bot.messaging.send_message(
        peer=event.peer,
        text="Создание мероприятия.\n\nШаг 1/11: введи, пожалуйста, название мероприятия:",
        interactive_media_groups=back_to_moderation_keyboard(),
    )
    context.set_state(EventCreateState.name)


@events_rt.message(state=EventCreateState.name)
@bot.di
def event_create_name_step(message: UpdateMessage, context: FSMContext):
    delete_prev_message(bot, message)
    raw_name = message.message.text_message.text.strip()
    try:
        name = validate_title(raw_name)
    except ValueError:
        bot.messaging.send_message(
            peer=message.peer,
            text=_err(
                "title",
                "⚠️ Название должно содержать от {min_len} до {max_len} символов. Попробуй ещё раз:",
            ),
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return
    context.update_data({"event_name": name})
    bot.messaging.send_message(
        peer=message.peer,
        text="Шаг 2/11: введи, пожалуйста, дату мероприятия например, в формате `20.02.2002`:",
        interactive_media_groups=back_to_moderation_keyboard(),
    )
    context.set_state(EventCreateState.date)


@events_rt.message(state=EventCreateState.date)
@bot.di
def event_create_date_step(message: UpdateMessage, context: FSMContext):
    delete_prev_message(bot, message)
    date = message.message.text_message.text.strip()
    try:
        date_obj = validate_date(date)
    except ValueError:
        bot.messaging.send_message(
            peer=message.peer,
            text="⚠️ Я не смог распознать дату.\nПожалуйста, введи дату, например, в формате `20.02.2002`:",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return
    if date_obj <= datetime.now().date():
        bot.messaging.send_message(
            peer=message.peer,
            text="⚠️ Ты не можешь создать мероприятие раньше завтрашнего дня.\nПожалуйста, введи дату, например, в формате `20.02.2002`:",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return
    context.update_data({"event_date": date_obj})
    bot.messaging.send_message(
        peer=message.peer,
        text="Шаг 3/11: введи, пожалуйста, время мероприятия например, в формате `10:30`:",
        interactive_media_groups=back_to_moderation_keyboard(),
    )
    context.set_state(EventCreateState.time)


@events_rt.message(state=EventCreateState.time)
@bot.di
def event_create_time_step(
    message: UpdateMessage, context: FSMContext, terbank_service: TerbankService
):
    delete_prev_message(bot, message)
    time = message.message.text_message.text.strip()
    try:
        time_obj = validate_time(time)
    except ValueError:
        bot.messaging.send_message(
            peer=message.peer,
            text="⚠️ Я не смог распознать время.\nПожалуйста, введи в формате `HH:MM`, например: `10:30`:",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return
    context.update_data({"event_time": time_obj})
    send_tb_select_menu(
        peer=message.peer, terbank_service=terbank_service, context=context, is_edit=False
    )


def _tb_select_footer_buttons(
    *,
    is_edit: bool,
    event_id: str | None,
    ctx_name: str,
) -> list[Button]:
    if is_edit:
        return [
            Button(
                media_id="event_menu_edit",
                value=f"{event_id}|{ctx_name}",
                label="⬅️ К редактированию мероприятия",
            )
        ]
    return [
        Button(
            media_id="event_create_back_to_time",
            value="1",
            label="⬅️ Назад",
        )
    ]


@bot.di
def event_create_back_to_time_handler(event: UpdateInteractiveMediaEvent, context: FSMContext):
    delete_prev_message_by_peer(bot, event.peer)
    bot.messaging.send_message(
        peer=event.peer,
        text="Шаг 3/11: введи, пожалуйста, время мероприятия например, в формате `10:30`:",
        interactive_media_groups=back_to_moderation_keyboard(),
    )
    context.set_state(EventCreateState.time)


def send_tb_select_menu(
    *,
    peer,
    terbank_service: TerbankService,
    context: FSMContext,
    is_edit: bool,
    event_id: str | None = None,
    ctx_name: str = "moderation",
    default_tb_id: str | None = None,
    default_gosb_id: str | None = None,
):
    saved_event_id = event_id or context.get_data().get("edit_event_id")

    if is_edit and not saved_event_id:
        logger.error("error while editing gosb: edit_event_id is empty before tb select")
        bot.messaging.send_message(
            peer=peer,
            text="⚠️ Сессия редактирования сброшена. Открой мероприятие заново.",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        context.clear()
        return

    context.update_data(
        {
            "tb_id": default_tb_id or "",
            "gosb_id": default_gosb_id or "",
            "edit_current_tb_id": default_tb_id or "",
            "edit_current_gosb_id": default_gosb_id or "",
            "edit_event_id": saved_event_id,
            "edit_event_ctx": ctx_name,
        }
    )

    if is_edit:
        context.set_state(EventEditState.gosb)
        title = "Редактирование ГОСБ: сначала выбери тербанк:"
    else:
        context.set_state(EventCreateState.gosb)
        title = "Шаг 4/11: выбери тербанк:"

    tbs = terbank_service.get_all_tbs(peer.id)

    bot.messaging.send_message(
        peer=peer,
        text=title,
        interactive_media_groups=tb_select_keyboard(
            terbanks=tbs,
            default_value=default_tb_id,
            footer_buttons=_tb_select_footer_buttons(
                is_edit=is_edit,
                event_id=saved_event_id,
                ctx_name=ctx_name,
            ),
        ),
    )


@bot.di
def tb_reselect_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    terbank_service: TerbankService,
):
    delete_prev_message_by_peer(bot, event.peer)

    data = context.get_data()

    send_tb_select_menu(
        peer=event.peer,
        terbank_service=terbank_service,
        context=context,
        is_edit=_is_equal_state(context, EventEditState.gosb),
        event_id=data.get("edit_event_id"),
        ctx_name=data.get("edit_event_ctx", "moderation"),
    )


@bot.di
def tb_update_selected(event: UpdateInteractiveMediaEvent, context: FSMContext):
    # keeping what user have selected in "tb_id"
    context.update_data({"tb_id": event.data.value})


@bot.di
def tb_approved(
    event: UpdateInteractiveMediaEvent,
    terbank_service: TerbankService,
    context: FSMContext,
):
    # tb was selected and approved. now selecting gosb:
    delete_prev_message_by_peer(bot, event.peer)

    selected_tb_id = context.get_data().get("tb_id")
    if not isinstance(selected_tb_id, str) or not selected_tb_id:
        logger.error(
            "error while creating event: error while selecting tb: selected tb must be a string, got '%s'",
            selected_tb_id,
        )
        bot.messaging.send_message(
            peer=event.peer,
            text="⚠️ Произошла ошибка. Попробуй позже.",
        )
        context.clear()
        return

    gosbs = terbank_service.get_gosbs_for_tb(tb_id=selected_tb_id, messenger_id=event.peer.id)

    is_edit = _is_equal_state(context, EventEditState.gosb)

    data = context.get_data()
    default_gosb_id = data.get("edit_current_gosb_id")

    bot.messaging.send_message(
        peer=event.peer,
        text="Редактирование ГОСБ: выбери новый ГОСБ" if is_edit else "Шаг 5/11: Выбери ГОСБ",
        interactive_media_groups=gosb_select_keyboard(gosbs, default_value=default_gosb_id),
    )


@bot.di
def gosb_update_selected(event: UpdateInteractiveMediaEvent, context: FSMContext):
    # keeping what user have selected in "gosb_id"
    context.update_data({"gosb_id": event.data.value})


@bot.di
def gosb_approved(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
):
    # gosb was selected and approved.
    delete_prev_message_by_peer(bot, event.peer)

    selected_gosb_id = context.get_data().get("gosb_id")
    if not isinstance(selected_gosb_id, str) or not selected_gosb_id:
        logger.error(
            "error while creating event: error while selecting gosb: selected gosb must be a string, got '%s'",
            selected_gosb_id,
        )
        bot.messaging.send_message(
            peer=event.peer,
            text="⚠️ Произошла ошибка. Попробуй позже.",
        )
        context.clear()
        return

    data = context.get_data()
    is_edit = _is_equal_state(context, EventEditState.gosb)

    if is_edit:
        event_id = str(data.get("edit_event_id") or "")
        ctx_name = str(data.get("edit_event_ctx") or "moderation")

        if not event_id:
            logger.error("error while editing gosb: edit_event_id is empty")
            bot.messaging.send_message(
                peer=event.peer,
                text="⚠️ Сессия редактирования сброшена. Открой мероприятие заново.",
                interactive_media_groups=back_to_moderation_keyboard(),
            )
            context.clear()
            return

        updated = event_service.update_event(
            event_id=event_id,
            messenger_id=event.peer.id,
            payload={"gosb_id": selected_gosb_id},
        )

        context.set_state(None)

        _send_event_edit_menu(
            event.peer,
            event_id=event_id,
            event_service=event_service,
            note="✅ ГОСБ успешно изменён." if updated else "⚠️ Не удалось изменить ГОСБ.",
            event_obj=event_service.get_event_by_id(event_id, event.peer.id),
            show_organizer=True,
            ctx_name=ctx_name,
        )
    else:
        bot.messaging.send_message(
            peer=event.peer,
            text=(
                "Шаг 6/11: Введи id проекта, если хочешь создать мероприятие в проекте\n"
                "Введи «-» или нажми «пропустить», если хочешь создать событийное мероприятие - мероприятие вне проекта."
            ),
            interactive_media_groups=back_to_moderation_and_skip_keyboard(),
        )
        context.set_state(EventCreateState.project)


@events_rt.message(state=EventCreateState.project)
@bot.di
def event_create_project_step(message: UpdateMessage, context: FSMContext):
    delete_prev_message(bot, message)
    raw = message.message.text_message.text.strip()
    _handle_project_input(message.peer, raw, context)


@bot.di
def project_input_dash_handler(event: UpdateInteractiveMediaEvent, context: FSMContext):
    delete_prev_message_by_peer(bot, event.peer)
    _handle_project_input(event.peer, "-", context)


def _handle_project_input(peer, raw_text: str, context: FSMContext):
    project_id = "" if raw_text == "-" else raw_text
    context.update_data({"project_id": project_id})

    bot.messaging.send_message(
        peer=peer,
        text="Шаг 7/11: Введи описание:",
        interactive_media_groups=back_to_moderation_keyboard(),
    )
    context.set_state(EventCreateState.description)


@events_rt.message(state=EventCreateState.description)
@bot.di
def event_create_description_step(message: UpdateMessage, context: FSMContext):
    delete_prev_message(bot, message)

    description = message.message.text_message.text.strip()

    try:
        description = validate_description(description)
    except ValueError as e:
        bot.messaging.send_message(
            peer=message.peer,
            text=f"⚠️ {e}. Попробуй ещё раз:",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return

    context.update_data({"event_description": description})
    bot.messaging.send_message(
        peer=message.peer,
        text="Шаг 8/11: Введи количество волонтёрских часов, начисляемых за мероприятие:",
        interactive_media_groups=back_to_moderation_keyboard(),
    )
    context.set_state(EventCreateState.hours)


@events_rt.message(state=EventCreateState.hours)
@bot.di
def event_create_hours_step(
    message: UpdateMessage,
    context: FSMContext,
    tag_service: TagService,
):
    delete_prev_message(bot, message)

    text = message.message.text_message.text.strip()
    try:
        hours = validate_hours(text)
    except ValueError as e:
        bot.messaging.send_message(
            peer=message.peer,
            text=f"⚠️ {e}. Попробуй ещё раз.",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return

    context.update_data({"hours": hours})

    all_tags = tag_service.get_all_tags()
    choices = choices_from_backend_tags(all_tags)

    context.update_data(
        {
            "event_tag_keys": [],
            "all_tags": [{"id": t.id, "title": t.title} for t in all_tags],
        },
    )

    _send_tags_toggle_ui(
        peer=message.peer,
        title="Шаг 9/11: выбери направления, которым соответствует мероприятие, затем нажми «Готово»:",
        choices=choices,
        selected_keys=set(),
        footer_buttons=[Button(media_id="leave", value="moderation", label="⬅️ В меню модерации")],
    )

    context.set_state(EventCreateState.tags)


@bot.di
def event_tags_toggle_handler(
    event: UpdateInteractiveMediaEvent,
    event_service: EventService,
    context: FSMContext,
):
    is_create = _is_equal_state(context, EventCreateState.tags)
    is_edit = _is_equal_state(context, EventEditState.tags)
    if not (is_create or is_edit):
        return

    raw = str(event.data.value or "")
    data = context.get_data()
    ctx_name = str(data.get("edit_event_ctx", "moderation"))

    key_field = "event_tag_keys" if is_create else "edit_tag_keys"
    selected = set(data.get(key_field) or [])

    all_tags = data.get("all_tags", [])
    if not all_tags:
        logger.info("No tags were taken while editing event tags. Please check seeding file.")

    if raw == TAGS_DONE_VALUE:
        title_to_id = {t["title"]: t["id"] for t in all_tags if t.get("title") and t.get("id")}
        app_tag_ids = [title_to_id[n] for n in selected if n in title_to_id]

        delete_prev_message_by_peer(bot, event.peer)

        if is_create:
            context.update_data({"event_app_tag_ids": app_tag_ids})
            bot.messaging.send_message(
                peer=event.peer,
                text=_err(
                    "verification_code",
                    "Шаг 10/11: введи код мероприятия:\nБез пробелов, от {min_len} до {max_len} символов. Например: WELCOME2025",
                ),
                interactive_media_groups=back_to_moderation_keyboard(),
            )
            context.set_state(EventCreateState.code)
            return

        if is_edit:
            current = set(data.get("edit_current_tag_keys") or [])
            event_id = str(data.get("edit_event_id"))

            if not event_id or event_id == "None":
                logger.error(
                    "Error while editing event tags: "
                    f"no event was found by saved id {event_id!r}, "
                    "saved in data by the key 'edit_event_id'. "
                    "exiting editing session"
                )

                context.set_state(None)
                bot.messaging.send_message(
                    peer=event.peer,
                    text="⚠️ Сессия редактирования сброшена. Открой мероприятие заново.",
                    interactive_media_groups=back_to_moderation_keyboard(),
                )
                return

            to_add = selected - current
            to_remove = current - selected

            failed = False

            # add
            for title in to_add:
                tag_id = title_to_id.get(title)
                if not tag_id:
                    logger.error(
                        "Error while editing event tags: "
                        f"selected tag with title={title!r} "
                        f"have no entrance in all_tags={all_tags}. "
                        "skipping this tag."
                    )
                elif not event_service.add_tag(
                    event_id=event_id, tag_id=tag_id, messenger_id=event.peer.id
                ):
                    failed = True

            # remove
            for title in to_remove:
                tag_id = title_to_id.get(title)
                if not tag_id:
                    logger.error(
                        "Error while editing event tags: "
                        f"selected tag with title={title!r} "
                        f"have no entrance in all_tags={all_tags}. "
                        "skipping this tag."
                    )
                elif not event_service.remove_tag(
                    event_id=event_id, tag_id=tag_id, messenger_id=event.peer.id
                ):
                    failed = True

            context.set_state(None)

            _send_event_edit_menu(
                event.peer,
                event_id=event_id,
                event_service=event_service,
                note="⚠️ Ошибка изменения тегов! Некоторые теги могли не измениться."
                if failed
                else "✅ Теги успешно изменены.",
                event_obj=event_service.get_event_by_id(event_id, event.peer.id),
                show_organizer=True,
                ctx_name=ctx_name,
            )

            return

    if raw.startswith(TAGS_TOGGLE_PREFIX):
        key = raw[len(TAGS_TOGGLE_PREFIX) :].strip()
        if key in selected:
            selected.remove(key)
        else:
            selected.add(key)

        context.update_data({key_field: list(selected)})
        delete_prev_message_by_peer(bot, event.peer)

        if is_create:
            footer = [Button(media_id="leave", value="moderation", label="⬅️ В меню модерации")]
            title = "Шаг 9/11: выбери направления, которым соответствует мероприятие, затем нажми «Готово»:"
        else:
            event_id = data.get("edit_event_id") or ""
            footer = [
                Button(
                    media_id="event_menu_edit",
                    value=f"{event_id}|{ctx_name}",
                    label="⬅️ К редактированию мероприятия",
                )
            ]
            title = "Редактирование направления: выбери направления, которым соответствует мероприятие, затем нажми «Готово»:"

        normalized_tags: list[TagSchema] = []

        for tag in all_tags:
            if isinstance(tag, TagSchema):
                normalized_tags.append(tag)
            elif isinstance(tag, dict):
                normalized_tags.append(TagSchema.from_dict(tag))

        choices = choices_from_backend_tags(normalized_tags)

        _send_tags_toggle_ui(
            peer=event.peer,
            title=title,
            choices=choices,
            selected_keys=set(selected),
            footer_buttons=footer,
        )
        return


def _validate_verification_code(text) -> str:  # throws ValueError
    if not isinstance(text, str):
        logger.error("expected type str for verification code, got '%s'", type(text))
        raise ValueError(f"expected type str for verification code, got {type(text)!r}")
    if not re.fullmatch(r"^[A-Za-zА-Яа-я0-9]{1,16}$", text):
        logger.error("unsupported verification code format: '%s'", text)
        raise ValueError(f"unsupported verification code format: {text!r}")
    return text


def _validate_participation_limit(value) -> int:  # throws ValueError
    try:
        limit = int(value)
    except (TypeError, ValueError) as e:
        logger.error(
            "expected int or numeric string for participation limit, got '%s', which has type '%s'",
            value,
            type(value),
        )
        raise ValueError(
            f"expected int or numeric string for participation limit, got {value!r}, which has type {type(value)!r}"
        ) from e

    if limit < 1:
        logger.error("participation limit must be positive integer, got '%d'", limit)
        raise ValueError(f"participation limit must be positive integer, got {limit!r}")

    if limit > 10000:
        logger.error("participation limit is too high: %d", limit)
        raise ValueError(f"participation limit is too high: {limit}")

    return limit


@events_rt.message(state=EventCreateState.code)
@bot.di
def event_create_code_step(
    message: UpdateMessage,
    context: FSMContext,
    event_service: EventService,
    request_service: RequestService,
    report_service: ReportService,
    user: UserSchema,
):
    delete_prev_message(bot, message)

    try:
        code = validate_verification_code(
            message.message.text_message.text.strip()
        )  # 🔹 импортированный валидатор
    except ValueError as e:
        bot.messaging.send_message(
            peer=message.peer,
            text=f"⚠️ {e}. Попробуй еще раз:",  # 🔹 сообщение из валидатора
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return

    context.update_data({"event_verification_code": code})

    bot.messaging.send_message(
        peer=message.peer,
        text=_err(  # 🔹 адаптивный текст через _err
            "participation_limit",
            "Шаг 11/11: введи требуемое количество участников:\nПусть это будет число от {min} до {max}",
        ),
        interactive_media_groups=back_to_moderation_keyboard(),
    )

    context.set_state(EventCreateState.participation_limit)


@events_rt.message(state=EventCreateState.participation_limit)
@bot.di
def event_create_participation_limit_step(
    message: UpdateMessage,
    context: FSMContext,
    event_service: EventService,
    request_service: RequestService,
    report_service: ReportService,
    user: UserSchema,
):
    delete_prev_message(bot, message)

    try:
        limit = validate_participation_limit(message.message.text_message.text.strip())
    except ValueError as e:
        bot.messaging.send_message(
            peer=message.peer,
            text=f"⚠️ {e}. Попробуй еще раз:",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return

    context.update_data({"event_participation_limit": limit})
    data = context.get_data()
    try:
        created = event_service.create_from_wizard(data, message.peer)
    except Exception as e:
        logger.error("Error while creating an event via wizard: %s", e)
        bot.messaging.send_message(
            peer=message.peer,
            text="⚠️ Произошла ошибка.",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        context.clear()
        return

    context.clear()

    note = {
        "REQUEST": "✅ Заявка на мероприятие успешно создана.",
        "COMPLETELY": "✅ Мероприятие успешно создано.",
        "FAILED": "⚠️ Произошла ошибка: не удалось создать заявку/мероприятие.",
    }.get(
        created.get("TYPE", ""),
        "⚠️ Произошла ошибка: не получить статус заявки/мероприятия. Проверь результат в «Своих заявках» или в «Своих мероприятиях».",
    )

    result_type = created.get("TYPE")
    event_id = str(created.get("event_id", ""))
    request_id = str(created.get("request_id", ""))

    if result_type == "COMPLETELY":
        if not event_id:
            logger.error("expected event_id, got none whlie creating event")
            bot.messaging.send_message(
                peer=message.peer,
                text="Мероприятие было создано успешно, но у нас не получилось показать его карточку. Попробуй открыть её из меню мероприятий.",
                interactive_media_groups=back_to_moderation_keyboard(),
            )
        else:
            send_event_card(
                message.peer,
                event_id=event_id,
                event_service=event_service,
                report_service=report_service,
                user=user,
                ctx_name="my_events",  # TODO fix context
                note=note,
            )

    if result_type == "REQUEST":
        if not request_id:
            logger.error("expected reequest_id, got none whlie creating request")
            bot.messaging.send_message(
                peer=message.peer,
                text="Заявка на мероприятие была создано успешно, но у нас не получилось показать её карточку. Попробуй открыть её из меню заявок.",
                interactive_media_groups=back_to_moderation_keyboard(),
            )
        else:
            request_card = request_service.get_request_by_id(request_id, message.peer.id)
            if request_card is not None:
                bot.messaging.send_message(
                    peer=message.peer,
                    text=f"{note}\n\n{format_request_details(request_card)}",
                    interactive_media_groups=build_request_actions_keyboard(
                        request_card,
                        ctx_name="moderation",
                    ),
                )


@bot.di
def event_view_by_id_start(event: UpdateInteractiveMediaEvent, context: FSMContext):  # legacy
    """старт диалога - просим у модератора UUID мероприятия"""
    delete_prev_message_by_peer(bot, event.peer)

    bot.messaging.send_message(
        peer=event.peer,
        text="Введи UUID мероприятия, которое хочешь посмотреть.\n\n",
        interactive_media_groups=back_to_moderation_keyboard(),
    )
    context.set_state(EventViewState.wait_event_id)


@events_rt.message(state=EventViewState.wait_event_id)
@bot.di
def event_view_by_id_step(  # legacy
    message: UpdateMessage,
    context: FSMContext,
    event_service: EventService,
    user: UserSchema,
):
    """читаем UUID, тянем мероприятие из бэкенда и показываем информацию"""
    delete_prev_message(bot, message)
    raw_id = message.message.text_message.text.strip()
    event = event_service.get_event_by_id(raw_id, message.peer.id)

    if event is None:
        bot.messaging.send_message(
            peer=message.peer,
            text="⚠️ Мероприятие с таким UUID не найдено.\nПопробуй ещё раз. Пример UUID: `932cf1cb-1ee7-4a45-bc96-41a232f3f538`",
            interactive_media_groups=back_to_moderation_keyboard(),
        )
        return

    context.update_data({"current_event_id": event.id})
    context.set_state(None)

    send_event_card(
        message.peer,
        event_id=event.id,
        event_service=event_service,
        user=user,
        ctx_name="moderation_menu",
    )


@bot.di
def event_menu_enter_code_start_handler(
    event: UpdateInteractiveMediaEvent, context: FSMContext
):  # legacy
    delete_prev_message_by_peer(bot, event.peer)
    event_id, ctx = split_event_value(event.data.value)
    ctx_name = ctx or "moderation"
    context.update_data({"current_event_id": event_id, "current_event_ctx": ctx_name})
    context.set_state(EventEnterCodeState.wait_code)

    bot.messaging.send_message(
        peer=event.peer,
        text="🎟️ Введи бонус-код мероприятия (без пробелов):",
        interactive_media_groups=build_back_to_event_keyboard(event_id, ctx_name),
    )


@events_rt.message(state=EventEnterCodeState.wait_code)
@bot.di
def event_menu_enter_code_step(
    message: UpdateMessage,
    context: FSMContext,
    event_service: EventService,
    report_service: ReportService,
    user: UserSchema,
):
    delete_prev_message(bot, message)

    event_id = str(context.get_data().get("current_event_id"))
    # если event_id is None, то клиент не найдёт event с id == "None", и всё ок

    code = message.message.text_message.text.strip()
    note: str
    status = event_service.get_participation_status(event_id, message.peer.id)
    if status is None:
        note = "⚠️ Код не принят, сначала нужно записаться на мероприятие"
    elif status.state == "confirmed":
        note = "⚠️ Код не принят, баллы уже получены"
    else:
        ok = event_service.enter_code_by_event(event_id, message.peer.id, code)
        if not ok:
            note = "⚠️ Код не принят, проверь правильность кода"
        else:
            note = "✅ Код принят. Часы начислены."

    data = context.get_data()
    ctx_name = data.get("current_event_ctx") or "moderation"
    context.set_state(None)

    send_event_card(
        message.peer,
        event_id=event_id,
        event_service=event_service,
        report_service=report_service,
        user=user,
        ctx_name=ctx_name,
        note=note,
    )


@bot.di
def event_menu_delete_handler(  # legacy
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
    report_service: ReportService,
    user: UserSchema | None,
):
    delete_prev_message_by_peer(bot, event.peer)
    context.set_state(None)

    event_id, ctx = split_event_value(event.data.value)
    ctx_name = ctx or "moderation"
    if ctx_name not in ("moderation", "moderation_menu"):
        ctx_name = "moderation"

    try:
        ok = event_service.delete_event(
            event_id,
            requester_messenger_id=event.peer.id,
        )
    except Exception:
        ok = False

    if ok:
        context.clear()
        if ctx_name == "moderation":
            send_event_cards_page(
                event.peer,
                offset=0,
                event_service=event_service,
                ctx_name="moderation",
                note="Мероприятие удалено.",
            )
        else:
            bot.messaging.send_message(
                peer=event.peer,
                text="Мероприятие удалено.\n\nТы в доме модерации",
                interactive_media_groups=moderation_menu_keyboard(),
            )
        return

    send_event_card(
        event.peer,
        event_id=event_id,
        event_service=event_service,
        report_service=report_service,
        user=user,
        ctx_name=ctx_name,
        note="⚠️ Не удалось удалить мероприятие",
    )


def _send_event_edit_menu(
    peer,
    *,
    event_id: str,
    event_service: EventService,
    note: str | None = None,
    event_obj=None,  # temporary remove type since it was fixed in another branch
    show_organizer: bool,
    ctx_name: str,
):
    ui = get_ui(ctx_name)
    refreshed = event_obj or event_service.get_event_by_id(event_id, peer.id)
    base = (
        format_event_details(refreshed, show_code=True, show_organizer=show_organizer)
        if refreshed
        else "⚠️ Мероприятие не найдено"
    )

    prefix = f"{note}\n\n" if note else ""
    bot.messaging.send_message(
        peer=peer,
        text=(f"{prefix}Редактирование мероприятия\n\n{base}\n\nВыбери поле для Редактирования:"),
        interactive_media_groups=event_edit_fields_keyboard(
            event_id,
            leave_media_id=ui.list_open_media_id,
            leave_value=f"{event_id}|{ui.name}",
            leave_label="⬅️ К мероприятию",
        ),
    )


@bot.di
def event_menu_edit_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
):
    delete_prev_message_by_peer(bot, event.peer)
    context.set_state(None)
    event_id, ctx_name = split_event_value(event.data.value)
    ctx_name = "moderation" if ctx_name is None else ctx_name
    context.update_data(
        {
            "edit_event_id": event_id,
            "edit_event_ctx": ctx_name,
        }
    )

    _send_event_edit_menu(
        event.peer,
        event_id=event_id,
        event_service=event_service,
        show_organizer=True,
        ctx_name=ctx_name,
    )


def _get_field_label(field: str) -> str:
    return {
        "title": "Название",
        "date": "Дата",
        "time": "Время",
        "tags": "Теги",
        "gosb_id": "ГОСБ",
        "event_organizers": "Организаторы",
        "participation_limit": "Максимальное количество участников",
        "hours": "Часы",
        "description": "Описание",
        "code": "Код",
    }.get(field, field)


def _get_current_value(event_card: EventCardSchema, field: str) -> str:
    event_fields = {
        # valid fields
        "title",
        "description",
        "gosb_id",
        "hours",
        "participation_limit",
        # TODO rename properly
        "verification_code",  # from code
        "event_organizers",  # from creator_id
    }

    match field:
        case "date":
            val = event_card.event.date_time.strftime("%d.%m.%Y")
        case "time":
            val = event_card.event.date_time.strftime("%H:%M")
        case "code":  # once 'code' is renamed, this will be deleted
            val = event_card.event.verification_code
        case "creator_id":  # once 'creator_id' is renamed, this will be deleted
            val = event_card.event.creator_id
        case "gosb_id":
            val = event_card.gosb.name
        case _:
            val = (
                getattr(event_card.event, field, None)
                if field in event_fields
                else getattr(event_card, field, None)
            )

    return (
        ("\n" if len(str(val)) > 140 else "")
        + "_"
        + ("-" if val is None or val == "" else str(val)).replace("\n", "_\n_")
        + "_"
    )


def _is_equal_state(context: FSMContext, state: State) -> bool:
    current = context.get_state()
    return current == state or str(current) == str(state)


def _send_tags_toggle_ui(
    *,
    peer,
    title: str,
    choices,
    selected_keys: set[str],
    footer_buttons: list[Button],
):
    bot.messaging.send_message(
        peer=peer,
        text=title,
        interactive_media_groups=tags_toggle_keyboard(
            choices=choices,
            selected_keys=selected_keys,
            media_id=TAGS_MEDIA_ID,
            toggle_value_prefix=TAGS_TOGGLE_PREFIX,
            done_value=TAGS_DONE_VALUE,
            done_label="✅ Готово",
            extra_footer=footer_buttons,
        ),
    )


@bot.di
def event_menu_edit_field_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    event_service: EventService,
    tag_service: TagService,
    terbank_service: TerbankService,
):
    delete_prev_message_by_peer(bot, event.peer)
    raw = str(event.data.value or "")

    if "|" not in raw:
        logger.error("Got unexpected event.data.value='%s' while trying to edit event", raw)
        bot.messaging.send_message(peer=event.peer, text="⚠️ Некорректная команда редактирования.")
        return

    event_id, field = raw.split("|", 1)
    field = field.strip()
    data = context.get_data()
    ctx_name = str(data.get("edit_event_ctx", "moderation"))

    edited_event_card = event_service.get_event_by_id(event_id, event.peer.id)
    if not edited_event_card:
        bot.messaging.send_message(peer=event.peer, text="⚠️ Мероприятие не найдено.")
        return

    if field == "tags":
        # редактируем теги через toggle UI
        all_tags = tag_service.get_all_tags()
        choices = choices_from_backend_tags(all_tags)

        selected_titles: set[str] = set()
        for t in edited_event_card.tags or []:
            if isinstance(t, dict) and t.get("title"):
                selected_titles.add(str(t["title"]))
            else:
                title = getattr(t, "title", None)
                if title:
                    selected_titles.add(str(title))

        context.update_data(
            {
                "edit_event_id": event_id,
                "all_tags": [{"id": t.id, "title": t.title} for t in all_tags],
                "edit_tag_keys": list(selected_titles),
                "edit_current_tag_keys": list(selected_titles),
            }
        )
        context.set_state(EventEditState.tags)

        _send_tags_toggle_ui(
            peer=event.peer,
            title="Редактирование направления: выбери направления, которым соответствует мероприятие, затем нажми «Готово»:",
            choices=choices,
            selected_keys=set(selected_titles),
            footer_buttons=[
                Button(
                    media_id="event_menu_edit",
                    value=f"{event_id}|{ctx_name}",
                    label="⬅️ К редактированию мероприятия",
                )
            ],
        )
        return

    if field == "gosb_id":
        # редактируем госб через UI
        # вызываем клавиатуру выбора тербанка, дальше она сама вызовет
        # выбор ГОСБ и определит, редактируем мы или создаем мероприятие

        default_tb_id = edited_event_card.terbank.id
        default_gosb_id = edited_event_card.gosb.id

        context.update_data(
            {
                "edit_event_id": event_id,
                "edit_event_ctx": ctx_name,
                "edit_field": "gosb_id",
                "edit_current_tb_id": default_tb_id,
                "edit_current_gosb_id": default_gosb_id,
            }
        )

        send_tb_select_menu(
            peer=event.peer,
            terbank_service=terbank_service,
            context=context,
            is_edit=True,
            event_id=event_id,
            ctx_name=ctx_name,
            default_tb_id=default_tb_id,
            default_gosb_id=default_gosb_id,
        )
        return

    context.update_data({"edit_event_id": event_id, "edit_field": field})
    context.set_state(EventEditState.wait_value)

    field_label = _get_field_label(field)
    field_current_value = _get_current_value(edited_event_card, field)

    if field == "event_organizers":
        bot.messaging.send_message(
            peer=event.peer,
            text="Пока не реализовано",
            interactive_media_groups=build_back_to_event_editing_keyboard(
                event_id=event_id,
                ctx_name=ctx_name,
            ),
        )
        return
    if field == "date":
        hint = "*Введи дату в формате* `dd.mm.yyyy`, например: `20.02.2002`"
    elif field == "time":
        hint = "*Введи время в формате* `HH:MM`, например: `10:30`"
    elif field == "participation_limit":
        hint = _err("participation_limit", "*Введи число от {min} до {max}*")
    elif field == "hours":
        hint = _err("hours", "*Введи число от {min} до {max}*")
    elif field == "title":
        hint = _err("title", "*Введи от {min_len} до {max_len} символов*")
    elif field == "code":
        hint = _err("verification_code", "*От {min_len} до {max_len} символов, буквы и цифры*")
    elif field == "description":
        hint = _err("description", "*Максимум {max_len} символов*")
    else:
        hint = "*Введи новое значение.* Чтобы очистить поле - отправь: «-»"

    bot.messaging.send_message(
        peer=event.peer,
        text=f" ✍️ *Редактирование*: _{field_label}_\n\n*Текущее значение*: {field_current_value}\n\n{hint}",
        interactive_media_groups=build_back_to_event_editing_keyboard(
            event_id=event_id,
            ctx_name=ctx_name,
        ),
    )


@events_rt.message(state=EventEditState.wait_value)
@bot.di
def event_edit_value_step(
    message: UpdateMessage,
    context: FSMContext,
    event_service: EventService,
):
    delete_prev_message(bot, message)

    data = context.get_data()
    event_id: str = str(data.get("edit_event_id", ""))
    field: str = str(data.get("edit_field", ""))
    ctx_name = str(data.get("edit_event_ctx", "moderation"))

    if not event_id or not field:
        logger.error("expected values for event_id and field, got '%s' and '%s'", event_id, field)
        bot.messaging.send_message(
            peer=message.peer,
            text="⚠️ Сессия редактирования сброшена. Открой мероприятие заново.",
        )
        context.set_state(None)
        return

    raw = message.message.text_message.text.strip()
    value: str | int = "" if raw == "-" else raw

    if field == "event_organizers":
        bot.messaging.send_message(
            peer=message.peer,
            text="Пока не реализовано.",
            interactive_media_groups=build_back_to_event_editing_keyboard(
                event_id=event_id,
                ctx_name=ctx_name,
            ),
        )
        context.set_state(None)
        return

    if field == "hours":
        try:
            value = validate_hours(value)
        except ValueError as e:
            bot.messaging.send_message(
                peer=message.peer,
                text=f"⚠️ {e}",
                interactive_media_groups=build_back_to_event_editing_keyboard(
                    event_id=event_id,
                    ctx_name=ctx_name,
                ),
            )
            return

    if field == "participation_limit":
        try:
            value = validate_participation_limit(value)
        except ValueError as e:
            bot.messaging.send_message(
                peer=message.peer,
                text=f"⚠️ {e}",
                interactive_media_groups=build_back_to_event_editing_keyboard(
                    event_id=event_id,
                    ctx_name=ctx_name,
                ),
            )
            return

    if field == "code":
        try:
            value = validate_verification_code(value)
        except ValueError as e:
            bot.messaging.send_message(
                peer=message.peer,
                text=f"⚠️ {e}",
                interactive_media_groups=build_back_to_event_editing_keyboard(
                    event_id=event_id,
                    ctx_name=ctx_name,
                ),
            )
            return

    if field == "title":
        try:
            value = validate_title(value)
        except ValueError as e:
            bot.messaging.send_message(
                peer=message.peer,
                text=f"⚠️ {e}",
                interactive_media_groups=build_back_to_event_editing_keyboard(
                    event_id=event_id,
                    ctx_name=ctx_name,
                ),
            )
            return

    if field == "date":
        try:
            date_obj = validate_date(str(value))
        except ValueError:
            bot.messaging.send_message(
                peer=message.peer,
                text=(
                    "⚠️ Я не смог распознать дату.\n"
                    "Пожалуйста, введи дату в формате `dd.mm.yyyy`, например: `20.02.2002`:"
                ),
                interactive_media_groups=build_back_to_event_editing_keyboard(
                    event_id=event_id,
                    ctx_name=ctx_name,
                ),
            )
            return

        current_event_card = event_service.get_event_by_id(event_id, message.peer.id)
        if current_event_card is None:
            logger.error(
                "expected event card while editing date, got None for event_id=%s",
                event_id,
            )
            bot.messaging.send_message(
                peer=message.peer,
                text="⚠️ Мероприятие не найдено. Открой мероприятие заново.",
            )
            context.set_state(None)
            return

        current_dt = current_event_card.event.date_time
        new_dt = current_dt.replace(
            year=date_obj.year,
            month=date_obj.month,
            day=date_obj.day,
        )

        field = "date_time"
        value = new_dt.strftime("%Y-%m-%dT%H:%M:%SZ")

    if field == "time":
        try:
            time_obj = validate_time(str(value))
        except ValueError:
            bot.messaging.send_message(
                peer=message.peer,
                text=(
                    "⚠️ Я не смог распознать время.\n"
                    "Пожалуйста, введи в формате `HH:MM`, например: `10:30`:"
                ),
                interactive_media_groups=build_back_to_event_editing_keyboard(
                    event_id=event_id,
                    ctx_name=ctx_name,
                ),
            )
            return

        current_event_card = event_service.get_event_by_id(event_id, message.peer.id)
        if current_event_card is None:
            logger.error(
                "expected event card while editing time, got None for event_id=%s",
                event_id,
            )
            bot.messaging.send_message(
                peer=message.peer,
                text="⚠️ Мероприятие не найдено. Открой мероприятие заново.",
            )
            context.set_state(None)
            return

        current_dt = current_event_card.event.date_time
        new_dt = current_dt.replace(
            hour=time_obj.hour,
            minute=time_obj.minute,
            second=0,
            microsecond=0,
        )

        field = "date_time"
        value = new_dt.strftime("%Y-%m-%dT%H:%M:%SZ")

    payload = {field: value}

    updated = event_service.update_event(
        event_id=event_id,
        messenger_id=message.peer.id,
        payload=payload,  # type(value) in (int, str)
    )
    context.set_state(None)

    _send_event_edit_menu(
        message.peer,
        event_id=event_id,
        event_service=event_service,
        note="⚠️ Не удалось обновить поле." if not updated else "✅ Поле обновлено.",
        event_obj=event_service.get_event_by_id(event_id, message.peer.id),
        show_organizer=True,
        ctx_name=ctx_name,
    )

--- services/bot/core/handlers/validators.py ---
import re
from datetime import datetime

from core.config.validation_config import validation_cfg

def _get_rule(field: str, rule: str, default=None):
    return validation_cfg.get(f"event.{field}.{rule}", default)


def validate_title(text: str):
    min_len = _get_rule("title", "min_length", 4)
    max_len = _get_rule("title", "max_length", 20)

    if len(text) < min_len or len(text) > max_len:
        raise ValueError(f"event_name must be in a {min_len} and {max_len}, got {len(text)}")
    return text


def validate_description(text: str):
    max_len = _get_rule("description", "max_length", 20)

    if len(text) > max_len:
        raise ValueError(f"event_name must be lower than {max_len}, got {len(text)} symbols")
    return text


def validate_date(text: str):
    """Парсинг даты с поддержкой разделителей . - _ . Возвращает date."""
    formats = []
    for first_gap in ". -_":
        for second_gap in ". -_":
            formats.append(f"%d{first_gap}%m{second_gap}%Y")

    date_obj = None
    for fmt in formats:
        try:
            date_obj = datetime.strptime(text.strip(), fmt).date()
            break
        except ValueError:
            continue

    if date_obj is None:
        raise ValueError(f"unsupported date format in date {text!r}")
    return date_obj


def validate_time(text: str):
    time_obj = None
    for fmt in ["%H:%M", "%H %M", "%H-%M", "%H_%M"]:
        try:
            time_obj = datetime.strptime(text.strip(), fmt).time()
            break
        except ValueError:
            continue

    if time_obj is None:
        raise ValueError(f"unsupported time format in time {text!r}")
    return time_obj


def validate_hours(text):
    """Валидация часов: 1-2 цифры, диапазон 1-18 (из YAML)."""
    if not isinstance(text, (int, str)):
        raise ValueError(f"expected type str or int for hours amount, got {type(text)!r}")

    text = str(text).strip()
    if not re.fullmatch(r"^[0-9]{1,2}$", text):
        raise ValueError(f"expected <= 2 long digit sequence for hours amount, got {text!r}")

    hours = int(text)
    min_val = _get_rule("hours", "min", 1)
    max_val = _get_rule("hours", "max", 18)

    if hours < min_val or hours > max_val:
        raise ValueError(
            f"hours value must be positive integer between {min_val} and {max_val}, got {hours!r}"
        )
    return hours


def validate_verification_code(text: str):
    if not re.fullmatch(r"^[A-Za-zА-Яа-я0-9]+$", text):
        raise ValueError(f"unsupported verification code format: {text!r}")

    min_len = _get_rule("verification_code", "min_length", 4)
    max_len = _get_rule("verification_code", "max_length", 20)

    if len(text) < min_len or len(text) > max_len:
        raise ValueError(
            f"verification code length must be between {min_len} and {max_len}, got {len(text)}"
        )
    return text


def validate_participation_limit(value):
    try:
        limit = int(value)
    except (TypeError, ValueError) as e:
        raise ValueError(
            f"expected int or numeric string for participation limit, got {value!r}, which has type {type(value)!r}"
        ) from e

    min_val = _get_rule("participation_limit", "min", 1)
    max_val = _get_rule("participation_limit", "max", 1000)

    if limit < min_val:
        raise ValueError(f"participation limit must be positive integer, got {limit!r}")
    if limit > max_val:
        raise ValueError(f"participation limit is too high: {limit}")
    return limit

вот список файлов репозитория:

.env.example
.gitignore
Makefile
docker-compose.yml
docs/TODO.md
docs/buisiness-model.md
docs/devops-instruction.md
docs/domain-model.md
docs/event-access-policy.md
docs/integration-notes.md
docs/specification.pdf
policy.yml
pyproject.toml
pyrightconfig.json
seed.yml
services/ai/Dockerfile
services/ai/main.py
services/ai/requirements.txt
services/ai/russian_trusted_root_ca_pem.crt
services/ai/src/api/__init__.py
services/ai/src/api/controllers/__init__.py
services/ai/src/api/controllers/assistant_controller.py
services/ai/src/api/controllers/health_controller.py
services/ai/src/api/controllers/index_controller.py
services/ai/src/api/middlewares/auth.py
services/ai/src/api/models/__init__.py
services/ai/src/api/models/request.py
services/ai/src/api/models/response.py
services/ai/src/api/routes.py
services/ai/src/app.py
services/ai/src/clients/embedding_model.py
services/ai/src/clients/language_model.py
services/ai/src/clients/vector_store.py
services/ai/src/config/env.py
services/ai/src/db/database.py
services/ai/src/repositories/__init__.py
services/ai/src/repositories/conversation.py
services/ai/src/run.py
services/ai/src/services/__init__.py
services/ai/src/services/assistant.py
services/ai/src/services/indexing.py
services/ai/src/services/intents.py
services/ai/src/utils/__init__.py
services/ai/src/utils/asterisks.py
services/ai/src/utils/logger.py
services/backend/.dockerignore
services/backend/Dockerfile
services/backend/bootstrap/modules.go
services/backend/cmd/root.go
services/backend/docs/docs.go
services/backend/docs/swagger.json
services/backend/docs/swagger.yaml
services/backend/go.mod
services/backend/go.sum
services/backend/internal/adapters/assistant/assistant_client.go
services/backend/internal/adapters/assistant/module.go
services/backend/internal/adapters/assistant/requests.go
services/backend/internal/adapters/policy/module.go
services/backend/internal/adapters/policy/policy_matrix.go
services/backend/internal/adapters/reports/collection_mapper.go
services/backend/internal/adapters/reports/collection_schema.go
services/backend/internal/adapters/reports/module.go
services/backend/internal/adapters/reports/report_query.go
services/backend/internal/adapters/reports/report_repo.go
services/backend/internal/adapters/reports/report_schema.go
services/backend/internal/adapters/volunteering/event_card/event_card_mappers.go
services/backend/internal/adapters/volunteering/event_card/event_card_query.go
services/backend/internal/adapters/volunteering/event_card/event_card_schema.go
services/backend/internal/adapters/volunteering/event_card/module.go
services/backend/internal/adapters/volunteering/event_organizer_card/module.go
services/backend/internal/adapters/volunteering/event_organizer_card/organizer_card_mappers.go
services/backend/internal/adapters/volunteering/event_organizer_card/organizer_card_query.go
services/backend/internal/adapters/volunteering/event_organizer_card/organizer_card_schema.go
services/backend/internal/adapters/volunteering/event_organizers/event_organizer_mappers.go
services/backend/internal/adapters/volunteering/event_organizers/event_organizer_repo.go
services/backend/internal/adapters/volunteering/event_organizers/event_organizer_schema.go
services/backend/internal/adapters/volunteering/event_organizers/module.go
services/backend/internal/adapters/volunteering/event_tags/event_tag_mappers.go
services/backend/internal/adapters/volunteering/event_tags/event_tag_repo.go
services/backend/internal/adapters/volunteering/event_tags/event_tag_schema.go
services/backend/internal/adapters/volunteering/event_tags/module.go
services/backend/internal/adapters/volunteering/events/event_config_provider.go
services/backend/internal/adapters/volunteering/events/event_mappers.go
services/backend/internal/adapters/volunteering/events/event_repository.go
services/backend/internal/adapters/volunteering/events/event_schema.go
services/backend/internal/adapters/volunteering/events/module.go
services/backend/internal/adapters/volunteering/gosbs/gosb_mappers.go
services/backend/internal/adapters/volunteering/gosbs/gosb_repo.go
services/backend/internal/adapters/volunteering/gosbs/gosb_schema.go
services/backend/internal/adapters/volunteering/gosbs/module.go
services/backend/internal/adapters/volunteering/module.go
services/backend/internal/adapters/volunteering/participation/module.go
services/backend/internal/adapters/volunteering/participation/participation_repo.go
services/backend/internal/adapters/volunteering/participation/participation_schema.go
services/backend/internal/adapters/volunteering/participation_card/participation_card_mapper.go
services/backend/internal/adapters/volunteering/participation_card/participation_card_schema.go
services/backend/internal/adapters/volunteering/project/module.go
services/backend/internal/adapters/volunteering/project/project_mappers.go
services/backend/internal/adapters/volunteering/project/project_repository.go
services/backend/internal/adapters/volunteering/project/project_schema.go
services/backend/internal/adapters/volunteering/request_card/module.go
services/backend/internal/adapters/volunteering/request_card/request_card_mappers.go
services/backend/internal/adapters/volunteering/request_card/request_card_query.go
services/backend/internal/adapters/volunteering/request_card/request_card_schema.go
services/backend/internal/adapters/volunteering/requests/module.go
services/backend/internal/adapters/volunteering/requests/request_mappers.go
services/backend/internal/adapters/volunteering/requests/request_repo.go
services/backend/internal/adapters/volunteering/requests/request_schema.go
services/backend/internal/adapters/volunteering/tags/module.go
services/backend/internal/adapters/volunteering/tags/tag_mapper.go
services/backend/internal/adapters/volunteering/tags/tag_schema.go
services/backend/internal/adapters/volunteering/tags/tags_repository.go
services/backend/internal/adapters/volunteering/terbank_moderators/module.go
services/backend/internal/adapters/volunteering/terbank_moderators/terbank_moderator_schema.go
services/backend/internal/adapters/volunteering/terbank_moderators/terbank_moderators_mappers.go
services/backend/internal/adapters/volunteering/terbank_moderators/terbank_moderators_repo.go
services/backend/internal/adapters/volunteering/terbanks/module.go
services/backend/internal/adapters/volunteering/terbanks/terbank_repo.go
services/backend/internal/adapters/volunteering/terbanks/terbank_schema.go
services/backend/internal/adapters/volunteering/terbanks/terbanks_mappers.go
services/backend/internal/adapters/volunteering/volunteer/module.go
services/backend/internal/adapters/volunteering/volunteer/volunteer_mapper.go
services/backend/internal/adapters/volunteering/volunteer/volunteer_repository.go
services/backend/internal/adapters/volunteering/volunteer/volunteer_schema.go
services/backend/internal/adapters/volunteering/volunteer_card/module.go
services/backend/internal/adapters/volunteering/volunteer_card/volunteer_card_mapper.go
services/backend/internal/adapters/volunteering/volunteer_card/volunteer_card_query.go
services/backend/internal/adapters/volunteering/volunteer_card/volunteer_card_schema.go
services/backend/internal/adapters/volunteering/volunteer_tags/module.go
services/backend/internal/adapters/volunteering/volunteer_tags/volunteer_tag_mappers.go
services/backend/internal/adapters/volunteering/volunteer_tags/volunteer_tag_repo.go
services/backend/internal/adapters/volunteering/volunteer_tags/volunteer_tag_schema.go
services/backend/internal/config/env.go
services/backend/internal/config/module.go
services/backend/internal/config/seed_config.go
services/backend/internal/domain/assistant/ask_response.go
services/backend/internal/domain/assistant/assistant_answer.go
services/backend/internal/domain/assistant/assistent_client.go
services/backend/internal/domain/assistant/assistent_errors.go
services/backend/internal/domain/assistant/assistent_service.go
services/backend/internal/domain/assistant/event_rules.go
services/backend/internal/domain/assistant/module.go
services/backend/internal/domain/assistant/rules.go
services/backend/internal/domain/event_bus.go
services/backend/internal/domain/policy/auth.go
services/backend/internal/domain/policy/context.go
services/backend/internal/domain/policy/module.go
services/backend/internal/domain/policy/permission_resolver.go
services/backend/internal/domain/policy/policy_matrix.go
services/backend/internal/domain/policy/role_resolver.go
services/backend/internal/domain/reports/module.go
services/backend/internal/domain/reports/report.go
services/backend/internal/domain/reports/report_collection.go
services/backend/internal/domain/reports/report_errors.go
services/backend/internal/domain/reports/report_query.go
services/backend/internal/domain/reports/report_repository.go
services/backend/internal/domain/reports/report_service.go
services/backend/internal/domain/volunteering/event_card/allowed_actions.go
services/backend/internal/domain/volunteering/event_card/event_card.go
services/backend/internal/domain/volunteering/event_card/event_card_errors.go
services/backend/internal/domain/volunteering/event_card/event_card_page.go
services/backend/internal/domain/volunteering/event_card/event_card_page_view.go
services/backend/internal/domain/volunteering/event_card/event_card_query.go
services/backend/internal/domain/volunteering/event_card/event_card_service.go
services/backend/internal/domain/volunteering/event_card/event_card_view.go
services/backend/internal/domain/volunteering/event_card/event_view.go
services/backend/internal/domain/volunteering/event_card/filter.go
services/backend/internal/domain/volunteering/event_card/module.go
services/backend/internal/domain/volunteering/event_card/page_meta.go
services/backend/internal/domain/volunteering/event_organizer_card/card_service.go
services/backend/internal/domain/volunteering/event_organizer_card/event_organizer_card.go
services/backend/internal/domain/volunteering/event_organizer_card/module.go
services/backend/internal/domain/volunteering/event_organizer_card/organizer_query.go
services/backend/internal/domain/volunteering/event_organizers/event_organizer.go
services/backend/internal/domain/volunteering/event_organizers/event_organizer_repository.go
services/backend/internal/domain/volunteering/event_organizers/event_organizer_service.go
services/backend/internal/domain/volunteering/event_organizers/event_roganizer_errors.go
services/backend/internal/domain/volunteering/event_organizers/module.go
services/backend/internal/domain/volunteering/event_organizers/organizer_request.go
services/backend/internal/domain/volunteering/event_tags/event_tag.go
services/backend/internal/domain/volunteering/event_tags/event_tag_errors.go
services/backend/internal/domain/volunteering/event_tags/event_tag_repository.go
services/backend/internal/domain/volunteering/event_tags/event_tag_service.go
services/backend/internal/domain/volunteering/event_tags/module.go
services/backend/internal/domain/volunteering/events/create_event_request.go
services/backend/internal/domain/volunteering/events/event.go
services/backend/internal/domain/volunteering/events/event_config.go
services/backend/internal/domain/volunteering/events/event_errors.go
services/backend/internal/domain/volunteering/events/event_events.go
services/backend/internal/domain/volunteering/events/event_repository.go
services/backend/internal/domain/volunteering/events/event_service.go
services/backend/internal/domain/volunteering/events/event_state.go
services/backend/internal/domain/volunteering/events/event_update.go
services/backend/internal/domain/volunteering/events/module.go
services/backend/internal/domain/volunteering/events/verificaion_result.go
services/backend/internal/domain/volunteering/gosbs/actual_gosb.go
services/backend/internal/domain/volunteering/gosbs/gosb.go
services/backend/internal/domain/volunteering/gosbs/gosb_errors.go
services/backend/internal/domain/volunteering/gosbs/gosb_repository.go
services/backend/internal/domain/volunteering/gosbs/gosb_service.go
services/backend/internal/domain/volunteering/gosbs/module.go
services/backend/internal/domain/volunteering/module.go
services/backend/internal/domain/volunteering/participation_card/participation_card.go
services/backend/internal/domain/volunteering/participation_card/participation_card_query.go
services/backend/internal/domain/volunteering/participations/module.go
services/backend/internal/domain/volunteering/participations/participation.go
services/backend/internal/domain/volunteering/participations/participation_errors.go
services/backend/internal/domain/volunteering/participations/participation_repository.go
services/backend/internal/domain/volunteering/participations/participation_service.go
services/backend/internal/domain/volunteering/projects/module.go
services/backend/internal/domain/volunteering/projects/project.go
services/backend/internal/domain/volunteering/projects/project_errors.go
services/backend/internal/domain/volunteering/projects/project_repository.go
services/backend/internal/domain/volunteering/projects/project_service.go
services/backend/internal/domain/volunteering/registry/create_result.go
services/backend/internal/domain/volunteering/registry/create_type.go
services/backend/internal/domain/volunteering/registry/module.go
services/backend/internal/domain/volunteering/registry/registry_service.go
services/backend/internal/domain/volunteering/registry/resource_activator.go
services/backend/internal/domain/volunteering/request_card/allowed_actions.go
services/backend/internal/domain/volunteering/request_card/filter.go
services/backend/internal/domain/volunteering/request_card/module.go
services/backend/internal/domain/volunteering/request_card/page_meta.go
services/backend/internal/domain/volunteering/request_card/request_card.go
services/backend/internal/domain/volunteering/request_card/request_card_errors.go
services/backend/internal/domain/volunteering/request_card/request_card_page.go
services/backend/internal/domain/volunteering/request_card/request_card_query.go
services/backend/internal/domain/volunteering/request_card/request_card_service.go
services/backend/internal/domain/volunteering/request_card/request_card_view.go
services/backend/internal/domain/volunteering/requests/module.go
services/backend/internal/domain/volunteering/requests/request.go
services/backend/internal/domain/volunteering/requests/request_errors.go
services/backend/internal/domain/volunteering/requests/request_repository.go
services/backend/internal/domain/volunteering/requests/request_service.go
services/backend/internal/domain/volunteering/requests/request_state.go
services/backend/internal/domain/volunteering/roles/module.go
services/backend/internal/domain/volunteering/roles/role_service.go
services/backend/internal/domain/volunteering/tags/actual_tag.go
services/backend/internal/domain/volunteering/tags/module.go
services/backend/internal/domain/volunteering/tags/tag.go
services/backend/internal/domain/volunteering/tags/tag_errors.go
services/backend/internal/domain/volunteering/tags/tag_repository.go
services/backend/internal/domain/volunteering/tags/tag_service.go
services/backend/internal/domain/volunteering/terbank_moderators/module.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator_errors.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator_repository.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator_service.go
services/backend/internal/domain/volunteering/terbanks/actual_terbank.go
services/backend/internal/domain/volunteering/terbanks/module.go
services/backend/internal/domain/volunteering/terbanks/terbank.go
services/backend/internal/domain/volunteering/terbanks/terbank_errors.go
services/backend/internal/domain/volunteering/terbanks/terbank_repository.go
services/backend/internal/domain/volunteering/terbanks/terbank_service.go
services/backend/internal/domain/volunteering/volunteer_card/module.go
services/backend/internal/domain/volunteering/volunteer_card/volunteer_card.go
services/backend/internal/domain/volunteering/volunteer_card/volunteer_card_query.go
services/backend/internal/domain/volunteering/volunteer_card/volunteer_card_service.go
services/backend/internal/domain/volunteering/volunteer_tags/module.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag_errors.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag_repository.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag_service.go
services/backend/internal/domain/volunteering/volunteers/module.go
services/backend/internal/domain/volunteering/volunteers/volunteer.go
services/backend/internal/domain/volunteering/volunteers/volunteer_errors.go
services/backend/internal/domain/volunteering/volunteers/volunteer_repository.go
services/backend/internal/domain/volunteering/volunteers/volunteer_service.go
services/backend/internal/drivers/http/middlewares/auth_middleware.go
services/backend/internal/drivers/http/middlewares/context_middleware.go
services/backend/internal/drivers/http/middlewares/module.go
services/backend/internal/drivers/http/middlewares/policy_middleware.go
services/backend/internal/drivers/http/middlewares/role_middleware.go
services/backend/internal/drivers/http/middlewares/test_auth_middleware.go
services/backend/internal/drivers/http/v1/assistant/assistant_controller.go
services/backend/internal/drivers/http/v1/assistant/assistant_requests.go
services/backend/internal/drivers/http/v1/assistant/assistant_routes.go
services/backend/internal/drivers/http/v1/health/health_controller.go
services/backend/internal/drivers/http/v1/health/health_routes.go
services/backend/internal/drivers/http/v1/reports/event_report_builder.go
services/backend/internal/drivers/http/v1/reports/report_controller.go
services/backend/internal/drivers/http/v1/reports/report_request.go
services/backend/internal/drivers/http/v1/reports/report_routes.go
services/backend/internal/drivers/http/v1/routes.go
services/backend/internal/drivers/http/v1/swagger/swagger_routes.go
services/backend/internal/drivers/http/v1/volunteering/event_card/event_card_controller.go
services/backend/internal/drivers/http/v1/volunteering/event_card/event_card_request.go
services/backend/internal/drivers/http/v1/volunteering/event_organizers/event_organizer_controller.go
services/backend/internal/drivers/http/v1/volunteering/event_organizers/event_organizer_requests.go
services/backend/internal/drivers/http/v1/volunteering/event_tags/event_tag_controller.go
services/backend/internal/drivers/http/v1/volunteering/event_tags/event_tag_request.go
services/backend/internal/drivers/http/v1/volunteering/events/event_controller.go
services/backend/internal/drivers/http/v1/volunteering/gosbs/gosb_controller.go
services/backend/internal/drivers/http/v1/volunteering/participation/participation_controller.go
services/backend/internal/drivers/http/v1/volunteering/participation/participation_requests.go
services/backend/internal/drivers/http/v1/volunteering/registry/registry_controller.go
services/backend/internal/drivers/http/v1/volunteering/request_card/request_card_controller.go
services/backend/internal/drivers/http/v1/volunteering/request_card/request_card_request.go
services/backend/internal/drivers/http/v1/volunteering/requests/request_contoller.go
services/backend/internal/drivers/http/v1/volunteering/requests/request_requests.go
services/backend/internal/drivers/http/v1/volunteering/tags/tags_controller.go
services/backend/internal/drivers/http/v1/volunteering/terbank_moderators/terbank_moderator_controller.go
services/backend/internal/drivers/http/v1/volunteering/terbank_moderators/terbank_moderator_requests.go
services/backend/internal/drivers/http/v1/volunteering/terbanks/terbank_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer/volunteer_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer/volunteer_requests.go
services/backend/internal/drivers/http/v1/volunteering/volunteer_card/volunteer_card_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer_tag/volunteer_tag_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer_tag/volunteer_tag_requests.go
services/backend/internal/drivers/http/v1/volunteering/volunteering_routes.go
services/backend/internal/drivers/seeders/admin_seeder.go
services/backend/internal/drivers/seeders/gosb_seeder.go
services/backend/internal/drivers/seeders/seeders.go
services/backend/internal/drivers/seeders/tag_seeder.go
services/backend/internal/drivers/seeders/terbank_seeder.go
services/backend/main.go
services/backend/migrations/000001_volunteer_table.down.sql
services/backend/migrations/000001_volunteer_table.up.sql
services/backend/migrations/000002_tag_table.down.sql
services/backend/migrations/000002_tag_table.up.sql
services/backend/migrations/000003_volunteer_tag_table.down.sql
services/backend/migrations/000003_volunteer_tag_table.up.sql
services/backend/migrations/000004_volunteer_role.down.sql
services/backend/migrations/000004_volunteer_role.up.sql
services/backend/migrations/000005_event_table.down.sql
services/backend/migrations/000005_event_table.up.sql
services/backend/migrations/000006_project_table.down.sql
services/backend/migrations/000006_project_table.up.sql
services/backend/migrations/000007_request_table.down.sql
services/backend/migrations/000007_request_table.up.sql
services/backend/migrations/000008_event_tag_table.down.sql
services/backend/migrations/000008_event_tag_table.up.sql
services/backend/migrations/000009_event_creator.down.sql
services/backend/migrations/000009_event_creator.up.sql
services/backend/migrations/000010_terbank_table.down.sql
services/backend/migrations/000010_terbank_table.up.sql
services/backend/migrations/000011_terbank_moderator_table.down.sql
services/backend/migrations/000011_terbank_moderator_table.up.sql
services/backend/migrations/000012_gosb_table.down.sql
services/backend/migrations/000012_gosb_table.up.sql
services/backend/migrations/000013_change_role.down.sql
services/backend/migrations/000013_change_role.up.sql
services/backend/migrations/000014_rename_terbank_id.down.sql
services/backend/migrations/000014_rename_terbank_id.up.sql
services/backend/migrations/000015_organizer_table.down.sql
services/backend/migrations/000015_organizer_table.up.sql
services/backend/migrations/000016_organizer_role.down.sql
services/backend/migrations/000016_organizer_role.up.sql
services/backend/migrations/000017_participation_table.down.sql
services/backend/migrations/000017_participation_table.up.sql
services/backend/migrations/000018_event_hours.down.sql
services/backend/migrations/000018_event_hours.up.sql
services/backend/migrations/000019_report_table.down.sql
services/backend/migrations/000019_report_table.up.sql
services/backend/migrations/000020_many_reports.down.sql
services/backend/migrations/000020_many_reports.up.sql
services/backend/migrations/000021_gosp_to_gosb.down.sql
services/backend/migrations/000021_gosp_to_gosb.up.sql
services/backend/migrations/000022_participation_limit.down.sql
services/backend/migrations/000022_participation_limit.up.sql
services/backend/migrations/000023_participation_count.down.sql
services/backend/migrations/000023_participation_count.up.sql
services/backend/pkg/bearer_parser.go
services/backend/pkg/excel_service.go
services/backend/pkg/gin_binding.go
services/backend/pkg/gorm.go
services/backend/pkg/gorm_logger.go
services/backend/pkg/gorm_paginate_simple.go
services/backend/pkg/http.go
services/backend/pkg/logger.go
services/backend/pkg/migrate_postgres.go
services/backend/pkg/module.go
services/backend/pkg/request_handler.go
services/bot/Dockerfile
services/bot/chain.pem
services/bot/core/__init__.py
services/bot/core/bot_kit/__init__.py
services/bot/core/bot_kit/fsm/__init__.py
services/bot/core/bot_kit/fsm/context.py
services/bot/core/bot_kit/fsm/state.py
services/bot/core/bot_kit/fsm/storage/__init__.py
services/bot/core/bot_kit/fsm/storage/base.py
services/bot/core/bot_kit/fsm/storage/in_memory.py
services/bot/core/bot_kit/router.py
services/bot/core/clients/__init__.py
services/bot/core/clients/admin.py
services/bot/core/clients/ai_assistant.py
services/bot/core/clients/base.py
services/bot/core/clients/event.py
services/bot/core/clients/report.py
services/bot/core/clients/request.py
services/bot/core/clients/tag.py
services/bot/core/clients/terbank.py
services/bot/core/clients/user.py
services/bot/core/config/__init__.py
services/bot/core/config/bootstrap.py
services/bot/core/config/settings.py
services/bot/core/config/validation_config.py
services/bot/core/dependencies/__init__.py
services/bot/core/dependencies/admin.py
services/bot/core/dependencies/base.py
services/bot/core/dependencies/context.py
services/bot/core/dependencies/di.py
services/bot/core/dependencies/event.py
services/bot/core/dependencies/registry.py
services/bot/core/dependencies/report.py
services/bot/core/dependencies/request.py
services/bot/core/dependencies/tag.py
services/bot/core/dependencies/terbank.py
services/bot/core/dependencies/user.py
services/bot/core/handlers/__init__.py
services/bot/core/handlers/admin.py
services/bot/core/handlers/ai.py
services/bot/core/handlers/event.py
services/bot/core/handlers/events_filters.py
services/bot/core/handlers/events_pages.py
services/bot/core/handlers/events_ui.py
services/bot/core/handlers/main.py
services/bot/core/handlers/moderation.py
services/bot/core/handlers/noop.py
services/bot/core/handlers/points.py
services/bot/core/handlers/profile.py
services/bot/core/handlers/report.py
services/bot/core/handlers/request.py
services/bot/core/handlers/validators.py
services/bot/core/handlers/view_by_uuid.py
services/bot/core/handlers/volunteer_home.py
services/bot/core/markups/__init__.py
services/bot/core/markups/ai.py
services/bot/core/markups/event.py
services/bot/core/markups/events_filters.py
services/bot/core/markups/gosb.py
services/bot/core/markups/inline.py
services/bot/core/markups/pagination.py
services/bot/core/markups/points.py
services/bot/core/markups/profile.py
services/bot/core/markups/request.py
services/bot/core/markups/tag.py
services/bot/core/markups/tags.py
services/bot/core/markups/terbank.py
services/bot/core/schemas/__init__.py
services/bot/core/schemas/event.py
services/bot/core/schemas/gosb.py
services/bot/core/schemas/participation.py
services/bot/core/schemas/request.py
services/bot/core/schemas/tag.py
services/bot/core/schemas/terbank.py
services/bot/core/schemas/user.py
services/bot/core/services/__init__.py
services/bot/core/services/admin.py
services/bot/core/services/base.py
services/bot/core/services/event.py
services/bot/core/services/file.py
services/bot/core/services/registry/__init__.py
services/bot/core/services/registry/base.py
services/bot/core/services/registry/registry.py
services/bot/core/services/report.py
services/bot/core/services/requests.py
services/bot/core/services/tag.py
services/bot/core/services/terbank.py
services/bot/core/services/user.py
services/bot/core/states/__init__.py
services/bot/core/utils/__init__.py
services/bot/core/utils/date_time.py
services/bot/core/utils/events_filter.py
services/bot/core/utils/fsm/__init__.py
services/bot/core/utils/fsm/base.py
services/bot/core/utils/fsm/state.py
services/bot/core/utils/fsm/storage/__init__.py
services/bot/core/utils/fsm/storage/base.py
services/bot/core/utils/fsm/storage/memory.py
services/bot/core/utils/logger.py
services/bot/core/utils/messages.py
services/bot/core/utils/tags_toggle_ui.py
services/bot/core/utils/text_wrap.py
services/bot/main.py
services/bot/requirements.txt
services/bot/validation.yml
templates.yml
utils/format
utils/prettify_logs.py
validation.yml

напиши, пожалуйста, план проверки изменений, введённых в этих коммитах. также вот файл validatoin.yml:

--- validation.yml ---
event:
  title:
    min_length: 3
    max_length: 96
  description:
    max_length: 1024
  hours:
    min: 1
    max: 18
  participation_limit:
    min: 1
    max: 1000
  verification_code:
    min_length: 4
    max_length: 20

Editor is loading...
Leave a Comment