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