Введение
В рамках этой статьи мы создадим своего бота, способного переотправлять отправленные пользователем сообщения с учётом форматирования и медиа.
Тема ботов была изъезжена до дыр уже многими блогами, постами, блогпостами и прочими кодерами. Однако в большинстве своём это боты на условных Python/JS/PHP, в которых обычно можно сделать как получится и всё вроде как даже заработает. Я же в своё время (почти) ушел из этих языков в мир строгой типизации и объектной ориентированности Java, а позже и в Kotlin. На момент, когда мне было интересно написать своего первого бота, на рынке github из интересных присутствовала только библиотека pengrad/java-telegram-bot-api, однако лично для меня она имела один фатальный недостаток: она на тот момент полностью дублировала Telegram Bot API, то есть фактически являлась прокладкой, которая кроме взаимодействия через Java классы больше ничего не давала.
Так я пришел к идее написать свою библиотеку для Telegram Bot API. Первая покрывающая основной API версия заняла у меня месяц, но выходили обновления, пользователи предлагали идеи по улучшению, да и мне часто не нравилось делать какие-то вещи руками на постоянной основе. В итоге, библиотека развивается по сей день, там есть удобный API, свой DSL, но что главное - она не утратила свою исходную идею о том, чтобы строго типизировать работу с Telegram Bot API.
Хотелось бы сразу предупредить, что строгая типизация как основной столп библиотеки ведет к определенным нюансам в работе с ботом:
Частенько приходится вручную или с помощью специальных функций кастить типы. Пример - вы хотите обрабатывать все сообщения в одном блоке. В таком случае вы получите как входные данные тип Message, который сам по-себе владеет не очень большим количеством информации. Как итог надо будет использовать что-то вроде message.asContentMessage, чтобы превратить этот тип в ContentMessage
После обновления API (особенно на ломающих версиях) часть типов будет несоответствовать спецификации, и, следовательно, иногда приходится обрабатывать UnknownUpdate
Из-за колоссального размера кода имеются проблемы с документацией. Эта проблема решается нашим чатом в телеграме и обновлениями со всё большим включением документации, но на данный момент проблема всё ещё есть
Теперь, когда вы обо всём предупреждены, давайте начинать :)
Как вообще работают боты в Telegram
У ботов в телеграме есть масса ограничений. На красивые циферки очень часто можно посмотреть тут, но если вкратце (ссылка на BotFather, чтобы дальше не повторяться) (скорее всего, будет пополняться для расширения кругозора):
По-умолчанию бот не видит сообщения других пользователей в группах до прямого обращения к боту командой (это можно поменять через group privacy настройку в BotFather)
По-умолчанию боты не инлайновые. Инлайновые боты - это как бот для гифок, то есть когда вы вводите ник бота, добавляете пробел и он вам предлагает варианты. После нажатия на вариант отправляется сообщение в чат с контентом, предложенным ботом (тоже включается через настройку в BotFather)
Кроме того, после включения инлайн-режима для бота, нужно будет отдельно включить поддержку инлайн-режима для локаций, когда бот может выдать результаты на основе геолокации пользователя
В API сейчас нет способа получить историю чата. Никакого. Вообще. Можно разве что кешировать сообщения
В среднем же пайплайн разработки бота выглядит следующим образом:
Идея
Создание тестового бота через BotFather с помощью
/newbot
Кодинг, исправление ошибок
Хостинг
Итак, бот
Топаем к BotFather и создаём бота через
/newbot
Для простоты открываем этот шаблон на гитхабе и создаём из него свой проект
Далее можно проследовать в Readme шаблона и настроить проект, а можно просто начать кодить в вашем файле App.kt
. Итак, что же у нас есть из коробки:
suspend fun main(args: Array<String>) {
val bot = telegramBot(args.first()) // 1
val scope = CoroutineScope(Dispatchers.Default) // 2
bot.buildBehaviour(scope) { // 3
val me = getMe()
onCommand("start", requireOnlyCommandInMessage = true) {
reply(it, "Hello, I am ${me.firstName}")
}
}.join() // 4
}
Создаём бота. Можно заменить
args.first()
на явную передачу вашего токена, полученного при создании бота через BotFatherСоздание
CoroutineScope
. Почитать о корутинах в котлине можно тут, но если вкратце - они позволяют производить работу с ботом асинхронноНачало самого интересного. В коллбэке для
buildBehaviour
мы будем составлять логику нашего бота в этом туториалеЗаставляем нашу программу не заканчиваться после составления логики бота и его запуска
Теперь давайте заменим код в логике бота на следующий:
onContentMessage { // 1
execute( // 2
it.content.createResend(it.chat.id) // 3
)
}
Готово ? Давайте теперь разберёмся, что же тут происходит:
Данный метод позволяет нам отдельно получать сообщения с некоторым контентом: медиа, локации, текст и т.д.
execute
- основной метод всей библиотеки. Этот метод отправляет запрос в систему телеграма. Скорее всего, напрямую вы будете использовать его очень редко, но знать о нём полезноМагия с createResend заключается в том, что практически любой контент в телеграме может быть переотправлен. По этой причине в библиотеке были сделаны методы переотправки контента - они автоматически формируют запрос на отправку сообщения с тем же контентом
Теперь можно сделать что-то по-интересней. Давайте научимся реагировать на команды:
onCommand("start") { // 1
reply(it, "Привет, я пока что умею не так много, но скоро всему научусь!") // 2
}
onCommand("help") {
reply(it, "Отправьте мне любое сообщение, и я отвечу вам тем же?")
}
Этот коллбэк будет вызываться каждый раз при отправке
/start
, но важно знать, что он будет вызываться при наличии в сообщении только этой командыОтвечаем текстовым сообщением.
it
тут обозначает само сообщение
Ну и напоследок давайте попробуем переписать часть с onContentMessage
так, чтобы попутно сохранять части текстовых сообщений:
fun save(sources: List<TextSource>) {
// работа с сохранением частей текста
println(sources.makeString()) // 3
}
onContentMessage {
it.content.asTextContent() ?.let { content -> // 1
save(content.textSources) // 2
}
execute(it.content.createResend(it.chat.id))
}
asTextContent()
проверяет, является ли контент сообщения текстом и вызываетlet
, если даcontent.textSources
создаст списокTextSource
объектов, с этим списком потенциально можно:Сохранять (у
TextSource
есть сериализатор дляkotlinx.serialization
)Копировать, перемещать, модифицировать
Использовать для форматирования - сурсы могут быть превращены в стандартные текстовые части телеграма
makeString
создаст из спискаTextSource
текст, который будет виден пользователю без учета форматирования
Заключение
Итак, мы создали бота, который:
Умеет отвечать на простые команды
/start
и/help
Умеет переотправлять полученные сообщения отправителю
Отбирает текстовые сообщения и производит операции с их контентом
Далее остаётся только развивать бота насколько хватит фантазии. Например, можно выделить модули в функции/плагины, как я это сделал в своём PlaguBotе.
Для более подробной информации о проекте можно посмотреть его основную страницу, вики, проект с примерами и заглянуть в наш телеграм канал.