Как я сделал платформу коротких видео в Telegram Mini Apps

В этой статье расскажу о проекте и технических особенностях создания подобных приложений.

В разработке использовались:

Node.js — для бекенда бота.
Angular 17 — для фронтенда самого сервиса.
PHP Laravel — для тестового бекенда самого сервиса.
Rust — для обработки видео и бекенда сервиса.

О проекте

Недавно я поехал на море с другом. Во время пути мы узнали, что YouTube собираются блокировать (или замедлять). Тут в моей голове родилась идея: почему бы не сделать видеохостинг в Telegram? Я поспорил с другом, что сделаю его по дороге, но ноутбук сел. Поэтому мы договорились, что я сделаю это за день (спойлер: получилось, но кривовато).

Плеер для больших видео

Первым делом накидал дизайн за полчаса. Вот как должен был изначально выглядеть плеер.

Я решил разместить все кнопки управления (кроме паузы) и информирования (кроме прогресс-бара) сверху. Иконки внизу будут только отвлекать пользователя и мешать ему нормально скроллить время видео.

В общем, тут всё стандартно для площадок подобного плана

Но от идеи делать полноценный видеохостинг я решил отказаться, как минимум по следующим причинам:

  1. Telegram вставляет ваше Mini app в iframe и отключает возможность разворачивать медиа в полноэкранный режим. Я долго пытался это хоть как-то компенсировать, но при любом решении проблема остаётся. Исключение составляют iOS-устройства, у которых есть встроенный плеер, позволяющий развернуть видео на полный экран, но это всё равно выглядит не совсем удобно.

  2. Вес видео: полноценное видео (например, подкаст) длительностью несколько часов может весить и 100 ГБ, и 500 ГБ, а это нужно где-то хранить. Даже если сильно урезать качество видео, на обработку одного видео будет уходить слишком много времени и ресурсов, поэтому это нецелесообразно. Читал на Хабре про хранилище ВКонтакте вроде на 1 эксабайт. Я прикинул стоимость подходящего диска, и в сумме их цена вышла в районе 20 миллиардов рублей). Это ещё без учёта всех остальных комплектующих, обслуживания и т.д., что делает проект слишком дорогим в перспективе. Таких инвестиций пока что нет), хотя я и не думаю, что нам понадобится такое хранилище, в любом случае, нужно много места. Расскажу о том, как решил проблему с хранилищем, чуть позже.

Поэтому я решил сфокусироваться на коротких видео. Все сейчас бегут в Telegram (и всегда бежали), YouTube работает с помехами. На площадках, кроме Instagram, YouTube и TikTok, никто толком не сидит. Репутация даже у крупных компаний в этом сегменте не очень, так что же делать Саше, который делает мемы, и Сереже, который накладывает на себя маску в TikTok, благодаря чему набирает миллионы просмотров? Что делать всем, кто убежал в Telegram (а это вообще все)? Правильно, продолжать пользоваться им. Только есть одна проблема: у Telegram нет рекомендаций для контента вообще, а новых подписчиков как-то привлекать нужно. Да и формат коротких видео уже прижился, поэтому мы подарим этому миру рекомендательную систему контента в Telegram.

Есть только одна проблема: Дурова посадили, но это не повод сдаваться. Да и вряд ли эта политическая игра сильно повлияет на Telegram, по крайней мере, остается на это надеяться. Я верю, что Павел справится, и вы верьте тоже.

Кстати, если вы слышали, что у Telegram есть план на случай задержания Дурова, то это вброс. Когда-то этот мем про Дмитрия Буткина уже проскакивал, интересно, когда?)

Теперь поговорим про техническую сторону коротких видео

Как это часто бывает, одни разработчики вставляют палки в колёса другим.

Как это часто бывает, одни разработчики вставляют палки в колёса другим.

Начнём с первой серьёзной проблемы, с которой я столкнулся. По умолчанию браузер блокирует автовоспроизведение видео (особенно на iOS), и это никак не обойти. Нельзя просто запросить разрешение на автовоспроизведение у пользователя — вот такая браузерная политика. Но, как вы знаете, одна из фишек коротких видео заключается в том, что их можно листать бесконечно, не нажимая каждый раз на кнопку «Плей», чтобы запустить его. Поэтому нужно как-то решать эту проблему.

