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

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,709
Реакции
1,447
Credits
25,001
Версия Ruby - 2.7.2, но не исключено, что всё будет работать и с более ранними/поздними версиями.
Примерная структура приложения будет выглядеть вот так
6ab1c16bb25f6ea5bcbc3fffdac7639f

Первым делом создадим 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:
Код:
# frozen_string_literal: true
module TelegramOrientedInfo
API_KEY = '__YOUR_API_KEY__'
end
Как видим в этом файле используется API-KEY для связи с нашим ботом, предлагаю сразу его получить, для этого обратимся к боту от Telegram API : Для просмотра ссылки Войди или Зарегистрируйся
460480b09528b36eb5861c5a1d775d7a

API-Key который нам вернул бот, следует вставить в константу API-Key, упомянутую ранее.
Файл security.rb:
Код:
require 'telegram/bot'
require './library/mac-shake'
require './library/database'
require './modules/listener'
require './modules/security'

# 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::API_KEY) do |bot|
# Start time variable, for exclude message what was sends before bot starts
start_bot_time = Time.now.to_i
# Active socket listener
bot.listen do |message|
# Processing the new income message #if that message sent after bot run.
Listener.catch_new_message(message,bot) if Listener::Security.message_is_new(start_bot_time,message)
end
end
end
end
# Bot start
FishSocket.new
В этом файле происходит две проверки : на то, что бы сообщение было отправлено после старта бота (не обрабатывать команды которые были отпраленны в прошлой сессии). И вторая проверка, что бы не обрабатывать сообщение которым больше 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 элементы взаемодействия с пользователем, я в своем боте использоват Для просмотра ссылки Войди или Зарегистрируйся это кнопки, после нажатия на которые сообщение которые прийдет на бота, будет типа 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 steam_account_list
Database.db.execute <<-SQL
CREATE TABLE steam_account_list (
accesses VARCHAR (128),
used INTEGER (1))
SQL
true
rescue SQLite3::SQLException
false
end
module_function(
:steam_account_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 get_table('steam_account_list')
Create.steam_account_list
end
end

# Get all from the selected table
# @var table_name
def get_table(table_name)
db.execute <<-SQL
Select * from #{table_name}
SQL
rescue SQLite3::SQLException
false
end

module_function(
:get_table,
:setup,
:db,
:db=
)
end
В этом файле просто происходит инициализация бд и проверка/создание таблиц которые мы хотим использовать.
Можем попытатся запустить нашего бота, посредством выполнения файла Для просмотра ссылки Войди или Зарегистрируйся. Если мы всё сделали правильно, то не должны увидеть никакого сообщения о завершеной работе, так как происходит Active Socket прослушывания ответа от Telegram API. Мы по-сути реестрируем наш локальный сервер прикрепляя его к Webhook от Telegram API, на который будут приходить сообщения о любых изменениях.
Попробуем добавить примитивный ответ на какое-то сообщение в боте
Создадим файл
standart_messages.rb, модуль который будет обрабатывать стандартные (текстовые) сообщение нашего бота. Как помним сообщение бывают двух типов : Standart и Callback.
Файл standart_messages.rb :
Код:
class FishSocket
module Listener
# This module assigned to processing all standart messages
module StandartMessages
def process
case Listener.message.text
when '/get_account'
Response.std_message 'Very sorry, нету аккаунтов на данный момент'
else
Response.std_message 'Первый раз такое слышу, попробуй другой текст'
end
end
module_function(
:process
)
end

end
end
 

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,709
Реакции
1,447
Credits
25,001
В этом примере мы обрабатываем примитивный запрос /get_account, и возвращаем ответ что на данный момент аккаунтов нету, ведь их дейстительно ещё нету.
Ах да, ответ мы отправляем с помощью модуля
Response, который прямо сейчас и создадим
Файл response.rb
Код:
class FishSocket
module Listener
# This module assigned to responses from bot
module Response
def std_message(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, а именно его функцию Для просмотра ссылки Войди или Зарегистрируйся. Все атрибуты можно посмотреть в Telegram API и поигратся с ними, скажу только лишь что этот метод может отправлять только обычные сообщения.
Запускаем бота и тестируем две команды : (Бота можно найти по ссылке которую вам вернул BotFather, вместе с API ключем)

Привет
eab9bdd84e07cc065852cb38bd41845e
/get_account
9d5470c7530ad9b0bcf876e3027c52d2

Результат совпал с ожиданиями.
Предлагаю увеличить обороты и сразу создать Inline кнопку, добавить реакцию на неё, добавить метод для отправки сообщения с Inline кнопкой.
Создадим подпапку assets/ в ней модуль inline_button.
Файл
inline_button.rb :
Код:
class FishSocket
# This module assigned to creating InlineKeyboardButton
module Inline_Button
GET_ACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callback_data: 'get_account')
end
end

Сдесь мы обращаемся всё к тому же telegram-ruby-gem что бы создать обьект типа InlineKeyboardButton.
Раcширим наш файл Reponse новыми методоми :
def inline_message(message, inline_markup,editless = false, chat_id = false)
chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
chat = chat_id if chat_id
Listener.bot.api.send_message(
chatid: chat,
parsemode: 'html',
text: message,
replymarkup: inline_markup)
end
def generate_inline_markup(kb, force = false)
Telegram::Bot::Types::InlineKeyboardMarkup.new(
inline_keyboard: kb
)
end

Не стоит забывать выносить новые методы в module_function() :
Код:
module_function(
:std_message,
:generate_inline_markup,
:inline_message
)

Добавим на действия
/start
, вывод нашей кнопки, для этого раcширим сначала модуль StandartMessages
def process
case Listener.message.text
when '/get_account'
Response.std_message 'Very sorry, нету аккаунтов на данный момент'
when '/start'
Response.inline_message 'Привет, выбери из доступных действий', Response::generate_inline_markup(
InlineButton::GET_ACCOUNT
)
else
Response.std_message 'Первый раз такое слышу, попробуй другой текст'
end
end

Создадим файл callback_messages.rb для обработки Callback сообщений :
Файл callback_messages.rb
Код:
class FishSocket
module Listener
# This module assigned to processing all callback messages
module CallbackMessages
attr_accessor :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 возвращает разную структуру сообщений для этих двух типов сообщений, и что бы не создавать спагетти-код выносим разную логику в разные файлы.
Не забываем обновить список подключаемых модулей, новыми модулями.
Файл fish_socket.rb
Код:
require 'telegram/bot'
require './library/mac-shake'
require './library/database'
require './modules/listener'
require './modules/security'
require './modules/standart_messages'
require './modules/response'
require './modules/callback_messages'
require './modules/assets/inline_button'
Entry point class
class FishSocket
include Database
def initialize
super

Пытаемся запустить бота и посмотреть что будет когда напишем
/start
7977c3dd53496dbd850c51dff57f5ed0

Нажимая на кнопку мы видим то - что хотели увидеть.
Я бы ещё очень много чем хотел поделится, но тогда это будет бесконечная статья по своей сути - мы же рассмотрим ещё буквально 2 примера на создание ForceReply кнопки, и на использование EditInlineMessage функции

ForceReply, создадим соответствующий метод в нашем Response модуле
Код:
def forcereplymessage(text, chat_id = false)
chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
chat = chat_id if chat_id
Listener.bot.api.send_message(
parse_mode: 'html',
chat_id: chat,
text: text,
replymarkup: Telegram::Bot::Types::ForceReply.new(
force_reply: true,
selective: true
)
)
end

Не нужно забывать обновлять modulefunction нашего модуля после изминения кол-ва методов.
Попробуем сделать банальную реакцию на ввод промокода (хз зачем, для примера)
Добавим новую кнопку :
Код:
module InlineButton
GET_ACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callbackdata: 'get_account')
HAVE_PROMO = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Есть промокод?', callbackdata: 'force_promo')
end

