Перейти к содержанию

Передача результата пользователем

Ссылка прошлого урока ведёт к известному публичному курсу. Теперь добавим действие, с помощью которого человек сам передаёт этот курс другому читателю. Результат — полный модуль формирования share link и точное разделение между подготовкой сообщения, открытием выбора и подтверждённой отправкой. Личный учебный план, bearer token и raw initData в распространяемый результат не входят.

Слово «поделиться» скрывает несколько разных механизмов. Можно открыть готовую ссылку Telegram с текстом. Можно использовать shareMessage с подготовленным сервером сообщением. Можно обращаться к Bot API, но это уже отдельная серверная операция с секретом бота и правилами платформы. Мы начнём с ссылки: она подходит публичному курсу, не требует собственного вызова Bot API и оставляет выбор чата и отправку пользователю.

Что именно передаём

Для JavaScript результат состоит из названия курса и ссылки Main Mini App с course_javascript. Мы не передаём частоту занятий человека: другой читатель получает приглашение к публичному материалу, а не снимок чужого плана. Название берётся из собственного каталога, поэтому ввод формы не становится произвольной частью сообщения. Текст можно показать рядом с действием до перехода, чтобы человек понимал, чем собирается поделиться.

Уведомление «Вы успешно отправили сообщение» нельзя показывать сразу после создания URL. Это только подготовка строки. Даже открытое окно выбора чата не доказывает отправку: человек может отменить действие. В нашем базовом механизме нет обратного подтверждения доставки или чтения. Статус говорит «Открываем выбор в Telegram», если открытие вообще было вызвано, и не делает вывод о получателе. Такая точность сохраняет доверие к остальным сообщениям интерфейса.

Полный модуль share link

Модуль sharing.js использует createCourseLink из предыдущего урока. Имя бота задаётся владельцем в конфигурации и не выдумывается из Telegram ID. Share URL имеет официальный origin Telegram, а вложенный адрес построен нашим ограниченным генератором. Формат share links описывает помещение подготовленного URL и текста в выбранный чат.

import {createCourseLink} from './course-links.js';

const titles = new Map([
  ['javascript', 'JavaScript'],
  ['html-css', 'HTML и CSS'],
  ['markdown', 'Статический сайт из Markdown'],
  ['performance', 'Скорость сайта']
]);

export function createShareLink(botUsername, courseId) {
  const title = titles.get(courseId);
  if (!title) throw new Error('Unknown public course');
  const link = new URL('https://t.me/share/url');
  link.searchParams.set('url', createCourseLink(botUsername, courseId));
  link.searchParams.set('text', `Учебный курс: ${title}`);
  return link.href;
}

export function openShareInHost(host, shareLink) {
  const link = new URL(shareLink);
  if (link.origin !== 'https://t.me' || link.pathname !== '/share/url' ||
      !link.searchParams.has('url')) throw new Error('Invalid share link');
  let supported = false;
  try {
    supported = typeof host?.isVersionAtLeast === 'function' &&
      host.isVersionAtLeast('6.1') && typeof host.openTelegramLink === 'function';
  } catch { supported = false; }
  if (!supported) return false;
  try { host.openTelegramLink(link.href); return true; }
  catch { return false; }
}

OpenShareInHost вызывается только из действия человека и принимает ссылку, созданную createShareLink. Возвращённое true означает успешный вызов метода SDK, а не фактическую отправку. Fallback — обычный a с заранее созданным href. Обработчик click может отменить браузерный переход лишь тогда, когда openShareInHost вернул true. Если метод недоступен или отказал, стандартная ссылка остаётся рабочим способом перейти; не показывайте кнопку, которая молча ничего не делает.

Обновление выбранного курса

При изменении карточки href нужно обновить до следующего клика. Иначе текст экрана будет про Markdown, а сохранённая в DOM ссылка — про JavaScript. Создавайте её из текущего известного courseId в одном проходе отображения. Это навигационное действие не зависит от dirty личной формы и не должно сохранять план заодно. Если человек хочет передать собственный результат обучения, сначала понадобится отдельная модель того, что считается публичным результатом и кому его можно раскрывать.

Не вызывайте openShareInHost автоматически после успешного PUT. Сохранение плана и открытие диалога отправки — разные намерения. Человек мог изменить частоту занятий для себя и не хотел приглашать других. Явная подпись «Поделиться курсом» удобнее двусмысленного значка без доступного имени. В браузерном режиме ссылка также должна иметь понятный текст, а не скрытое событие на карточке целиком.

Когда нужен shareMessage

У Telegram есть shareMessage, доступный начиная с Bot API 8.0. Он принимает идентификатор PreparedInlineMessage, полученный через серверный метод savePreparedInlineMessage. Нельзя передать произвольный текст вместо идентификатора или получить нужный объект, положив bot token в клиент. Сервер выбирает target user из подтверждённой сессии и подготавливает известный публичный результат.

Prepared message имеет свой срок и разрешённые типы чатов. Для полноценного варианта понадобятся собственный endpoint подготовки, ограничение частоты, проверка получателя и обработка истечения. Наш архив не содержит реализованного endpoint или настоящего message ID. Callback shareMessage сообщает результат отправки по договору SDK; это не подтверждение того, что каждый получатель прочитал сообщение или начал курс. Эти события нельзя склеивать в одну метрику успеха.

Почему не sendData

Выбранный нами Main Mini App и прямые ссылки не дают универсального права отправлять сообщение от имени пользователя в текущий чат. Метод sendData имеет свой сценарий запуска через keyboard button и не является заменой нашей кнопки «поделиться». Если использовать его без учёта запуска, пример может работать в другом режиме и перестать работать именно в нашем. В серии режим фиксирован с первого урока; изменение способа открытия должно сопровождаться новым договором и ожидаемыми сценариями.

Имя текущего чата также не определяет адресата личного учебного плана. Некоторые launch поля зависят от режима и могут отсутствовать. Для публичного приглашения достаточно собственного курса и явного выбора человека. Не добавляйте автоматический обход чатов или сообщения всем участникам: это другая задача и другой продуктовый договор. Учебный модуль не получает список получателей вообще.

Запасная ссылка особенно полезна при открытии вне Telegram: читатель видит обычное действие документа и может сохранить адрес. Но интерфейс не обещает одинаковое окно на всех устройствах. После возвращения из внешнего перехода форма должна сохранять текущий draft в памяти, пока документ ещё жив. Если страница была полностью выгружена, это уже новое открытие с правилами сессии и восстановления. Сам share link не является механизмом сохранения локальных данных и не сообщает приложению, какое окно пользователь закрыл.

Ожидаемый результат

Для известного курса получается HTTPS share URL с двумя кодированными параметрами. Неизвестный courseId отклоняется до открытия. При отсутствии поддержки хоста остаётся обычная ссылка. Отмена пользователем не объявляется отправкой и не изменяет base или draft плана. Реальные ссылки, SDK, диалоги, Bot API и JavaScript здесь не выполнялись; имя и настройки бота ещё задаются владельцем.

Сохраните в документации именно эти ограничения. Тогда следующий разработчик не станет трактовать возвращённое true как событие покупки, регистрации или доставки. В следующем уроке вернёмся к собственному API и разберём более сложный случай: сеть оборвалась после отправки плана, а интерфейс пока не знает, произошла запись или нет. Для него потребуется такой же аккуратный язык результата и отдельный серверный договор повторов.

Оглавление серии