Есть одна фишка: она заключается в том, что видео должно воспроизводиться только после взаимодействия пользователя с ним.

Изначально я попробовал просто добавить тег autoplay.

И это даже сработало, но только на ПК и некоторых Android-устройствах, и то только потому, что пользователь попадал на страницу с видео после нажатия, например, на его карточку. Если переходить по ссылке (чуть позже расскажу, как работают ссылки в TG-ботах и Mini Apps), то мы ловим баг: видео не воспроизводится. Решил попробовать через TS.

// Воспроизводим выбранное видео
if (this.selectVideoId >= 0 && this.selectVideoId < this.videoElements.length) {
  const video = this.videoElements[this.selectVideoId];
  video.play().catch((error) => {});
  this.isPlaying = true;
}

Когда видео находится на экране у пользователя, оно должно начинать воспроизводиться, но, опять же, политика браузеров запрещает даже это. Ответа на вопрос, как же решить эту проблему, в интернете я так и не нашёл. Пришлось копать глубже. Вот краткий вывод: нужно, чтобы воспроизведение начиналось только после взаимодействия пользователя с экраном. При этом alert не подходит — пользователь должен именно прикоснуться к блоку с медиа-контентом (например, к div, в котором находится видео). Достаточно одного нажатия, чтобы можно было воспроизводить все видео, которые уже инициализированы в HTML. Но самое главное: «можно воспроизводить все видео, которые уже инициализированы в HTML», то есть если мы будем подгружать видео, они не будут воспроизводиться, придётся нажимать на плей на каждом новом видео. Заставлять пользователя нажимать на экран каждый раз, когда он пролистал, например, 10 видео, и код подгружает ещё 10, было бы муторно, и никто бы даже пользоваться этим не стал. Поэтому я в конце концов пришёл к следующему решению, которое позволяет решить все вышеописанные проблемы, в том числе позволяет листать видео и тем самым воспроизводить следующие, а также переходить по ссылке и сразу получать автовоспроизведение (Нет). Есть только один момент: пользователю нужно один раз коснуться экрана плеера, чтобы появился звук. Вот код, который работает для моей задачи.

Добавляем тег muted, потому что он позволяет автоматически воспроизводить видео, только без звука. Добавляем loop, чтобы зациклить видео, и playsinline, чтобы плеер отображался так, как мы этого захотим, без всяких заморочек системы, типа встроенного плеера.

Теперь каждый раз, когда пользователь касается экрана (а касается он, когда листает видео) или нажимает на него, тег muted устанавливается в false, и voilà, звук появляется на всех видео, включая ещё не прогруженные.

@HostListener('document:click', ['$event'])
  @HostListener('document:touchstart', ['$event'])
  onClickOrTouch(event: Event) {
    this.videoElements.forEach(video => {
      if (video) {
        video.muted = false;
        this.muted = video.muted;
        if(this.muted){
          return;
        }
      }
    });
  }

Вот как выглядит плеер теперь:

Отлично, проблему с воспроизведением решили, но теперь есть другая проблема, которая касается не юзабилити, а денег и ресурсов серверов.

Постобработка видео, или как сжать видос в 10 раз

Хотя короткие видео и весят куда меньше больших, они всё равно имеют некоторый вес (30 секунд вертикального видео, снятого на iPhone 14 Pro Max, весит чуть больше 50 МБ). А ещё их будет много, очень много:

Ежедневно на TikTok публикуется 34 миллиона видеороликов.

Если прикинуть, то под 1 миллион роликов нужно условно 50 000 000 МБ или 50 ТБ. Многовато, конечно, для приколов и танцев, но что поделаешь — нужно как-то решать эту проблему. Самый лучший вариант — это постобработка.

Что может помочь с этим? Я решил использовать ffmpeg. Он позволяет обрабатывать видео и работает через CLI, поэтому проблем с ним возникнуть не должно. К тому же, он бесплатный и open-source.

Какие пункты я учёл в обработке:

