Автор: Денис Петраков — старший программист, выпускник курса «Node.js Developer»

Цель работы: реализовать бота для публикации в разные telegram-каналы через API. Пользователь бота может владеть несколькими каналами, но отправлять посты в них по расписанию. Идея в том, чтобы пользователь мог сначала добавлять каналы в бот, а потом создавать сообщения и назначать им дату и время отправки.

Ход выполнения

Выбор технологий сразу пал на Nest.JS из-за хорошей интеграции с TG и готовой шаблонной архитектурой. Далее выбор библиотек:

  • стандартный набор, который идет в комплекте с Nest (для управления env, для интеграции с DB и тд);
  • дополнительные: для интеграции с telegram (https://grammy.dev/, https://www.npmjs.com/package/@grammyjs/nestjs), для решения отложенных задач (https://docs-nestjs.netlify.app/techniques/task-scheduling);
  • в качестве БД была выбрана postgres, т.к. в документации к nest предлагалась она, а также она достаточно популярна в данный момент.

Была настроена интеграция с базой данных, которая крутится в Docker-контейнере. Далее был зарегистрирован telegram-бот и получен tg api key (https://core.telegram.org/api). Все это было размещено в .env.

С настройкой покончено, пришло время реализации.

Реализация

Сначала был зарегистрирован список команд бота, на которые он мог реагировать. При первом использовании бота пользователь добавлялся в БД и получал сообщение:

Проект «TG Autoposter на Nest.JS»
При повторном взаимодействии с ботом сообщение уже отличалось:

Проект «TG Autoposter на Nest.JS»

Интеграция telegram — пользователь — БД оказалась достаточно простой, поэтому я сразу накидал список команд и приступил к пошаговой реализации.

Проект «TG Autoposter на Nest.JS»
Затем было принято решение привязать к пользователю telegram-каналы. Для этого в БД появилась дополнительная сущность, также появились 2 новых обработчика команд: /add_channel, /delete_channel.

Далее начались проблемы. Дело в том, что библиотека @grammy/nest предоставляет декораторы, но обработка некоторых команд в боте и канале работают по-разному. На это стоит обратить внимание при обработке контекста.

Также всплыл подводный камень: в канале, который нужно подключить к боту, обязательно нужно писать сообщение от своего имени, чтобы можно было сделать привязку к пользователю. Возможно, было какое-то дополнительное решение, но на его поиск не хотелось тратить время. Также в настройках канала нужно дать боту права администратора и возможность отправлять сообщения.

Теперь, после добавления бота в канал, выдачи ему прав и отправки /add_channel появлялось сообщение:

Проект «TG Autoposter на Nest.JS»

Также в БД появилась связка пользователь — каналы.

Итак, реализация /start, /help была самой простой, а /add_channel, /delete_channel имела нюансы.

Пришло время реализовывать публикации. Сначала была заведена сущность в БД с постами. Далее добавлен обработчик команды /create_post. Поскольку идея была в том, чтобы создавать сущность поста пошагово, пришлось завести промежуточный синглтон, который бы позволил управлять текущим состоянием создания.

https://github.com/isitfall/tg-autoposter-otus/blob/main/src/telegram/telegram.post-creation-state.ts

В самих же обработчиках появилась привязка к шагам (https://github.com/isitfall/tg-autoposter-otus/blob/main/src/telegram/telegram.update.ts#L247-L470).
Получился следующий флоу: /create_post -> выбор канала -> ввод текста -> подтверждение -> публикация в нужный канал через grammyjs api -> запись сущности в БД к постам. Сам текст моментально появляется в канале.

Последним шагом реализации стало добавление отложенных постов.

Сам по себе флоу создания поста не сильно изменился, добавилось лишь промежуточное состояние. Сам по себе telegram не имеет календаря, равно как и возможности выбора даты (на момент написания софта). Поэтому было решено считывать текущее значение в формате iso8601 (https://www.bairesdev.com/tools/onlinedevtools/)

Проект «TG Autoposter на Nest.JS»
Если дата введена в корректном формате, то произойдет отложенная публикация, иначе — синхронная.

Также добавилась вспомогательная таблица, в которой хранились отложенные посты. Кроме того появился обработчик cron (https://docs-nestjs.netlify.app/techniques/task-schedulin). Этот инструмент позволяет совершать операции один раз в определенный промежуток времени.


Проект «TG Autoposter на Nest.JS»

https://github.com/isitfall/tg-autoposter-otus/blob/main/src/post/post-scheduler.service.ts#L64-L92

В итоге получился финальный флоу для отложенных публикаций: /create_post -> выбор канала -> ввод текста -> ввод даты -> подтверждение -> запись в вспомогательную таблицу -> ожидание нужного времени через Cron -> публикация в нужный канал через grammyjs api -> запись сущности в БД к постам. Текст появлялся в нужное время, т.к. записей было немного и нагрузка на сервер была минимальная.

Флоу создания публикации:

Проект «TG Autoposter на Nest.JS»
Проект «TG Autoposter на Nest.JS»

Результат

Проект «TG Autoposter на Nest.JS»