Telegram bot на Ruby c gem telegram-bot-ruby

РЕСУРСЫ, переменные И МЕТОДЫ
  • https://core.telegram.org/bots/api – основной необходимый MAN. Очень крутой, но не хватает примеров
  • https://github.com/atipugin/telegram-bot-ruby – ruby gem telegram, есть пример кода для разных кейсов
  • https://github.com/mustafababil/Telegram-Weather-Bot/blob/master/responseController.py – пример бота в телеграм на python
METHOD
bot.api.send_message - основной метод, отправка сообщений
bot.api.answer_callback_query- ответ на CallBackQuery, отвечать нужно после обработки сообщения, чтобы ошибки ответа на callback не привели к проблемам с самим ответом
bot.api.deleteMessage - удаление сообщений (своих, исходящих от бота)

ID
rqst.from.id - user info id. Если ботом отвечать на него, то даже если бот добавлен в группу ответ пользователю будет в личный чат, а не в групповой.
rqst.chat.id - id чата из которого сделан запрос (с пользователем, групповой). Если ботом отвечать на него, то если бот добавлен в группу ответ пользователю будет в групповой чат. rqst.message.message_id - id сообщения внутри чата rqst.message.id - id сообщения глобальный
TEXT
rqst.text - текст сообщения в случае типа Telegram::Bot::Types
rqst.data - текст "за кнопкой" InlineKeyBoardButton в случае типа Telegram::Bot::Types
rqst.message.text - текст сообщения, на которое мы отвечаем, в случае типа Telegram::Bot::Types::CallbackQuery

Разное
rqst.from.first_name - имя
rqst.from.last_name - фамилия
rqst.chat.type - тип сообщения в чате
ЗАПУСКАЕМ Простой БОТ
1) BotFather – создаем bot, получаем token
Done! Congratulations on your new bot. You will find it at t.me/xxxxxx_Bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:
xxxxxxx

For a description of the Bot API, see this page: https://core.telegram.org/bots/api
2) Ставим gem telegram-bot-ruby
sudo gem install telegram-bot-ruby
3) Проверяем работу токена (код из Usage)
require 'telegram/bot'

token = 'YOUR_TELEGRAM_BOT_API_TOKEN'

Telegram::Bot::Client.run(token) do |bot|
  bot.listen do |message|
  case message.text
  when '/start'
  bot.api.send_message(chat_id: message.chat.id, text: "Hello, #{message.from.first_name}")
  when '/stop'
  bot.api.send_message(chat_id: message.chat.id, text: "Bye, #{message.from.first_name}")
  end
  end
end
Добавляем плюшки

THREAD

Бот надо делать многопоточным, по умолчанию это не так – запросы разных пользователей в одной очереди и один сложный запрос может всю очередь на минуту затормозить. Для фикса заворачиваем обработку в Thread. Чтобы не путаться с конструкциями типа “.message.message”, message выше заменено на rqst (в telegram есть тип сообщения message).

Telegram::Bot::Client.run(token) do |bot|
  bot.listen do |rqst|
    Thread.start(rqst) do |rqst|
      <ОСНОВНОЙ КОД БОТА>
    end
  end
end
RESCUE
Необходимо завернуть основной код бота в конструкцию loop-do и begin-rescue. Пример уже с многопоточностью.
loop do
  begin
    Telegram::Bot::Client.run(token) do |bot|
      bot.listen do |rqst|
        Thread.start(rqst) do |rqst|
          begin
            <ОСНОВНОЙ КОД БОТА>
          rescue
            <RESCUE, можно сделать лог в файл типа "RESCUE PRCSNG">
          end
        end
      end
    end
  rescue
    <RESCUE, можно сделать лог в файл типа "RESCUE API">
  end
end
Это самое важное. Без него бот будет падать как из-за ошибок при обработке ваших запросов (в том числе это может быть и нормой – например при работе с callback или message edit, об этом ниже), так и из-за работ на платформе telegram, которые проводятся регулярно.
CALLBACK
2017-10-02 11:01:45 +0300;out: Telegram API has returned the error. (ok: false, error_code: 400, description: Bad Request: QUERY_ID_INVALID);mess: /var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:74:in call 
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:58:in method_missing 
/home/redkin_p/bin/telegram_bot.rb:72:in block (4 levels) in <main>