Кодеки — на всех устройствах используются разные кодеки, хоть и есть наиболее популярные. Всё равно то, что воспроизводится на одном телефоне, может не воспроизводиться на другом, поэтому нужно выбрать достаточно сбалансированный кодек, который позволит воспроизводить видео на всех устройствах, не будет требовать много памяти и не ухудшит качество видео. По этим параметрам я выбрал H.264. Он обеспечивает высокую степень сжатия видео, что позволяет значительно уменьшить размер файлов без заметной потери качества. Это особенно важно для потоковой передачи и хранения видео, так как снижает требования к пропускной способности сети и объему хранилища. Также поддерживается большинством современных устройств и платформ.

Расширение — понятное дело, что нужно брать самое популярное, а это MP4.

Разрешение и соотношение сторон — горизонтальные видео — это 16:9, а вертикальные — 9:16. Например, горизонтальное видео, снятое на телефон, может иметь соотношение сторон 16:9 и разрешение 3840×2160 пикселей. Чем больше разрешение, тем больше пикселей в видео, чем больше пикселей, тем больше вес видео. Если видео больше 1920×1080, то будем уменьшать его до этого значения; если разрешение меньше, то не будем его трогать. После того как мы разобрались с разрешением, нам нужно привести видео к формату 9:16, то есть поменять 1920×1080 местами, получится 1080×1920. Но это растянет видео, и оно потеряет начальное качество, поэтому будем просто добавлять чёрные полосы к 1920×1080, чтобы получить соотношение 9:16. Видео, которые уже имеют соотношение сторон 9:16, просто меняем разрешение.

Переворот видео — некоторые устройства (например, iPhone) снимают всегда горизонтальные видео, даже когда вы снимаете вертикальное. iPhone просто добавляет метаданные поворота. В конечном итоге, вы видите вертикальное видео 2160×3840, а на деле оно будет 3840×2160, просто перевёрнутое плеером. Это нужно учесть в обработке, так как мы будем менять кодек видео и его расширение. Лучшим решением будет перевернуть видео после всей обработки (если в метаданных есть данные о повороте видео). В ffmpeg это делает команда transpose=ПОВОРОТ, где ПОВОРОТ — это число от 0 до 3, которое обозначает поворот на 90 градусов.

0: Поворот на 90 градусов по часовой стрелке (эквивалентно transpose=clock).

1: Поворот на 90 градусов против часовой стрелки (эквивалентно transpose=cclock).

2: Поворот на 90 градусов по часовой стрелке и зеркальное отражение (эквивалентно transpose=clock_flip).

3: Поворот на 90 градусов против часовой стрелки и зеркальное отражение (эквивалентно transpose=cclock_flip).

Битрейт видео — определяет количество данных, используемых для кодирования видео. Более высокий битрейт может улучшить качество видео, но увеличивает размер файла, а это нам не нужно. Будем использовать среднее значение 1500 килобит в секунду (кбит/с).

Коэффициент постоянного битрейта CRF — это параметр, который контролирует качество видео при использовании кодека H.264. Значение 23 считается средним качеством. Меньшие значения дают лучшее качество и больший размер файла, а большие значения — более низкое качество и меньший размер файла.

B-кадры или двухнаправленные кадры — являются одним из типов кадров в видео, сжатом с использованием современных кодеков, таких как H.264, H.265 (HEVC), MPEG-2 и других. В отличие от I-кадров (intra-coded frames) и P-кадров (predictive frames), B-кадры используют информацию как из предыдущих, так и из последующих кадров для повышения эффективности сжатия. B-кадры предсказывают части изображения на основе движения объектов между предыдущим и следующим кадрами. Компенсация движения помогает определить разницу и закодировать только изменения, а не полный кадр. B-кадры могут интерполировать (оценивать) данные на основе окружающих кадров. Это особенно полезно, когда движение объектов плавное, и изменения между кадрами минимальны.

Обработка аудио — будем использовать аудиокодек AAC (Advanced Audio Coding). Это стандартный аудиокодек, обеспечивающий высокое качество звука при более низких битрейтах. Битрейт аудио установим на 64 килобита в секунду (кбит/с). Битрейт в 64 кбит/с используется для аудио среднего качества, например, в голосовых записях.