Добавить её в вывод по команде
/start
Модуль StandartMessages
Код:
when '/start'
Response.inlinemessage 'Привет, выбери из доступных действий', Response::generate_inline_markup(
[
InlineButton::GET_ACCOUNT,
InlineButton::HAVE_PROMO
]
)

Поскольку теперь используется больше одной кнопки, их стоит поместить в массив.
Добавим реакцию на нажатие на кнопку, с использованием
ForceReply:Модуль CallbackMessages
Код:
def process
self.callbackmessage = Listener.message.message
case Listener.message.data
when 'get_account'
Listener::Response.std_message('Нету аккаунтов на данный момент')
when 'force_promo'
Listener::Response.force_reply_message('Отправьте промокод')
end
end

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

На сообщение от бота сработал ForceReply, что это значит : сообщение выбрано как сообщение для ответа (Reply) так, как если бы мы сами выбрали ответим на сообщение. Очень юзефул если речь о пошаговых операциях где нам нужно наверняка знать что именно хочет сказать юзер.
Добавим реакцию на ответ пользователя на сообщение "Отправьте промкод." Поскольку человек отправляет текст, то реагировать мы будем в StandartMessages : Модуль StandartMessages
Код:
def process
case Listener.message.text
when '/get_account'
Response.std_message 'Very sorry, нету аккаунтов на данный момент'
when '/start'
Response.inline_message 'Привет, выбери из доступных действий', Response::generate_inline_markup(
[
InlineButton::GET_ACCOUNT,
InlineButton::HAVE_PROMO
]
)
else
unless Listener.message.reply_to_message.nil?
case Listener.message.reply_to_message.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/standart_messages'
require './modules/response'
require './modules/callback_messages'
require './modules/assets/inline_button'
require './modules/promos'
Entry point class
class FishSocket
include Database
def initialize

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

