Tools for developing bot for Telegram. Best used with Rails, but can be be used in standalone app. Supposed to be used in webhook-mode in production, and poller-mode in development, but you can use poller in production if you want.
Package contains:
- Ligthweight client for bot API (with fast and thread-safe httpclient under the hood).
- Controller with message parser. Allows to write separate methods for each command.
- Middleware and routes helpers for production env.
- Poller with automatic source-reloader for development env.
- Rake tasks to update webhook urls.
- Async mode for Telegram and/or Botan API. Let the queue adapter handle network errors!
Here is sample telegram_bot_app with session, keyboards and inline queries. Run it on your local machine in 1 minute!
And here is app teamplate to generate clean app in seconds.
Add this line to your application's Gemfile:
gem 'telegram-bot'
And then execute:
$ bundle
Or install it yourself as:
$ gem install telegram-bot
Require if necessary:
require 'telegram/bot'
Add telegram
section into secrets.yml
:
telegram:
bots:
# just set the token
chat: TOKEN_1
# or add username to support commands with mentions (/help@ChatBot)
auction:
token: TOKEN_2
username: ChatBot
# Single bot can be specified like this
bot: TOKEN
# or
bot:
token: TOKEN
username: SomeBot
From now clients will be accessible with Telegram.bots[:chat]
or Telegram.bots[:auction]
.
Single bot can be accessed with Telegram.bot
or Telegram.bots[:default]
.
You can create clients manually with Telegram::Bot::Client.new(token, username)
.
Username is optional and used only to parse commands with mentions.
There is request(path_suffix, body)
method to perform any query.
And there are also shortcuts for available queries in underscored style
(answer_inline_query
instead of answerInlineQuery
).
All this methods just post given params to specific URL.
bot.request(:getMe) or bot.get_me
bot.request(:getupdates, offset: 1) or bot.get_updates(offset: 1)
bot.send_message chat_id: chat_id, text: 'Test'
By default client will return parsed json responses. You can enable
response typecasting to virtus models using telegram-bot-types
gem:
# Add to your gemfile:
gem 'telegram-bot-types', '~> x.x.x'
# Enable typecasting:
Telegram::Bot::Client.typed_response!
# or for single instance:
bot.extend Telegram::Bot::Client::TypedResponse
bot.get_me.class # => Telegram::Bot::Types::User
Any API request error will raise Telegram::Bot::Error
with description in its message.
Special Telegram::Bot::Forbidden
is raised when bot can't post messages to the chat anymore.
class Telegram::WebhookController < Telegram::Bot::UpdatesController
# use callbacks like in any other controllers
around_action :with_locale
# Every update can have one of: message, inline_query, chosen_inline_result,
# callback_query.
# Define method with same name to respond to this updates.
def message(message)
# message can be also accessed via instance method
message == self.payload # true
# store_message(message['text'])
end
# This basic methods receives commonly used params:
#
# message(payload)
# inline_query(query, offset)
# chosen_inline_result(result_id, query)
# callback_query(data)
# Define public methods to respond to commands.
# Command arguments will be parsed and passed to the method.
# Be sure to use splat args and default values to not get errors when
# someone passed more or less arguments in the message.
#
# For some commands like /message or /123 method names should start with
# `on_` to avoid conflicts.
def start(data = nil, *)
# do_smth_with(data)
# There are `chat` & `from` shortcut methods.
# For callback queries `chat` if taken from `message` when it's available.
response = from ? "Hello #{from['username']}!" : 'Hi there!'
# There is `respond_with` helper to set `chat_id` from received message:
respond_with :message, text: response
# `reply_with` also sets `reply_to_message_id`:
reply_with :photo, photo: File.open('party.jpg')
end
private
def with_locale(&block)
I18n.with_locale(locale_for_update, &block)
end
def locale_for_update
if from
# locale for user
elsif chat
# locale for chat
end
end
end
You can enable typecasting of update
with telegram-bot-types
by including
Telegram::Bot::UpdatesPoller::TypedUpdate
:
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::TypedUpdate
def message(message)
message.class # => Telegram::Bot::Types::Message
end
end
There is support for sessions using ActiveSupport::Cache
stores.
# configure store in env files:
config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month}
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::Session
# or just shortcut:
use_session!
# You can override global config
self.session_store = :file_store
def write(text = nil, *)
session[:text] = text
end
def read
respond_with :message, text: session[:text]
end
private
# By default it uses bot's username and user's id as a session key.
# Chat's id is used only when `from` field is empty.
# Override `session_key` method to change this behavior.
def session_key
# In this case session will persist for user only in specific chat:
"#{bot.username}:#{chat['id']}:#{from['id']}"
end
end
It's usual to support chain of messages like BotFather: after receiving command
it asks you for additional argument. There is MessageContext
for this:
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::MessageContext
def rename(*)
# set context for the next message
save_context :rename
respond_with :message, text: 'What name do you like?'
end
# register context handlers to handle this context
context_handler :rename do |*words|
update_name words[0]
respond_with :message, text: 'Renamed!'
end
# You can do it in other way:
def rename(name = nil, *)
if name
update_name name
respond_with :message, text: 'Renamed!'
else
save_context :rename
respond_with :message, text: 'What name do you like?'
end
end
# This will call #rename like if it is called with message '/rename %text%'
context_handler :rename
# If you have a lot of such methods you can use
context_to_action!
# It'll use context value as action name for all contexts which miss handlers.
end
You can use CallbackQueryContext
in the similar way to split #callback_query
into
several specific methods. It doesn't require session support, and takes context from
data. If data has a prefix with colon like this my_ctx:smth...
it'll call
my_ctx_callback_query('smth...')
when there is such action method. Otherwise
it'll call callback_query('my_ctx:smth...')
as usual.
To process update run:
ControllerClass.dispatch(bot, update)
There is also ability to run action without update:
# Most likely you'll want to pass :from and :chat
controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat)
controller.process(:help, *args)
Use telegram_webhooks
helper to add routes. It will create routes for bots
at "telegram/#{bot.token}" path.
# Create routes for all Telegram.bots to use same controller:
telegram_webhooks TelegramController
# Or pass custom bots usin any of supported config options:
telegram_webhooks TelegramController,
bot,
{token: token, username: username},
other_bot_token
# Use different controllers for each bot:
telegram_webhooks bot => TelegramChatController,
other_bot => TelegramAuctionController
# telegram_webhooks creates named routes.
# Route name depends on `Telegram.bots`.
# When there is single bot it will use 'telegram_webhook'.
# When there are it will use bot's key in the `Telegram.bots` as prefix
# (eg. `chat_telegram_webhook`).
# You can override this options or specify others:
telegram_webhooks TelegramController, as: :my_webhook
telegram_webhooks bot => [TelegramChatController, as: :chat_webhook],
other_bot => TelegramAuctionController,
admin_chat: TelegramAdminChatController
For Rack applications you can also use Telegram::Bot::Middleware
or just
call .dispatch(bot, update)
on controller.
Use rake telegram:bot:poller
to run poller. It'll automatically load
changes without restart in development env. Optionally specify bot to run poller for
with BOT
envvar (BOT=chat
).
This task will not work if you don't use telegram_webhooks
.
You can run poller manually with
Telegram::Bot::UpdatesPoller.start(bot, controller_class)
.
There is Telegram::Bot::ClientStub
class to stub client for tests.
Instead of performing API requests it stores them in requests
hash.
To stub all possible clients use Telegram::Bot::ClientStub.stub_all!
before
initializing clients. Most likely you'll want something like this:
# environments/test.rb
# Make sure to run it before defining routes or storing bot to some place in app!
Telegram.reset_bots
Telegram::Bot::ClientStub.stub_all!
# rails_helper.rb
RSpec.configure do |config|
# ...
config.after { Telegram.bot.reset }
# ...
end
There are integration and controller contexts for RSpec and some built-in matchers:
# spec/requests/telegram_webhooks_spec.rb
require 'telegram/bot/rspec/integration'
RSpec.describe TelegramWebhooksController, :telegram_bot do
# for old rspec add:
# include_context 'telegram/bot/integration'
describe '#start' do
subject { -> { dispatch_command :start } }
it { should respond_with_message 'Hi there!' }
end
end
# For controller specs use
require 'telegram/bot/updates_controller/rspec_helpers'
RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do
# for old rspec add:
# include_context 'telegram/bot/updates_controller'
end
# Matchers are available for custom specs:
include Telegram::Bot::RSpec::ClientMatchers
expect(&process_update).to send_telegram_message(bot, /msg regexp/, some: :option)
expect(&process_update).
to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text'))
See sample app for more examples.
Use rake telegram:bot:set_webhook
to update webhook url for all configured bots.
Certificate can be specified with CERT=path/to/cert
.
Initialize with bot = Telegram::Bot::Client.new(token, botan: 'botan token')
or just add botan
key in secrets.yml
:
telegram:
bot:
token: bot_token
botan: botan_token
Access to Botan client with bot.botan
.
Use bot.botan.track(event, uid, payload)
to track events.
There are some helpers for controllers in Telegram::Bot::Botan::ControllerHelpers
:
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::Botan::ControllerHelpers
# This will track with event: action_name & data: payload
before_action :botan_track_action
def smth(*)
# This will track event for current user only when botan is configured.
botan_track :my_event, custom_data
# or get access directly to botan client:
botan.track(...)
end
end
There is no stubbing for botan clients, so don't set botan token in tests.
There is built in support for async requests using ActiveJob. Without Rails you can implement your own worker class to handle such requests. This allows:
- Process updates very fast, without waiting for telegram and botan responses.
- Handle and retry network and other errors with queue adapter.
- ???
Instead of performing request instantly client serializes it, pushes to queue, and immediately return control back. The job is then fetched with a worker and real API request is performed. And this all is absolutely transparent for the app.
To enable this mode add async: true
to bot's and botan's config.
For more information and custom configuration check out
docs or
source.
If you want async mode, but don't want to setup queue, know that Rails 5 are shipped with Async adapter by default, and there is Sucker Punch for Rails 4.
Be aware of some limitations:
- Client will not return API response.
- Sending files is not available in async mode [now], because them can not be serialized.
To disable async mode for the block of code use bot.async(false) { bot.send_photo }
.
Yes, it's threadsafe too.
After checking out the repo, run bin/setup
to install dependencies.
Then, run rake spec
to run the tests.
You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
.
To release a new version, update the version number in version.rb
,
and then run bundle exec rake release
, which will create a git tag for the version,
push git commits and tags, and push the .gem
file to rubygems.org.
To setup development for specific major Rails version use:
RAILS=5 bundle install
# or
RAILS=5 bundle update
Bug reports and pull requests are welcome on GitHub at https://github.com/telegram-bot-rb/telegram-bot.