Теперь посмотрим на команду для ffmpeg с учётом наших настроек:

Для перевернутых видео

ffmpeg -vf "transpose=0, scale=x" -c:v libx264 -b:v 1500k -preset medium -bf 1 -crf 23 -c:a aac -b:a 64k 

Для остальных видео

ffmpeg -vf "scale=::force_original_aspect_ratio=decrease,pad=::(ow-iw)/2:(oh-ih)/2" -c:v libx264 -b:v 1500k -preset medium -bf 1 -crf 23 -c:a aac -b:a 64k 

Теперь посмотрим на код на Rust, который будет делать всё это за нас.

Получение разрешения видео

// Функция для получения разрешения видео
fn get_video_resolution(input_path: &Path) -> std::result::Result<(i32, i32), Box> {
    // Выводим путь к файлу для отладки
    eprintln!("Path {:?}", input_path);

    // Выполняем команду `ffprobe` для получения разрешения видео
    let output: Output = Command::new("ffprobe")
        .args(&[
            "-v", "error",                        // Подавляем вывод сообщений об ошибках, кроме фатальных
            "-select_streams", "v:0",             // Выбираем первый видеопоток
            "-show_entries", "stream=width,height", // Показываем только ширину и высоту потока
            "-of", "csv=s=x:p=0",                 // Форматируем вывод как CSV, используя 'x' в качестве разделителя
            "-i", input_path.to_str().unwrap()    // Указываем путь к входному файлу
        ])
        .output()?;  // Выполняем команду и получаем вывод, результат обрабатывается с помощью оператора ?

    // Преобразуем байтовый вывод команды в строку
    let res = String::from_utf8(output.stdout)?;
    // Выводим полученное разрешение для отладки
    eprintln!("Resolution {}", res);

    // Разделяем строку на части, используя 'x' как разделитель (например, "1920x1080" -> ["1920", "1080"])
    let mut parts: Vec<&str> = res.trim().split('x').collect();

    // Если после разбиения не получилось двух частей (ширина и высота), возвращаем ошибку
    if parts.len() != 2 {
        return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Invalid resolution format")));
    }

    // Преобразуем строки ширины и высоты в целые числа
    let width: i32 = parts[0].parse()?;
    let height: i32 = parts[1].parse()?;

    // Возвращаем кортеж с шириной и высотой видео
    Ok((width, height))
}

Проверка, повернуто ли видео

// Функция для проверки наличия информации о вращении видео
fn has_video_rotation(file_path: &str) -> bool {
    // Выполняем команду `ffprobe` для получения информации о тегах потока, связанных с вращением
    let output = Command::new("ffprobe")
        .args(&[
            "-v", "error",                        // Подавляем вывод сообщений об ошибках, кроме фатальных
            "-select_streams", "v:0",             // Выбираем первый видеопоток
            "-show_entries", "stream_tags=rotate", // Показываем только тег 'rotate', который указывает на вращение
            "-of", "default=nw=1:nk=1",           // Форматируем вывод в виде строки, без заголовков
            file_path                              // Указываем путь к входному файлу
        ])
        .output()
        .expect("Failed to execute command");  // Если команда не удалась, программа завершится с ошибкой

    // Преобразуем байтовый вывод команды в строку и удаляем возможные пробелы по краям
    let output_str = std::str::from_utf8(&output.stdout).unwrap_or("").trim();

    // Пытаемся преобразовать строку в целое число (которое указывает угол вращения)
    if let Ok(rotation) = output_str.parse::() {
        // Если значение угла не равно 0, значит, видео имеет вращение
        rotation != 0
    } else {
        // Если не удалось преобразовать строку в число, считаем, что вращения нет
        false
    }
}

Теперь пример кода для обработки видео

