Приветики-омлетики, как-то недавно у меня появилась идея написать 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. Я считаю, что в наше время стать ботоводом может любой желающий, и теперь этот желающий знает что нужно делать. Всем мир.
Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…
Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…
Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…
У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…
24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…
27 июня Яндекс проведет гик-фестиваль Young Con для студентов и молодых специалистов, которые интересуются технологиями и хотят работать в IT.…