API
2017-10-02 06:39:23 +0300;out: Telegram API has returned the error. (error_code: 502, uri: https://api.telegram.org/botxxxxxxx/getUpdates);mess: /var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:74:in call
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:58:in method_missing
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:30:in fetch_updates
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:25:in listen
/home/redkin_p/bin/telegram_bot.rb:16:in block (2 levels) in <main>
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:18:in run
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:8:in run
/home/redkin_p/bin/telegram_bot.rb:14:in block in <main>
/home/redkin_p/bin/telegram_bot.rb:12:in loop
/home/redkin_p/bin/telegram_bot.rb:12:in <main>;
Отправка файлов

Чтобы расшарить файл скидываем в чате с ботом необходимый файл боту и узнаем ботом id файла – в ответе от бота получаем id (типо BQ1DAsSDF@14FSFDSFFDAJ-3diLAg). Осторожно применение метода .file_id не на тот тип данных приведет к ошибке: фотографии windows desktop приложение отсылает как document, а не фото, а в мобильном приложении по умолчанию используется тип именно photo.

Узнаем ID файла
  fid = rqst.document.file_id
  bot.api.send_message(chat_id: rqst.chat.id, text: "#{fid}")

Отсылка файла требует id пользователя и file_id сообщения, остальные параметры опциональные.

Отсылаем файл
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg")

Простой пример – отсылаем на основе запросов в inline keyboard файлы.

Создаем кнопки по вызову файлов. 
kb = [ 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test1', callback_data: "test1"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test2', callback_data: "test2"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test3', callback_data: "test3") 
] 
markup_retry = Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: kb) 

Делаем соответствующие кнопкам ответы со ссылками на файлы (вместо send_message используем send_document).
if rqst.data == "test1" 
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg1") 
elsif rqst.data == "test2" 
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg2") 
elsif rqst.data == "test3" 
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg3") 
else
 "тут что то еще"
end

Не забываем отвечать на Callback. Отвечать нужно после обработки сообщения, чтобы ошибки ответа на callback не привели к проблемам с самим ответом.
 bot.api.answer_callback_query(callback_query_id: rqst.id)
Запрос phone_number

Авторизация по номеру очень клевая штука. Тут есть особенность – нужно проверять чтобы ID пользователя совпадал с ID контакта, потому что свой номер передается как контакт и легко передать контакт “левого”, авторизованного, человека.

Для начала запрашиваем контакт (например, если чела с таким ID telegram еще нет в нашей телеграм-базе) через кастом-клавиатуру с одной кнопкой (https://github.com/atipugin/telegram-bot-ruby#Usage раздел Custom keyboards):

kb = Telegram::Bot::Types::KeyboardButton.new(text: 'Отправить номер', request_contact: true)
markup = Telegram::Bot::Types::ReplyKeyboardMarkup.new(keyboard: kb)
bot.api.send_message(chat_id: rqst.chat.id, text: 'Для работы с ботом вам нужно пройти идентификацию посредством отправки вашего номера телефона.', reply_markup: markup)

Далее при приеме контакта (например, через проверку rqst.contact != nil) проверяем что пользователь отправивший контакт имеет такой же ID как указанный в полученном контакте, если это не так – ругаемся.

bot.api.send_message(chat_id: rqst.chat.id, text: 'Нет, так не получится.') if rqst.from.id != rqst.contact.user_id

Потом проверяем уже наличие номера в своих системах. Если есть – добавляем чела с таким ID telegram в нашу телеграм-базу. Если нет – просим не беспокоить. В любом случае удаляем кастом клавиатуру с запросом номера.

number = rqst.contact.phone_number.sub(/\+/,"")
check = check_number(number)
if check == "ok"
  add_to_telega(number,rqst.contact.user_id)
  text = "Спасибо, идентификация пройдена."
else
  text = "Вас нет в базе данных."
end
kb = Telegram::Bot::Types::ReplyKeyboardRemove.new(remove_keyboard: true)
bot.api.send_message(chat_id: rqst.chat.id, text: "#{text}", reply_markup: kb)
Inline keyboards

Пример простого статического InlineKeyBoard.

kb = [ 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test1', callback_data: "test1"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test2', callback_data: "test2"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test3', callback_data: "test3") 
] 
markup_retry = Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: kb)

Пример статического InlineKB с кнопками на разных рядах.

kb = 
[ 
  [ 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text1', callback_data: "1"), 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text2', callback_data: "2"), 
  ], 
  [ 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text3', callback_data: "3"), 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text4', callback_data: "4"), 
  ], 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text5', callback_data: "5") 
]
markup_retry = Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: kb)

Далее отсылаем ответ с созданной клавой.

bot.api.send_message(chat_id: user_id, text: "#{res}", reply_markup: markup_retry)
bot.api.answer_callback_query(callback_query_id: rqst.id)
Inline keyboards on-the-fly

Очень крутая штука, с помощью нее можно создавать многоуровневые меню, я так сделал трехуровневое меню с кнопками туда-сюда. Очень удобно.

Inline keyboards and on-the-fly updating

There are times when you'd prefer to do things without sending any messages to the chat. For example, when your user is changing settings or flipping through search results. In such cases you can use Inline Keyboards that are integrated directly into the messages they belong to.