// Добавляем параметры командной строки для ffmpeg
command
    // Добавляем параметр для фильтра видео, который изменяет размер и добавляет черные полосы
    .arg("-vf")
    .arg(format!(
        "scale={}:{}:force_original_aspect_ratio=decrease," // Изменяем размер видео до новых размеров, сохраняя оригинальное соотношение сторон
        "pad={}:{}:(ow-iw)/2:(oh-ih)/2",                   // Добавляем черные полосы, чтобы заполнить оставшееся пространство до новых размеров
        new_width, new_height,                             // Новая ширина и высота видео
        new_width, new_height                              // Размеры области для заполнения (выравнивание по центру)
    ))
    // Указываем кодек для видеопотока
    .arg("-c:v")
    .arg("libx264")                                      // Используем кодек libx264 для кодирования видео в формате H.264
    // Указываем битрейт для видеопотока
    .arg("-b:v")
    .arg("1500k")                                       // Битрейт видео установлен на 1500 килобит в секунду
    // Указываем предустановку для кодека
    .arg("-preset")
    .arg("medium")                                      // Выбираем предустановку 'medium', сбалансированную между качеством и скоростью кодирования
    // Указываем количество кадров между последовательными кадрами (опции для улучшения качества видео)
    .arg("-bf")
    .arg("1")                                           // Используем 1 кадр с обратным поиском (B-frames) для улучшения качества сжатия
    // Указываем коэффициент постоянной скорости качества (CRF) для кодека
    .arg("-crf")
    .arg("23")                                          // Устанавливаем значение CRF на 23, что обеспечивает хорошее качество при разумном размере файла
    // Указываем кодек для аудиопотока
    .arg("-c:a")
    .arg("aac")                                         // Используем кодек AAC для кодирования аудио
    // Указываем битрейт для аудиопотока
    .arg("-b:a")
    .arg("64k")                                         // Битрейт аудио установлен на 64 килобита в секунду
    // Указываем путь к выходному файлу
    .arg(output_path.to_str().unwrap())                  // Путь к файлу, в который будет сохранен результат
    // Передаем стандартный поток ошибок основной программе
    .stderr(Stdio::inherit());                           // Выводим стандартный поток ошибок на стандартный поток ошибок текущего процесса

Думаю, вы поняли, как делается постобработка, поэтому давайте посмотрим на результаты работы этого кода в продакшене. Для примера возьмём запись экрана с iPhone 14 Pro Max, которую я показывал раньше. Вот ссылка на видео: https://vimeo.com/1003546084? share=copy. Исходный вес файла — 77 МБ, длительность — 37 секунд.

Размер видео до обработки

Размер видео до обработки

Загружаем видео на платформу

Загрузка видео

Загрузка видео

Получаем сообщение от бота. Обработка видео не заняла больше 2-х минут.

18b56263e392182aa472d791a23715fd.png

Ура! Видео уменьшилось примерно в 11,5 раз; в среднем оно уменьшается в 10 раз от исходного размера, если оно не было обработано до загрузки.

Размер видео после обработки

Размер видео после обработки

Отлично, теперь мы можем загружать куда больше видео. Это лишь пример постобработки. Думаю, его всегда можно улучшить в разы и достичь ещё лучших результатов, но сейчас не об этом.

Немного про канал на платформе и взаимодействие с Telegram Mini Apps API

Для того чтобы подключить сайт к Telegram и сделать из него mini app, нужно просто добавить в тег вашего сайта поверх всех остальных скриптов. Вот официальная документация от Telegram.

Далее создадим сервис в Angular 17, который позволит нам общаться с Telegram.

export class TelegramService {
  private window;
  tg;
  user: any;
  // @ts-ignore
  constructor(@Inject(DOCUMENT) private _document) {
    this.window = this._document.defaultView;
    this.tg = this.window.Telegram.WebApp;
    this.user = this.GetUser();
    this.tg.disableVerticalSwipes();
    if(this.tg.version < 7.7 && this.tg.platform !== 'weba'  && this.tg.platform !== 'web'){
      alert('Советуем обновить телеграм, иначе, приложение может работать некоректно!')
    }
  }
}

Теперь можем использовать всё, что даёт нам Telegram: информацию о чате, пользователе и т.д., а также функции, описанные в документации.

Зачем я проверяю версию клиентского приложения Telegram? Потому что начиная с версии 7.7 нам доступен метод disableVerticalSwipes(), который позволяет приложению не скрываться при скролле. Это очень важно, так как на нашей платформе это основная механика (свайп видео). Как использовать этот метод? Очень просто: вызываем this.tg.disableVerticalSwipes();.

