# Приглашение в группу через PM В desktop chatgui и Android кнопка «+» доступна в строке другого участника и в шапке личной беседы. Она открывает один диалог с фиксированным получателем и выбором группы, в которой отправитель состоит. Группа не выбирается автоматически. После нажатия «Пригласить» клиент создаёт свежую ссылку через `invite_build`, ожидает подтверждения регистрации ключа и отправляет PM с `ct="invite"`. Зашифрованное тело: `[group_name:UTF8][NUL][полная ссылка:UTF8]`, без конечного NUL. Снимок для GUI содержит полную ссылку в `data` и название в `tags.group_name`. GUI добавляет заголовок «Приглашение в группу «название»» из перевода интерфейса; язык отправителя не фиксирует язык сообщения у получателя. Успех означает сохранение сообщения и исходящей задачи в одной транзакции. Доставку, повтор и шифрование обеспечивает существующий механизм DM. После успешной отправки открывается личная беседа с получателем. Срок действия ключа — **10 минут с момента создания**, а не получения PM. Он указан в диалогах отправки и подключения. Поздняя offline-доставка может принести уже просроченную ссылку; в этом случае нужно новое приглашение. Ссылка не содержит время истечения, поэтому клиент показывает длительность, без обратного отсчёта и вычисления оставшегося времени. Нажатие на `utun://` в сообщении запускает существующий JOIN и показывает его результат. Если клиент уже состоит в группе, открывается эта группа. Ссылка приглашения отображается одной строкой с многоточием; нажатие и копирование используют полный адрес. В Android долгое нажатие копирует ссылку, в desktop выделение сокращённой строки копирует ссылку целиком без запуска JOIN. Обычные текстовые PM с вручную вставленными ссылками сохраняют текстовое отображение. ## Контракт операции `dm_invite_trampoline` выполняется в uasync-потоке. Запрос содержит `request_id`, ID получателя, выбранную группу и исходную общую группу для создания PM. Из шапки существующей PM исходная группа может быть пустой. На instance допускается одно ожидание регистрации приглашения в PM. Повтор во время ожидания возвращает BUSY. Закрытие диалога отменяет только соответствующий `request_id`; уже отправленное сообщение не отзывается. Отмена и остановка DM завершают принятый запрос ровно один раз. `CHAT_EVT_DM_INVITE_RESULT` содержит 28 байт: `[request_id:u64][result:i32][peer:u64][conv_id:u64]` в порядке байтов ядра. Desktop и Android сопоставляют результат с запросом и получателем. Ошибки регистрации или сохранения сообщения оставляют диалог открытым для повторной попытки. JOIN во вторую группу может использовать уже открытый транспорт общей группы. Поэтому обработчики JOIN_REQUEST и JOIN_READY явно планируют member_sync, а JOIN_READY обновляет кеш каналов до запуска синхронизации. ## Проверка и диагностика - `tests/test_dm_invite`: настоящая UDP-доставка ссылки в другую группу, JOIN, репликация участника и отправка группового сообщения после вступления; отмена, BUSY, неверная группа, rollback при ошибке commit и остановка DM; передача названия с Unicode, кавычками и переводом строки. - `tests/test_chat_join`, `tests/test_chat_join_e2e`, `tests/test_dm_e2e`: существующие сценарии регистрации ключей, JOIN и PM-доставки. - Qt `test_invite_links`: одна строка приглашения на разных ширинах, полный адрес при нажатии и копировании, переносы двух обычных ссылок, их полное содержимое при нажатии, выделение текста и выбор получателя кнопкой в строке участника. - Qt `test_emoji_playback`: перетаскивание ссылки не активирует подключение. Для сетевого теста подробности включаются через `UTUN_TEST_DEBUG=1`. В приложении полезны категории `dm=debug`, `chat_sync=debug`, `member_sync=debug`. Операция пишет request, peer, group, результат и TTL; сообщения диагностики новой операции не выводят саму invite-ссылку.