Функционал работает как и ожидалось, перейдем к последнему пункту: изминения InlineMessages:
Вынесем промокоды в отдельное "Меню", для этого добавим новую кнопку на ответ на сообщение
/start
заменив её кнопку "Есть промкод?"Модуль InlineButton
module InlineButton
Код:
GET_ACCOUNT = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Получить account', callback_data: 'get_account')
HAVE_PROMO = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Есть промокод?', callback_data: 'force_promo')
ADDITION_MENU = Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Ништяки', callback_data: 'advanced_menu')
end

Модуль StandartMessages
when '/start'
Response.inlinemessage 'Привет, выбери из доступных действий', Response::generate_inline_markup(
[
InlineButton::GET_ACCOUNT,
InlineButton::ADDITION_MENU
]
)

Отлично
Теперь добавим реакцию на новую кнопку в модуль СallbackMessages: Модуль
CallbackMessages
Код:
def process
self.callback_message = Listener.message.message
case Listener.message.data
when 'get_account'
Listener::Response.std_message('Нету аккаунтов на данный момент')
when 'force_promo'
Listener::Response.force_reply_message('Отправьте промокод')
when 'advanced_menu'
Listener::Response.inline_message('Дополнительное меню:', Listener::Response.generate_inline_markup([
Inline_Button::HAVE_PROMO
]), true)
end
end
Предлагаю реализовать обработку этого атрибута в модуле Response, немного изменив метод inline_message
Модуль Response
Код:
def inline_message(message, inline_markup, editless = false, chat_id = false)
chat = (defined?Listener.message.chat.id) ? Listener.message.chat.id : Listener.message.message.chat.id
chat = chat_id if chat_id
if editless
return Listener.bot.api.edit_message_text(
chat_id: chat,
parse_mode: 'html',
message_id: Listener.message.message.message_id,
text: message,
reply_markup: inline_markup
)
end
Listener.bot.api.send_message(
chat_id: chat,
parse_mode: 'html',
text: message,
reply_markup: inline_markup
)
end
Какова идея? - Мы заменяем уже существующее сообщение на новое, с новым интерфейсом, этот переход позволяет меньше растягивать историю сообщений, и создавать модульные сообщения - такие как меню, оплата, список участников, витрина итд.
Что ж, попробуем :
1deca9608e3628acf8d2aa6a7025046c
f4e06bf9cfa0680bf7ad96fb09cac158


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

Собственно всё работает как часы.
Для просмотра ссылки Войди или Зарегистрируйся