diff --git a/README.md b/README.md
index b06c0f9..053b213 100644
--- a/README.md
+++ b/README.md
@@ -151,7 +151,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho
## Версионирование
-- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.4.14`.
+- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.4.15`.
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
@@ -180,6 +180,13 @@ git tag -a vX.Y -m "Release vX.Y"
git push origin master --tags
```
+## Приветствие новых участников
+
+Бот отправляет фото с короткой персональной подписью (первая строка шаблона `welcome.text`),
+а затем одно сообщение со всем остальным текстом (`welcome.text` без первой строки +
+`welcome.text2` + `welcome.text3`) и кнопкой на канал. Так приветствие остаётся одним
+смысловым блоком, но не упирается в лимит подписи к фото (1024 символа).
+
## Пасхальное событие «Яички»
`/яички` работает только в сезон: по умолчанию с **1 апреля по православную Пасху**
diff --git a/admin/templates/messages/edit.html b/admin/templates/messages/edit.html
index ad22528..9d006cb 100644
--- a/admin/templates/messages/edit.html
+++ b/admin/templates/messages/edit.html
@@ -36,6 +36,12 @@
Лимит Telegram — {{ max_length }} символов. Теги подставляет рендерер бота; неизвестный тег — ошибка.
+ {% if template.key == 'welcome.text' %}
+
+ Первая строка этого шаблона уходит подписью к фото, остальное — одним сообщением
+ вместе с welcome.text2 и welcome.text3.
+
+ {% endif %}
diff --git a/bot/handlers/member_events.py b/bot/handlers/member_events.py
index 8caf68b..8e8916e 100644
--- a/bot/handlers/member_events.py
+++ b/bot/handlers/member_events.py
@@ -52,18 +52,39 @@ async def welcome_handler(event: ChatMemberUpdated) -> None:
builder = InlineKeyboardBuilder()
builder.button(text=welcome.MSG_WELCOME_BUTTON_CAPTION, url=welcome.MSG_WELCOME_BUTTON_URL)
+ markup = builder.as_markup()
- await event.bot.send_photo(
- chat_id=event.chat.id,
- photo=FSInputFile("images/welcome_image.jpg"),
- caption=render(await get_text("welcome.text"), {"username": user.username}),
+ caption, body = welcome.split_welcome(
+ await get_text("welcome.text"),
+ await get_text("welcome.text2"),
+ await get_text("welcome.text3"),
)
- await event.bot.send_message(
- chat_id=event.chat.id,
- text=await get_text("welcome.text2"),
- reply_markup=builder.as_markup(),
- )
- await event.bot.send_message(chat_id=event.chat.id, text=await get_text("welcome.text3"))
+ caption = render(caption, {"username": user.username})
+ body = render(body, {"username": user.username})
+
+ # Подпись к фото ограничена 1024 символами; если не влезла — уйдёт в тело.
+ if len(caption) > 1024:
+ body = caption + (f"\n\n{body}" if body else "")
+ caption = ""
+
+ if body:
+ await event.bot.send_photo(
+ chat_id=event.chat.id,
+ photo=FSInputFile("images/welcome_image.jpg"),
+ caption=caption or None,
+ )
+ await event.bot.send_message(
+ chat_id=event.chat.id,
+ text=body,
+ reply_markup=markup,
+ )
+ else:
+ await event.bot.send_photo(
+ chat_id=event.chat.id,
+ photo=FSInputFile("images/welcome_image.jpg"),
+ caption=caption or None,
+ reply_markup=markup,
+ )
@router.chat_member(ChatMemberUpdatedFilter(member_status_changed=LEAVE_TRANSITION))
diff --git a/common/texts/welcome.py b/common/texts/welcome.py
index eb4ef1e..e875655 100644
--- a/common/texts/welcome.py
+++ b/common/texts/welcome.py
@@ -36,3 +36,18 @@ MSG_WELCOME_TEXT3 = """❗️По игре:
MSG_WELCOME_BUTTON_CAPTION = "Канал"
MSG_WELCOME_BUTTON_URL = "https://t.me/+DQk8MyN2y6QzNTM6"
+
+
+def split_welcome(*parts: str) -> tuple[str, str]:
+ """Разделить приветствие на подпись к фото и одно общее сообщение.
+
+ Первая строка первого шаблона становится подписью (персональное приветствие),
+ всё остальное (включая ``text2``/``text3``) — одним сообщением по порядку.
+ """
+ if not parts:
+ return "", ""
+
+ caption, _, rest = parts[0].partition("\n")
+ body_parts = [rest.rstrip(), *parts[1:]]
+ body = "\n\n".join(part.strip() for part in body_parts if part and part.strip())
+ return caption.strip(), body
diff --git a/common/version.py b/common/version.py
index 621672c..7510ebe 100644
--- a/common/version.py
+++ b/common/version.py
@@ -7,4 +7,4 @@
from __future__ import annotations
-__version__ = "0.4.14"
+__version__ = "0.4.15"
diff --git a/tests/test_version.py b/tests/test_version.py
index 9491810..c50f862 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -7,4 +7,4 @@ from common import __version__
def test_version_is_set():
assert __version__
- assert __version__ == "0.4.14"
+ assert __version__ == "0.4.15"
diff --git a/tests/test_welcome_split.py b/tests/test_welcome_split.py
new file mode 100644
index 0000000..7b520a3
--- /dev/null
+++ b/tests/test_welcome_split.py
@@ -0,0 +1,34 @@
+"""Тесты сборки приветствия: подпись к фото + одно общее сообщение."""
+
+from __future__ import annotations
+
+from common.texts.welcome import split_welcome
+
+
+def test_first_line_is_caption_rest_joins_body() -> None:
+ caption, body = split_welcome(
+ "💠{username}, добро пожаловать в Dispel Evil!\n\n❗️Правила клана\n1. …",
+ "Часть 2",
+ "Часть 3",
+ )
+
+ assert caption == "💠{username}, добро пожаловать в Dispel Evil!"
+ assert body == "❗️Правила клана\n1. …\n\nЧасть 2\n\nЧасть 3"
+
+
+def test_without_newline_all_text_is_caption() -> None:
+ caption, body = split_welcome("Привет", "Второй", "Третий")
+
+ assert caption == "Привет"
+ assert body == "Второй\n\nТретий"
+
+
+def test_empty_parts_are_skipped() -> None:
+ caption, body = split_welcome("Привет\n\nТело", "", " ")
+
+ assert caption == "Привет"
+ assert body == "Тело"
+
+
+def test_no_parts() -> None:
+ assert split_welcome() == ("", "")