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