export class TelegramService {
  private window;
  tg;
  user: any;
  // @ts-ignore
  constructor(@Inject(DOCUMENT) private _document) {
    this.window = this._document.defaultView;
    this.tg = this.window.Telegram.WebApp;
    this.user = this.GetUser();
    if(this.tg.version < 7.7 && this.tg.platform !== 'weba'  && this.tg.platform !== 'web'){
      alert('Советуем обновить телеграм, иначе, приложение может работать некоректно!')
    } else {
      this.tg.disableVerticalSwipes(); // отключаем свайпы
    }
  }
}

Чтобы просто открыть приложение во всю высоту*, можно использовать this.tg.expand(); .

Как работать с ссылками Telegram Mini Apps?

Первое, что нам нужно сделать, это зарегистрировать Mini App и подключить его к боту.

Заходим в чат с BotFather, пишем /newapp, выбираем бота, к которому хотим привязать наше приложение, затем вводим название для приложения, далее вводим краткое описание приложения, загружаем фото 640×360 для предпросмотра (я залил просто логотип с градиентом), затем отправляем ссылку на наше приложение. Теперь можно задать username для приложения, и вуаля — получаем ссылку на наш Mini App: https://t.me/Test_bot/test_app_name, по которой можно открыть его в любой части мессенджера. Это очень важный этап, если его пропустить, то приложение в боте по ссылке будет открываться только у вас.

Как передать стартовые параметры в приложение?

Например, чтобы открыть видео, нужно просто указать ?startapp=параметры. Вот как будет выглядеть полноценная ссылка: https://t.me/ВАШ_БОТ/ИМЯ_ПРИЛОЖЕНИЯ?startapp=ПАРАМЕТРЫ. Параметры можно передавать, например, указывая идентификатор параметра и кодируя его значение в base64, например multi_${btoa(video.id.toString())}.

Как получить стартовые параметры в нашем фронтенде?

Параметры, переданные в startapp, передаются в initData и initDataUnsafe в поле start_param. Вот пример кода, который извлекает эти параметры:

// Проверяем, есть ли параметр start_param в объекте initDataUnsafe и начинается ли он с 'multi_'
if (this.telegram.tg.initDataUnsafe.start_param && 
    this.telegram.tg.initDataUnsafe.start_param.startsWith('multi_')) {
  
  // Извлекаем часть строки после 'multi_' и декодируем её из base64
  const encodedId = this.telegram.tg.initDataUnsafe.start_param.substring(6); // Обрезаем 'multi_'
  const multi_id = atob(encodedId); // Декодируем из base64
  
  // Навигация к маршруту, связанному с multi_id
  this.router.navigate([`/multi/${multi_id}`]); 

} else {
  // Если параметр start_param отсутствует или не начинается с 'multi_', перенаправляем на основной маршрут
  this.router.navigate(['/main']); 
}

В Telegram API, есть почти всё, но всё таки

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

Ниже будет приведён код на Node.js, который демонстрирует взаимодействие с топиками. Я не буду включать код, который сохраняет ID топика и привязанного к нему пользователя в БД.

Когда пользователь, например, пишет в службу поддержки, нам нужно создавать новый топик в нашем чате для поддержки с именем пользователя и его ID. Так у нас будет появляться удобный чат с пользователем через бота.

async function createForumTopic(chatId, title) {
    try {
        const url = `https://api.telegram.org/bot${token}/createForumTopic`;
        const response = await axios.post(url, {
            chat_id: chatId, // ID чата в котором сидит служба поддержки
            name: title // Название топика, Имя пользователя и его Id, для удобства
        });

        if (response.data.ok) {
            return response.data.result;
        } else {
            throw new Error(response.data.description);
        }
    } catch (error) {
        console.error('Ошибка при создании топика:', error);
        throw error;
    }
}

Отлично, топик создан, сохраняем его ID — response.data.result.message_thread_id .

Теперь когда пользователь нажимает кнопку «Служба заботы», он может присылать нам сообщения, вот как это выглядит в коде.

