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

Telegram Bot на Kotlin: Введение

Kotlin *
Tutorial

Прошло уже какое-то время с момента, когда я публиковал свой первый туториал по tgbotapi и пришло время начать уже серию статей, которая должна разложить по полочкам, как можно разбивать логику Telegram ботов (а потенциально, любых ботов :) ) в целом и как это делать в вышеупомянутой библиотеке в связке с надстройкой PlaguBot.

С чего бы начать?

Начнем с того, какой инструментарий будет использован:

  • Kotlin 😊

  • Gradle (Groovy)

  • tgbotapi 😊

  • PlaguBot

  • Koin

  • KSLog

  • Exposed (базы данных)

Все исходники туториалов будут доступны в этом репозитории, запуск описан там же, но если вкратце - просто замените значение по ключу botToken в конфиге репозитория и вызовите ./gradlew run --args="config.json". В идеале, нужно создать отдельный конфигурационный файл local.config.json и положить конфигурацию в него, потому что он не попадет в репозиторий, но если вы хотите использовать config.json - не забудьте вычистить это токен, если захотите пушить изменения в свой форк или репозиторий

О хитрости local.config.json

В рамках репозитория в .gitignore внесены две строчки:

local.*
local.*/

Это значит, что все файлы и папки, начинающиейся с local. будут проигнорированы гитом. Таким образом, достаточно назвать вашу конфигурацию local.config.json и она точно не попадёт в репозиторий

Моё рабочее окружение

Для собственно кодинга я использую Intellij IDEAиз-под линукс (Elementary OS), но практика показывает, что обычно можно работать и под любыми другими системами (а обладатели Linux могут обойтись текстовым редактором и терминалом для сборки/запуска)

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

А? (Или как оно там работает)

PlaguBot устроен достаточно просто: он берёт путь до файла, превращает его в конфиг, берёт все плагины из конфига и дальше в два этапа загружает бота:

  • Этап загрузки конфигураций - на этом этапе в Koin DI необходимо объявить свои объекты и прочие настройки.

  • Этап конфигурирования поведения бота - тут мы уже настраиваем реакцию на сообщения от пользователей и прочее такое.

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

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

А теперь введение

Первое (почти), что мы делаем в рамках плагина - объявляем его класс конфигурации:

@Serializable
private class Config(
    val onStartCommandMessage: String
)

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

Почти?

На самом деле, первое, что мы делаем - определяем логгер для плагина

private val log = logger

logger создаст нам объект KSLog для автоматического логгирования с тэгом нашего плагина. Подробнее можно посмотреть тут.

Пояснение

Отдельный логгер удобнее создавать так, поскольку logger - это метод-расширение к любому объекту, а посколько мы не хотим, чтобы тэги перепутались, лучше создать отдельный логгер. Кроме прочего, это еще и будет оптимальней в плане быстродействия :)

Вторая вещь, которую мы делаем - это переопределяем setupDI метод для собственно десериализации конфига и его передачи в Koin.

override fun Module.setupDI(database: Database, params: JsonObject) {
    single { /*1*/get<Json>()./*4*/decodeFromJsonElement(/*3*/Config.serializer(), /*2*/params["introduction"] ?: return@single null) }
}

Для начала, на вход setupDI мы получаем три параметра:

  • Module является ресивером (receiver) функции и доступен в её рамках как this.

  • database: Database - собственно, Exposed Database, которую можно будет использовать для сохранения данных (но в этой части мы этого делать не будем).

  • params: JsonObject - конфигурация PlaguBot'а как она была декодирована из конфигурационного файла.

А теперь построчно разберем, что тут происходит:

  1. get<Json>() - получаем объект типа Json для дальнейшей десериализации конфига.

  2. params["introduction"] ?: return@single null - берем элемент из PlaguBot конфига по ключу introduction, а если такого нет - возвращаем null (то есть нельзя создать конфиг).

  3. Config.serializer() - получаем сериализатор конфига (он доступен благодаря аннотации @Serializable).

  4. decodeFromJsonElement(сериализатор, наш элемент из конфига) - собственно, превращаем элемент из конфига в объект конфигурации нашего плагина.

Собственно, вызов single скажет Koin вызвать этот код один раз и сохранить его результат.

Уф, да чего так сложно/просто-то?

Во введении я старался максимально подробно расписать все пришедшие в голову важные моменты, поэтому с одной стороны информации много, а с другой - она вся очень скучная 😔

Ну и третья, самая сложная часть - это настройка бота. Собственно, код:

override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
    val config = koin.getOrNull<Config>()

    if (config == null) {
        log.w("Plugin has been disabled due to absence of \"introduction\" field in config or some error during configuration loading")
        return
    }

    onCommand("start", initialFilter = { it.chat is PrivateChat }) {
        sendMessage(
            it.chat,
            config.onStartCommandMessage
        )
    }
}

Ну что ж, а вот здесь давайте построчно

  • (1) На вход получаем два параметра: BehaviourContext, сочетающий в себе контекст для асинхронности, бота и информацию об обновлениях, приходящих в бота; и koin: Koin - наш источник зависимостей и параметров, в том числе тех, что мы зарегистрировали в setupDI.

  • (2) val config = koin.getOrNull<Config>() - получение текущего конфига нашего плагина либо null - то самое отсутствие конфига в случае, если поле introduction было пропущено в json конфигурации.

  • (4 - 7) Если конфиг не был получен - ругаемся об этом в консоль и заканчиваем настройку плагина.

  • (9) - onCommand("start", initialFilter = { it.chat is PrivateChat }) - конфигурация ожидания команды start. Кроме того, тут используется фильтрация - блок, который будет описан далее, вызовется только для приватных сообщений. Подробнее можно почитать тут.

  • sendMessage(it.chat, config.onStartCommandMessage) - отправка сообщений в чат, в котором нам пришла команда старта. Тут it - это CommonMessage<TextContent> - то есть обычное сообщение с текстом.

Вот, в общем-то, и всё 😊

Итоги

В данном туториале мы:

  • Разобрались, как запускать ботов и конфигурировать их.

  • Создали свой плагин для PlaguBot.

  • Научились отвечать пользователям на команду /start.

Кроме прочего, немного разобрались в терминологии и строении ботов в рамках этой серии туториалов. Для создания своего бота с нуля можно использовать вот этот шаблон, а если вы хотите использовать tgbotapi без каких-либо лишних обвязок вроде Koin/PlaguBot/Exposed - можно воспользоваться этим шаблоном.

Спасибо за внимание к статье и буду рад комментариям/замечаниям/предложениям 😊

Теги:
Хабы:
Всего голосов 5: ↑3 и ↓2 +1
Просмотры 4.4K
Комментарии 0
Комментарии Комментировать