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

Let’s block ads! (Why?)

Read More

Recent Posts

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

4 дня ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

4 дня ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

4 дня ago

Нейросети будут писать тексты объявления за продавцов на Авито

У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…

5 дней ago

Объявлены победители международной премии Workspace Digital Awards-2024

24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…

5 дней ago

Яндекс проведет гик-фестиваль Young Con

27 июня Яндекс проведет гик-фестиваль Young Con для студентов и молодых специалистов, которые интересуются технологиями и хотят работать в IT.…

6 дней ago