Как стать автором
Обновить

Эхо-бот для Telegram на Kotlin

Время на прочтение5 мин
Количество просмотров21K

Введение

В рамках этой статьи мы создадим своего бота, способного переотправлять отправленные пользователем сообщения с учётом форматирования и медиа.

Тема ботов была изъезжена до дыр уже многими блогами, постами, блогпостами и прочими кодерами. Однако в большинстве своём это боты на условных 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

  • Кодинг, исправление ошибок

  • Хостинг

Итак, бот

  1. Топаем к BotFather и создаём бота через /newbot

  2. Для простоты открываем этот шаблон на гитхабе и создаём из него свой проект

Далее можно проследовать в 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
}
  1. Создаём бота. Можно заменить args.first() на явную передачу вашего токена, полученного при создании бота через BotFather

  2. Создание CoroutineScope. Почитать о корутинах в котлине можно тут, но если вкратце - они позволяют производить работу с ботом асинхронно

  3. Начало самого интересного. В коллбэке для buildBehaviour мы будем составлять логику нашего бота в этом туториале

  4. Заставляем нашу программу не заканчиваться после составления логики бота и его запуска

Теперь давайте заменим код в логике бота на следующий:

onContentMessage { // 1
    execute( // 2
      	it.content.createResend(it.chat.id) // 3
    )
}

Готово ? Давайте теперь разберёмся, что же тут происходит:

  1. Данный метод позволяет нам отдельно получать сообщения с некоторым контентом: медиа, локации, текст и т.д.

  2. execute - основной метод всей библиотеки. Этот метод отправляет запрос в систему телеграма. Скорее всего, напрямую вы будете использовать его очень редко, но знать о нём полезно

  3. Магия с createResend заключается в том, что практически любой контент в телеграме может быть переотправлен. По этой причине в библиотеке были сделаны методы переотправки контента - они автоматически формируют запрос на отправку сообщения с тем же контентом

Теперь можно сделать что-то по-интересней. Давайте научимся реагировать на команды:

onCommand("start") { // 1
  reply(it, "Привет, я пока что умею не так много, но скоро всему научусь!") // 2
}
onCommand("help") {
  reply(it, "Отправьте мне любое сообщение, и я отвечу вам тем же?")
}
  1. Этот коллбэк будет вызываться каждый раз при отправке /start , но важно знать, что он будет вызываться при наличии в сообщении только этой команды

  2. Отвечаем текстовым сообщением. 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))
}
  1. asTextContent() проверяет, является ли контент сообщения текстом и вызывает let, если да

  2. content.textSources создаст список TextSource объектов, с этим списком потенциально можно:

    1. Сохранять (у TextSource есть сериализатор для kotlinx.serialization)

    2. Копировать, перемещать, модифицировать

    3. Использовать для форматирования - сурсы могут быть превращены в стандартные текстовые части телеграма

  3. makeString создаст из списка TextSource текст, который будет виден пользователю без учета форматирования

Заключение

Итак, мы создали бота, который:

  • Умеет отвечать на простые команды /start и /help

  • Умеет переотправлять полученные сообщения отправителю

  • Отбирает текстовые сообщения и производит операции с их контентом

Далее остаётся только развивать бота насколько хватит фантазии. Например, можно выделить модули в функции/плагины, как я это сделал в своём PlaguBotе.


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

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+2
Комментарии6

Публикации

Истории

Ближайшие события