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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Приветики-омлетики, как-то недавно у меня появилась идея написать 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:

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

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

  • Файл security.rb

    class FishSocket
      module Listener
        # Module for checks
        module Security
          def messageisnew(starttime, message)
            messagetime = (defined? message.date) ? message.date : message.message.date
            messagetime.toi > starttime
          end
      def message_too_far
        message_date = (defined? Listener.message.date) ? Listener.message.date : Listener.message.message.date
        message_delay = Time.now.to_i - message_date.to_i
        # if message delay less then 5 min then processing message, else ignore
        message_delay > (5 * 60)
      end
      module_function :message_is_new, :message_too_far
    end
    
    end
    end
    

    В этом файле происходит две проверки : на то, что бы сообщение было отпарвлено после старта бота (не обрабатывать команды которые были отпраленны в прошлой сессии). И вторая проверка, что бы не обрабатывать сообщение которым больше 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 steamaccountlist
          Database.db.execute <<-SQL
        CREATE TABLE steamaccountlist (
        accesses VARCHAR (128),
        used INTEGER (1))
          SQL
          true
        rescue SQLite3::SQLException
          false
        end
        modulefunction(
            :steamaccount_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 gettable('steamaccountlist')
          Create.steamaccount_list
        end
      end
    # Get all from the selected table
      # @var tablename
      def gettable(tablename)
        db.execute <<-SQL
        Select * from #{tablename}
        SQL
      rescue SQLite3::SQLException
        false
      end
    modulefunction(
        :gettable,
        :setup,
        :db,
        :db=
      )
    end
    

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

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

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

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

    Файл standartmessages.rb :

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

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

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

    Файл response.rb

    class FishSocket
      module Listener
        # This module assigned to responses from bot
        module Response
          def stdmessage(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.sendmessage. Все атрибуты можно посмотреть в Telegram API и поигратся с ними, скажу только лишь что этот метод может отправлять только обычные сообщения.

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

    Привет
    /getaccount

    Как видим всё отработала так как мы и хотели.

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

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

    class FishSocket
      # This module assigned to creating InlineKeyboardButton
      module InlineButton
        GETACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callbackdata: 'getaccount')
      end
    end
    

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

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

    def inlinemessage(message, inlinemarkup,editless = false, 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(
        chatid: chat,
        parsemode: 'html',
        text: message,
        replymarkup: inlinemarkup)
    end
    def generateinlinemarkup(kb, force = false)
      Telegram::Bot::Types::InlineKeyboardMarkup.new(
        inlinekeyboard: kb
      )
    end
    

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

    modulefunction(
      :stdmessage,
      :generateinlinemarkup,
      :inlinemessage
    )
    
  • Добавим на действия 

    /start

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

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

    class FishSocket
      module Listener
        # This module assigned to processing all callback messages
        module CallbackMessages
          attraccessor :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 возвращает разную структуру сообщений для этих двух типов сообщений, и что бы не создавать спагетти-код выносим разную логику в разные файлы.

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

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

    /start

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

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


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

    def forcereplymessage(text, 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: text,
        replymarkup: Telegram::Bot::Types::ForceReply.new(
          forcereply: true,
          selective: true
        )
      )
    end
    

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

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

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

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

    /start

    Модуль StandartMessages

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

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

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

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

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

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

    def process
      case Listener.message.text
      when '/getaccount'
        Response.stdmessage 'Very sorry, нету аккаунтов на данный момент'
      when '/start'
        Response.inlinemessage 'Привет, выбери из доступных действий', Response::generateinlinemarkup(
          [
              InlineButton::GETACCOUNT,
              InlineButton::HAVEPROMO
          ]
        )
      else
        unless Listener.message.replytomessage.nil?
          case Listener.message.replytomessage.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/standartmessages'
    require './modules/response'
    require './modules/callbackmessages'
    require './modules/assets/inline_button'
    require './modules/promos'
    Entry point class
    class FishSocket
      include Database
      def initialize
    
  • Предлагаю протестировать с заведомо не рабочим промокодом, и правильно написаным:

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

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

    /start

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

    module InlineButton
      GETACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callbackdata: 'getaccount')
      HAVEPROMO = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Есть промокод?', callbackdata: 'forcepromo')
      ADDITIONMENU = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Ништяки', callbackdata: 'advancedmenu')
    end
    

    Модуль StandartMessages

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

    Отлично

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

    def process
      self.callbackmessage = Listener.message.message
      case Listener.message.data
      when 'getaccount'
        Listener::Response.stdmessage('Нету аккаунтов на данный момент')
      when 'forcepromo'
        Listener::Response.forcereply¨C222Cmenu'
        Listener::Response.inline¨C223Cinline¨C224CButton::HAVE¨C225Cmessage
  • Предлагаю реализовать обработку этого атрибута в модуле Response, немного изменив метод inlinemessageМодуль Response

    def inlinemessage(message, inlinemarkup, editless = false, chatid = false)
      chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
      chat = chatid if chatid
      if editless
        return Listener.bot.api.editmessagetext(
          chatid: chat,
          parsemode: 'html',
          messageid: Listener.message.message.messageid,
          text: message,
          replymarkup: inlinemarkup
        )
      end
      Listener.bot.api.sendmessage(
        chatid: chat,
        parsemode: 'html',
        text: message,
        replymarkup: inline_markup
      )
    end
    

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

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

      

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

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

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

Источник: https://habr.com/ru/post/535618/


Интересные статьи

Интересные статьи

Если раньше удаленный формат работы был просто трендом, то сейчас стал необходимостью. И многие из-за принудительного режима самоизоляции столкнулись с проблемой доступа к файлам и приложения...
Я люблю проект длиной в неделю, и меня пугают проекты длиной в год. В аджайле мне очень понравилась концепция MVP и инкремента, это прям мое: сделать работоспособный кусок, внедрить его и двинуть...
Мир пчёл велик и увлекателен, и вы даже не представляете, насколько они окружены вниманием людей: пасечники, фермеры, пчелиные врачи, пчёловладельцы, пчетенёры, айтишники, AI. Что дальше? ...
Есть люди, которые в гневе закрывают вакансию при виде фразы «у нас есть печеньки». Есть люди, которые отказываются от новой дико интересной работы с высокой зарплатой, потому что в офисе не ...
Вместо тысячи слов... xZibit тоже рад, ведь здесь GIF вставлены в стикеры, чтобы быть вставлеными в GIF для КДПВ! А теперь о подробностях реализации.