// Проверяем, активен ли сервис поддержки
if (isCareServiceActive) {
    // Получаем идентификатор темы пользователя для текущего чата
    let topicId = await getUserTopic(msg.from.id);

    // Если идентификатор темы не найден, создаем новую тему
    if (!topicId) {
        // Формируем заголовок темы на основе данных пользователя
        const topicTitle = `${msg.from.first_name || ''} ${msg.from.last_name || ''} ${msg.from.username || ''} (${msg.from.id})`;

        try {
            // Создаем новую тему в форуме для поддержки
            const topic = await createForumTopic(careServiceChatId, topicTitle);
            // Получаем идентификатор созданной темы
            topicId = topic.message_thread_id;
            // Сохраняем идентификатор темы для пользователя
            await saveUserTopic(msg.from.id, topicId);
        } catch (error) {
            // В случае ошибки при создании темы выводим сообщение об ошибке
            console.error('Ошибка при создании топика:', error);
            await bot.sendMessage(chatId, 'Ошибка при создании запроса, пожалуйста, попробуйте позже.');
            return; // Прерываем выполнение функции, чтобы не отправлять сообщение
        }
    }

    // Отправляем сообщение в тему поддержки
    await bot.sendMessage(careServiceChatId, `Сообщение от пользователя ${msg.from.username || msg.from.first_name}: ${text}`, { message_thread_id: topicId });
}

Вот как это выглядит на скриншотах

f29b3353267863f52074f830ce527dab.png

Удобно? Даже очень. В принципе, всем советую перейти на это решение, если у вас есть поддержка в Telegram. Через бота можно также добавить кнопки к сообщениям, поэтому это Must Have.

Немного про платформу, что в итоге?

А в итоге есть весь базовый функционал, который должен быть на подобной площадке. Так как это Telegram Mini App, дизайн и механики в основном взяты из Telegram. Например, вместо лайков — реакции.

a9990b88ba733b13a46d3dcdf4ef4fd3.png

Канал в Multi создаётся путём добавления бота в ваш публичный Telegram-канал. Соответственно, они связаны: название, аватарка и прочее будут совпадать. При нажатии на кнопку «Подписаться» вы подписываетесь на канал в Multi, и всплывающее окно (alert) предложит вам открыть привязанный Telegram-канал.

953471c463eb8611c46bd4652dc51974.png

В общем, всё по красоте. Конечно, ещё могут быть баги, недоработки и то, что ещё можно улучшить, но в целом проект рабочий и закрывает основной функционал. Кому интересно попробовать в деле, вот ссылка на бота: https://t.me/Miracle_Multi_bot. Видео уже можно выкладывать, а также приглашать пользователей, но система рекомендаций пока что отключена; включим её, как запустим.

Ещё один момент

Надеюсь, всем понятно, что проект достаточно перспективный в нынешних обстоятельствах. Поэтому сильно заморачиваться над маркетингом смысла нет; нужно лишь простимулировать заинтересованность. В связи с этим появилось предложение: получите 15% пожизненно от просмотра рекламы пользователями, которых вы пригласили. Чтобы пригласить пользователя, нужно иметь канал на площадке. Зайдя в профиль канала, вы найдёте кнопку «Поделиться» — эта ссылка на ваш канал и будет реферальной.

Думаю, со временем придумаем что-нибудь совместное с Хабром), например лучшее образовательное видео по IT-тематике по хештегу #Хабр)).

Завершение

Довольно большая получилась статья, тем более если учитывать, что это моя первая статья. Не получилось вложить и 10% проделанной работы, но вроде самое интересное для вас я изложил. Спор, кстати, я проиграл. На всю разработку ушло чуть меньше месяца, но я работал без остановок, иногда спал и ел). Проект получился интересным и полезным (в каком-то смысле). Если увидите баги, недоработки или у вас есть предложения, пишите в комментариях. Также, если хотите узнать про какие-то моменты в разработке подробнее, тоже пишите в комментариях. Постараюсь публиковать новые статьи по мере возможности, так как сейчас переписываю бэк на Rust. Всем спасибо!

Если здесь есть люди, которые разбираются в вышеописанных вопросах лучше, чем я, то напишите свои замечания в комментарии. Думаю, всем будет полезно.

Habrahabr.ru прочитано 11956 раз