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

Пошаговый туториал по написанию Telegram бота на Ruby (native)

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

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

И вот пока я занимался написанием этого бота то познакомился с библиотекой (gem) telegram-bot-ruby, научился её использовать вместе с gem 'sqlite3-ruby’ и, кроме того, проникся многими возможностями Telegram ботов чем и хочу поделится с уважаемыми читателями этого форума, внести вклад так сказать.

Много людей хочет писать Telegram боты, ведь это весело и просто.

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

Сразу кидаю ссылку на свой репозиторий по этому посту: here,
Ибо во время тестирования были баги, которые я мог сюда и не перенести, вдруг чего смотреть прямо в репозиторий.

В следствии прочтения этого топика, я надеюсь читатель сможет улучшить своего уже написаного бота, или прямо сейчас скачать Ruby, Telegram и создать что-то новое и прекрасное. Ведь как уже было сказано в «Декларации Киберпространства»:

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

  • Предлагаю начать :

    У меня версия Ruby - 2.7.2, но не исключено, что всё будет работать и с более ранними/поздними версиями.

  • Примерная структура приложения будет выглядеть вот так

  • Первым делом создадим Gemfile - основной держатель зависимостей для сторонних gem’s в Ruby.

  • Файл Gemfile:

    source 'https://rubygems.org'
    gem 'json'
    gem 'net-http-persistent', '~> 2.9'
    gem 'sqlite3'#gem для БД
    gem 'telegram-bot-ruby'#основной гем для создания соеденения с Telegram ботом
    

    Сохраняем файл и выполняем в терминале операцию

    bundle install

    Увидим успешную установку всех гемов (ну это же прелесть Ruby) и на этом с Gemfile будет покончено.

  • Если вы (как и я) лабораторная крыса GitHub’a, то создаем .gitignore для нашего репозитория, у меня прописан классический для продуктов JetBrains файл:

  • Файл .gitignore:

    /.idea/
  • Далее создадим первый класс в корне проекта, называем как хотим этот класс будет выступать в роли инициализатора, в моем случае это FishSocket:

  • файл FishSocket.rb :

    require 'telegram/bot'
    require './library/mac-shake'
    require './library/database'
    require './modules/listener'
    require './modules/security'
    require './modules/standart_messages'
    require './modules/response'
    Entry point class
    class FishSocket
      include Database
      def initialize
        super
        # Initialize BD
        Database.setup
        # Establishing webhook via @gem telegram/bot, using API-KEY
        Telegram::Bot::Client.run(TelegramOrientedInfo::APIKEY) do |bot|
          # Start time variable, for exclude message what was sends before bot starts
          startbottime = Time.now.toi
          # Active socket listener
          bot.listen do |message|
            # Processing the new income message    #if that message sent after bot run.
            Listener.catchnewmessage(message,bot) if Listener::Security.messageisnew(startbottime,message)
          end
        end
      end
    end
    Bot start
    FishSocket.new
    

    Как видим в этот файле упомянуты сразу 5 различных файлов :

    gem telegram/bot,
    mac-shake,
    listener,
    security,
    database.

  • Поэтому предлагаю сразу их создать и показать что к чему:

  • Файл mac-shake.rb:

    # frozen_string_literal: true
    module TelegramOrientedInfo
      API_KEY = '__YOUR_API_KEY__'
    end
  • Как видим в этом файле используется API-KEY для связи с нашим ботом, предлагаю сразу его получить, для этого обратимся к боту от Telegram API : @BotFather

    API-Key который нам вернул бот, следует вставить в константу API-Key, упомянутую ранее.

  • Файл security.rb

    require 'telegram/bot'
    require './library/mac-shake'
    require './library/database'
    require './modules/listener'
    require './modules/security'
    
    # Entry point class
    class FishSocket
      include Database
      def initialize
        super
        # Initialize BD
        Database.setup
        # Establishing webhook via @gem telegram/bot, using API-KEY
        Telegram::Bot::Client.run(TelegramOrientedInfo::API_KEY) do |bot|
          # Start time variable, for exclude message what was sends before bot starts
          start_bot_time = Time.now.to_i
          # Active socket listener
          bot.listen do |message|
            # Processing the new income message    #if that message sent after bot run.
            Listener.catch_new_message(message,bot) if Listener::Security.message_is_new(start_bot_time,message)
          end
        end
      end
    end
    # Bot start
    FishSocket.new

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

  • Файл listener.rb:

    class FishSocket
      # Sorting new message module
      module Listener
        attr_accessor :message, :bot
    
        def catch_new_message(message,bot)
          self.message = message
          self.bot = bot
    
          return false if Security.message_too_far
    
          case self.message
          when Telegram::Bot::Types::CallbackQuery
            CallbackMessages.process
          when Telegram::Bot::Types::Message
            StandartMessages.process
          end
        end
    
        module_function(
          :catch_new_message,
          :message,
          :message=,
          :bot,
          :bot=
        )
      end
    end

    В этом файле мы делим сообщения на две группы, являются ли они ответом на callback функцию, или они обычные. Сейчас проясню что такое callback сообщение в телеграме. Telegram API версии 2.0 предоставляет достаточно обширную поддержку InlineMessages. Это такие сообщение, которые в себе содержает UI элементы взаемодействия с пользователем, я в своем боте использоват InlineKeyboardMarkup это кнопки, после нажатия на которые сообщение которые прийдет на бота, будет типа CallbackMessage, и текст сообщение будет равен тому, который мы указали в атрибут кнопки, при отправке запроса на Telegram API. Позже мы ешё вернёмся к этому принципу.

  • Файл Database.rb 

    # This module assigned to all database operations
    module Database
      attr_accessor :db
    
      require 'sqlite3'
      # This module assigned to create table action
      module Create
        def steam_account_list
          Database.db.execute <<-SQL
        CREATE TABLE steam_account_list (
        accesses VARCHAR (128),
        used INTEGER (1))
          SQL
          true
        rescue SQLite3::SQLException
          false
        end
        module_function(
            :steam_account_list
        )
      end
    
      def setup
        # Initializing database file
        self.db = SQLite3::Database.open 'autosteam.db'
        # Try to get custom table, if table not exists - create this one
        unless get_table('steam_account_list')
          Create.steam_account_list
        end
      end
    
      # Get all from the selected table
      # @var table_name
      def get_table(table_name)
        db.execute <<-SQL
        Select * from #{table_name}
        SQL
      rescue SQLite3::SQLException
        false
      end
    
      module_function(
        :get_table,
        :setup,
        :db,
        :db=
      )
    end

    В этом файле просто происходит инициализация бд и проверка/создание таблиц которые мы хотим использовать.

  • Можем попытатся запустить нашего бота, посредством выполнения файла fish_socket.rb. Если мы всё сделали правильно, то не должны увидеть никакого сообщения о завершеной работе, так как происходит Active Socket прослушывания ответа от Telegram API. Мы по-сути реестрируем наш локальный сервер прикрепляя его к Webhook от Telegram API, на который будут приходить сообщения о любых изменениях.

  • Попробуем добавить примитивный ответ на какое-то сообщение в боте 

    Создадим файл standart_messages.rb, модуль который будет обрабатывать стандартные (текстовые) сообщение нашего бота. Как помним сообщение бывают двух типов : Standart и Callback. 

    Файл standart_messages.rb :

    class FishSocket
      module Listener
        # This module assigned to processing all standart messages
        module StandartMessages
          def process
            case Listener.message.text
            when '/get_account'
              Response.std_message 'Very sorry, нету аккаунтов на данный момент'
            else
              Response.std_message 'Первый раз такое слышу, попробуй другой текст'
            end
          end
      module_function(
          :process
      )
    end
    
    end
    end
    

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

  • Ах да, ответ мы отправляем с помощью модуля Response, который прямо сейчас и создадим

    Файл response.rb

    class FishSocket
      module Listener
        # This module assigned to responses from bot
        module Response
          def std_message(message, chatid = false )
            chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
            chat = chatid if chatid
            Listener.bot.api.sendmessage(
              parsemode: 'html',
              chatid: chat,
              text: message
            )
          end
      module_function(
        :std_message
      )
    end
    
    end
    end
    

    В этом файле мы обращаемся к API Telegrama согласно документации, но уже используя gem telegram-ruby, а именно его функцию api.send_message. Все атрибуты можно посмотреть в Telegram API и поигратся с ними, скажу только лишь что этот метод может отправлять только обычные сообщения.

  • Запускаем бота и тестируем две команды : (Бота можно найти по ссылке которую вам вернул BotFather, вместе с API ключем)

    Привет
    /get_account

    Результат совпал с ожиданиями.

  • Предлагаю увеличить обороты и сразу создать Inline кнопку, добавить реакцию на неё, добавить метод для отправки сообщения с Inline кнопкой.

  • Создадим подпапку assets/ в ней модуль inline_button.
    Файл 
    inline_button.rb : 

    class FishSocket
      # This module assigned to creating InlineKeyboardButton
      module Inline_Button
        GET_ACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callback_data: 'get_account')
       end
    end
    

    Сдесь мы обращаемся всё к тому же telegram-ruby-gem что бы создать обьект типа InlineKeyboardButton.

  • Раcширим наш файл Reponse новыми методоми : 

    def inline_message(message, inline_markup,editless = false, chat_id = false)
      chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
      chat = chat_id if chat_id
      Listener.bot.api.send_message(
        chatid: chat,
        parsemode: 'html',
        text: message,
        replymarkup: inline_markup)
    end
    def generate_inline_markup(kb, force = false)
      Telegram::Bot::Types::InlineKeyboardMarkup.new(
        inline_keyboard: kb
      )
    end
    

    Не стоит забывать выносить новые методы в module_function() :

    module_function(
      :std_message,
      :generate_inline_markup,
      :inline_message
    )
    
  • Добавим на действия 

    /start

    , вывод нашей кнопки, для этого раcширим сначала модуль StandartMessages

    def process
      case Listener.message.text
      when '/get_account'
        Response.std_message 'Very sorry, нету аккаунтов на данный момент'
      when '/start'
        Response.inline_message 'Привет, выбери из доступных действий', Response::generate_inline_markup(
            InlineButton::GET_ACCOUNT
        )
      else
        Response.std_message 'Первый раз такое слышу, попробуй другой текст'
      end
    end
    
  • Создадим файл callback_messages.rb для обработки Callback сообщений :
    Файл callback_messages.rb

    class FishSocket
      module Listener
        # This module assigned to processing all callback messages
        module CallbackMessages
          attr_accessor :callback_message
      def process
        self.callback_message = Listener.message.message
        case Listener.message.data
        when 'get_account'
          Listener::Response.std_message('Нету аккаунтов на данный момент')
        end
      end
    
      module_function(
          :process,
          :callback_message,
          :callback_message=
      )
    end
    
    end
    end
    

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

  • Не забываем обновить список подключаемых модулей, новыми модулями.
    Файл fish_socket.rb

    require 'telegram/bot'
    require './library/mac-shake'
    require './library/database'
    require './modules/listener'
    require './modules/security'
    require './modules/standart_messages'
    require './modules/response'
    require './modules/callback_messages'
    require './modules/assets/inline_button'
    Entry point class
    class FishSocket
      include Database
      def initialize
        super
    
  • Пытаемся запустить бота и посмотреть что будет когда напишем 

    /start

    Нажимая на кнопку мы видим то - что хотели увидеть.

  • Я бы ещё очень много чем хотел поделится, но тогда это будет бесконечная статья по своей сути - мы же рассмотрим ещё буквально 2 примера на создание ForceReply кнопки, и на использование EditInlineMessage функции


  • ForceReply, создадим соответствующий метод в нашем Response модуле

    def forcereplymessage(text, chat_id = false)
      chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
      chat = chat_id if chat_id
      Listener.bot.api.send_message(
        parse_mode: 'html',
        chat_id: chat,
        text: text,
        replymarkup: Telegram::Bot::Types::ForceReply.new(
          force_reply: true,
          selective: true
        )
      )
    end
    

    Не нужно забывать обновлять modulefunction нашего модуля после изминения кол-ва методов.

    Попробуем сделать банальную реакцию на ввод промокода (хз зачем, для примера)

  • Добавим новую кнопку : 

    module InlineButton
      GET_ACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callbackdata: 'get_account')
      HAVE_PROMO = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Есть промокод?', callbackdata: 'force_promo')
    end
    
  • Добавить её в вывод по команде 

    /start

    Модуль StandartMessages

    when '/start'
      Response.inlinemessage 'Привет, выбери из доступных действий', Response::generate_inline_markup(
        [
            InlineButton::GET_ACCOUNT,
            InlineButton::HAVE_PROMO
        ]
      )
    

    Поскольку теперь используется больше одной кнопки, их стоит поместить в массив.

  • Добавим реакцию на нажатие на кнопку, с использованием ForceReply:Модуль CallbackMessages

    def process
      self.callbackmessage = Listener.message.message
      case Listener.message.data
      when 'get_account'
        Listener::Response.std_message('Нету аккаунтов на данный момент')
      when 'force_promo'
        Listener::Response.force_reply_message('Отправьте промокод')
      end
    end
    
  • Проверим то что мы написали, 

    На сообщение от бота сработал ForceReply, что это значит : сообщение выбрано как сообщение для ответа (Reply) так, как если бы мы сами выбрали ответим на сообщение. Очень юзефул если речь о пошаговых операциях где нам нужно наверняка знать что именно хочет сказать юзер.

  • Добавим реакцию на ответ пользователя на сообщение "Отправьте промкод." Поскольку человек отправляет текст, то реагировать мы будем в StandartMessages : Модуль StandartMessages

    def process
      case Listener.message.text
      when '/get_account'
        Response.std_message 'Very sorry, нету аккаунтов на данный момент'
      when '/start'
        Response.inline_message 'Привет, выбери из доступных действий', Response::generate_inline_markup(
          [
              InlineButton::GET_ACCOUNT,
              InlineButton::HAVE_PROMO
          ]
        )
      else
        unless Listener.message.reply_to_message.nil?
          case Listener.message.reply_to_message.text
          when /Отправьте промокод/
            return Listener::Response.std_message 'Промокод существует, вот бесплатный аккаунт :' if Promos::validate Listener.message.text
        return Listener::Response.std_message 'Промокод не найден'
      end
    end
    Response.std_message 'Первый раз такое слышу, попробуй другой текст'
    
    end
    end
    
  • Создадим файл promos.rb для обрабоки промокодов.
    Файл promos.rb

    class FishSocket
      module Listener
        # This module assigned to processing all promo-codes
        module Promos
          def validate(code)
            return true if code =~ /^1[a-zA-Z]*0$/
            false
          end
      module_function(
          :validate
      )
    end
    
    end
    end
    

    Здесь мы используем регулярное выражение для проверки промокода.
    НЕ забываем подключить новый модуль в FishSocket модуле : 
    Модуль FishSocket

    require 'telegram/bot'
    require './library/mac-shake'
    require './library/database'
    require './modules/listener'
    require './modules/security'
    require './modules/standart_messages'
    require './modules/response'
    require './modules/callback_messages'
    require './modules/assets/inline_button'
    require './modules/promos'
    Entry point class
    class FishSocket
      include Database
      def initialize
    
  • Предлагаю протестировать с заведомо не рабочим промокодом, и правильно написаным:

    Функционал работает как и ожидалось, перейдем к последнему пункту: изминения InlineMessages: 

  • Вынесем промокоды в отдельное "Меню", для этого добавим новую кнопку на ответ на сообщение 

    /start

    заменив её кнопку "Есть промкод?"Модуль InlineButton

    module InlineButton
      GET_ACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callback_data: 'get_account')
      HAVE_PROMO = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Есть промокод?', callback_data: 'force_promo')
      ADDITION_MENU = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Ништяки', callback_data: 'advanced_menu')
    end
    

    Модуль StandartMessages

    when '/start'
      Response.inlinemessage 'Привет, выбери из доступных действий', Response::generate_inline_markup(
        [
            InlineButton::GET_ACCOUNT,
            InlineButton::ADDITION_MENU
        ]
      )
    

    Отлично

  • Теперь добавим реакцию на новую кнопку в модуль СallbackMessages: Модуль CallbackMessages

    def process
            self.callback_message = Listener.message.message
            case Listener.message.data
            when 'get_account'
              Listener::Response.std_message('Нету аккаунтов на данный момент')
            when 'force_promo'
              Listener::Response.force_reply_message('Отправьте промокод')
            when 'advanced_menu'
              Listener::Response.inline_message('Дополнительное меню:', Listener::Response.generate_inline_markup([
                  Inline_Button::HAVE_PROMO
              ]), true)
            end
          end
  • Предлагаю реализовать обработку этого атрибута в модуле Response, немного изменив метод inline_message
    Модуль Response

    def inline_message(message, inline_markup, editless = false, chat_id = false)
            chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
            chat = chat_id if chat_id
            if editless
              return Listener.bot.api.edit_message_text(
                chat_id: chat,
                parse_mode: 'html',
                message_id: Listener.message.message.message_id,
                text: message,
                reply_markup: inline_markup
              )
            end
            Listener.bot.api.send_message(
              chat_id: chat,
              parse_mode: 'html',
              text: message,
              reply_markup: inline_markup
            )
          end

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

  • Что ж, попробуем :

      

    После того как нажали на кнопку, сообщение измененилось, отобразив другой ReplyKeyboard. 
    И если мы клацнем на неё : 

    Собственно всё работает как часы. 

Послесловие: Много чего тут не было затронуто, но ведь на всё есть руки и документация, лично мне, было не достаточно описания либы на GitHub. Я считаю, что в наше время стать ботоводом может любой желающий, и теперь этот желающий знает что нужно делать. Всем мир.

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

Публикации

Истории

Работа

Ruby on Rails
4 вакансии
Программист Ruby
4 вакансии

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

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань