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

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

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

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

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

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

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

Можем попытатся запустить нашего бота, посредством выполнения файла 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 ключем.

Привет
eab9bdd84e07cc065852cb38bd41845e
/getaccount
9d5470c7530ad9b0bcf876e3027c52d2

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

Предлагаю увеличить обороты и сразу создать 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
7977c3dd53496dbd850c51dff57f5ed0

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

Я бы ещё очень много чем хотел поделится, но тогда это будет бесконечная статья по своей сути — мы же рассмотрим ещё буквально 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

Проверим то что мы написали,  

59d7380d176167572de89516ebca396a

На сообщение от бота сработал 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

Предлагаю протестировать с заведомо не рабочим промокодом, и правильно написаным:

4b61c01a7c7d13606546041aaf989642

Функционал работает как и ожидалось, перейдем к последнему пункту: изминения 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

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

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

1deca9608e3628acf8d2aa6a7025046cf4e06bf9cfa0680bf7ad96fb09cac158

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

3106f39e4acce8a38abc12f927efc4e9

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

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

© Habrahabr.ru