# Telegram::Bot

[![Gem Version](https://badge.fury.io/rb/telegram-bot.svg)](http://badge.fury.io/rb/telegram-bot)
[![Code Climate](https://codeclimate.com/github/telegram-bot-rb/telegram-bot/badges/gpa.svg)](https://codeclimate.com/github/telegram-bot-rb/telegram-bot)
[![Build Status](https://travis-ci.org/telegram-bot-rb/telegram-bot.svg)](https://travis-ci.org/telegram-bot-rb/telegram-bot)

Tools for developing bot for Telegram. Best used with Rails, but can be be used in
[standalone app](https://github.com/telegram-bot-rb/telegram-bot/wiki/Non-rails-application).
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](https://github.com/nahi/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](#async-mode)__ for Telegram and/or Botan API.
  Let the queue adapter handle network errors!

Here is sample [telegram_bot_app](https://github.com/telegram-bot-rb/telegram_bot_app)
with session, keyboards and inline queries.
Run it on your local machine in 1 minute!

And here is [app teamplate](https://github.com/telegram-bot-rb/rails_template)
to generate clean app in seconds.

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'telegram-bot'
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install telegram-bot

## Usage

### Configuration

Add `telegram` section into `secrets.yml`:

```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
```

### Client

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.

```ruby
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:
```ruby
# 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::StaleChat` is raised when bot can't post messages to the chat anymore.

### Controller

```ruby
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
```

#### Optional typecasting

You can enable typecasting of `update` with `telegram-bot-types` by including
`Telegram::Bot::UpdatesPoller::TypedUpdate`:

```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController
  include Telegram::Bot::UpdatesController::TypedUpdate

  def message(message)
    message.class # => Telegram::Bot::Types::Message
  end
end
```

#### Session

There is support for sessions using `ActiveSupport::Cache` stores.

```ruby
# 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
```

#### Message context

It's usual to support chain of messages like BotFather: after receiving command
it asks you for additional argument. There is `MessageContext` for this:

```ruby
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.

#### Processesing updates

To process update run:

```ruby
ControllerClass.dispatch(bot, update)
```

There is also ability to run action without update:

```ruby
# Most likely you'll want to pass :from and :chat
controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat)
controller.process(:help, *args)
```

### Routes

Use `telegram_webhooks` helper to add routes. It will create routes for bots
at "telegram/#{bot.token}" path.

```ruby
# 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.

### Development & Debugging

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)`.

### Testing

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:

```ruby
# 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:

```ruby
# 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.

### Deploying

Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots.
Certificate can be specified with `CERT=path/to/cert`.

### Botan.io metrics

Initialize with `bot = Telegram::Bot::Client.new(token, botan: 'botan token')`
or just add `botan` key in `secrets.yml`:

```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`:

```ruby
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.

### Async mode

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](http://www.rubydoc.info/github/telegram-bot-rb/telegram-bot/master/Telegram/Bot/Async) or
[source](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/async.rb).

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](https://github.com/brandonhilkert/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.

## Development

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](https://rubygems.org).

### Different Rails versions

To setup development for specific major Rails version use:

```
RAILS=5 bundle install
# or
RAILS=5 bundle update
```

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/telegram-bot-rb/telegram-bot.