Unlike with custom reply keyboards, pressing buttons on inline keyboards doesn't result in messages sent to the chat. Instead, inline keyboards support buttons that work behind the scenes: callback buttons, URL buttons and switch to inline buttons.

В интернете кто-то почему то думает, что чтобы поменять свое сообщение нужно сохранить результат операции отправки (в виде ID) в файле. По факту это не нужно. При получении CallBackQuery можно извлечь ID и даже текст сообщения, на которое пришел ответ.

last_bot_message_id_to_user = rqst.message.message_id
last_bot_message_text_to_user = rqst.message.text

В целом для ответа нужен только id и новая клавиатура. Если клавиатура будет такая же как раньше – telegram ругнется (тут нам поможет exception выше) и менять ничего не будет.

bot.api.editMessageReplyMarkup(chat_id: rqst.from.id, message_id: last_bot_message_id_to_user, reply_markup: markup_retry)
Emoji
Отсылаются просто по unicode коду внутри сообщения. Unicode можно взять отсюда. Буква \u обязательно должна быть маленькая. Например, используем в инлайн-кнопках.
hammer = "\u{1F528}"
Telegram::Bot::Types::InlineKeyboardButton.new(text: "#{hammer} Сделать то-то", callback_data: "test_operations")
Разметка
Отправка форматированных сообщений. Можно размечать через Markdown, можно через HTML. HTML базовый, без каких либо таблиц и прочего. В одном сообщении не должно быть больше 100 URL ссылок (последующие обрезаются) вне зависимости от типа форматирования.
bot.api.send_message(chat_id: "#{id}", text: "[test](https://t.me/TESTBot?start=command-TEST)", parse_mode: "Markdown")

bot.api.send_message(chat_id: "#{id}", text: "<a href=\"http://www.example.com/\">inline URL</a>", parse_mode: "HTML")

Markdown симпатичнее HTML, но на практике очень капризный – из-за него могут сыпаться ошибки и не доставляться сообщения пользователю если markdown увидит в сообщении спец. символы типа _, причем даже не в самой ссылке, что можно было бы “понять и простить”, а дальше в тексте.

bot.api.send_message(chat_id: "#{id}", text: "[Test](https://t.me/TestBot?start) My_Test", parse_mode: "Markdown")

Telegram API has returned the error. (ok: "false", error_code: "400", description: "Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 37") (Telegram::Bot::Exceptions::ResponseError)
Редирект на свой же бот, для исполнения команды  можно сделать двумя способами:
1) Используя встроенные команды, поставив перед числом или неразрывной латиницей знак \
\start
2) Используя Deep Linking (1,2,3). Единственный найденный способ для редиректа IP. Недостаток – пользователю придется нажать кнопку Start (не во всех реализациях клиента), даже если он уже общался с ботом и запрос от пользователя в чате для самого пользователя будет выглядеть как /start, хотя по факту за ним будут скрываться какие-то данные. В случае не первого общения с ботом выглядит немного странно, но работает. В целом, Deep Linking нужен именно для старта общения – авторизации, связи между ботом и другими системами, реферальных ссылок и прочего через передачу ID при старте общения с ботом.
bot.api.send_message(chat_id: "#{id}", text: "[test](https://t.me/TESTBot?start=command-TEST)", parse_mode: "Markdown")

УВЕДОМЛЕНИЯ

По умолчанию бот отправляет сообщения только в ответ на сообщение пользователя. Чтобы отправлять уведомления нужно вместо Listen использовать New. Можно отправлять в ответ и отправлять уведомления одновременно.

# initialize bot client and store it in constant
# that can be done in any file which is loaded at your application's boot process
BOT = Telegram::Bot::Client.new(TOKEN)

# anywhere in another file
BOT.send_message(chat_id: id, text: "hello world")
КОМАНДЫ

Как установить команды которые видит клиент в боте:

  1. Открыть чат с BotFather
  2. Отправить ему команду /setcommands
  3. Выбрать бота для установки
  4. Составить лист команд
    /help Помощь
    /settings Настройки
  5. enjoy

РАБОТА ЧЕРЕЗ PROXY

Gem использует Faraday для подключения. В Faraday встроена возможность работать через HTTP proxy – он читает глобальные переменные http_proxy, ftp_proxy и берет из них адреса серверов. Можно так же задать напрямую в коде.

Faraday will try to automatically infer the proxy settings from your system using URI#find_proxy. This will retrieve them from environment variables such as http_proxy, ftp_proxy, no_proxy, etc. If for any reason you want to disable this behaviour, you can do so by setting the global varibale ignore_env_proxy:

Faraday.ignore_env_proxy = true
You can also specify a custom proxy when initializing the connection

Faraday.new('http://www.example.com', :proxy => 'http://proxy.com')
export http_proxy=http://your.proxy.server:port/ # включаем
unset http_proxy # отключаем

Leave a Reply