Пошаговый туториал по написанию 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *