1
0
зеркало из https://github.com/glebtv/telegram-bot.git synced 2026-09-03 17:55:52 +03:00

Сравнить коммиты

..

28 Коммитов

Автор SHA1 Сообщение Дата
Max Melentiev
995cde5607 v0.8.0 2016-06-02 21:36:05 +03:00
Max Melentiev
9ba5a817cd Fixed doubled instrumentation for reply_with 2016-06-02 21:19:27 +03:00
Max Melentiev
0205891985 Controller#respond_with to reply without reply_to_message_id 2016-06-02 21:07:15 +03:00
printercu
7393b1a1f3 Merge pull request #7 from telegram-bot-rb/rails5
Rails5 compatibility
2016-06-02 20:34:09 +03:00
Max Melentiev
43b519c5e8 fixed controller's callbacks for rails5 2016-06-02 20:30:35 +03:00
Max Melentiev
93ff9851f6 Add rails 5 to travis matrix 2016-06-02 19:48:19 +03:00
Max Melentiev
7c1f1b5b63 Middleware is rails5-compatible 2016-06-02 19:38:29 +03:00
printercu
028849a617 Merge pull request #5 from dreyks/reply_to_message_id
Fix: reply_with sets reply_to_message_id instead of reply_to_message
2016-06-01 23:22:32 +03:00
Roman Usherenko
ba1b3c0974 fix reply_to_message_id 2016-06-01 15:18:29 +03:00
Max Melentiev
f61cc1e536 v0.7.4 2016-05-31 17:44:24 +03:00
printercu
2f7ff9d50e Merge pull request #4 from dreyks/rails5
Rails5 compatibility
2016-05-31 17:34:31 +03:00
Roman Usherenko
aca35a1934 Rails5 compatibility 2016-05-31 17:12:36 +03:00
Max Melentiev
5bf98b419b v0.7.3 2016-05-31 11:49:54 +03:00
Max Melentiev
8077ef9bcf Fixed issues with poller in production. Fixes #3 2016-05-31 11:46:58 +03:00
Max Melentiev
5fef6fb43a v0.7.2 2016-05-31 11:02:55 +03:00
Max Melentiev
07be01c07d Bot API 2.1 2016-05-31 11:01:00 +03:00
Max Melentiev
33a1220c5f Fixed controller crashes on unsupported update types 2016-05-31 10:43:11 +03:00
Max Melentiev
a52e7ff31b Updated readme, prepare to 0.7.0 2016-05-17 11:40:06 +03:00
Max Melentiev
23fb9a28d1 Contextual actions for callback_query 2016-05-17 11:39:42 +03:00
Max Melentiev
8994328654 Basic actions are called with most useful params instead of payload 2016-05-17 11:39:35 +03:00
Max Melentiev
4593a10dec New api methods and support for inline keyboards 2016-05-17 11:12:51 +03:00
printercu
148023bb0d Add link to sample app to readme 2016-05-15 18:01:39 +03:00
Max Melentiev
9b9e55a449 botan.io tracker 2016-05-12 14:21:49 +03:00
Max Melentiev
1f139a2b36 MessageContext doesnt use #process to run actions 2016-05-12 14:21:49 +03:00
Max Melentiev
9b3fb562a3 Fixed MessageContext for messages without text 2016-05-11 22:13:45 +03:00
Max Melentiev
62475965e9 v0.6.0 2016-03-25 00:16:05 +03:00
Max Melentiev
d7a20a745c StaleChat error 2016-03-25 00:14:47 +03:00
Max Melentiev
1963533e33 Encode arrays as json in requests 2016-03-24 23:57:14 +03:00
33 изменённых файлов: 810 добавлений и 192 удалений

Просмотреть файл

@@ -2,5 +2,8 @@ language: ruby
cache: bundler cache: bundler
rvm: rvm:
- 2.2.3 - 2.2.3
env:
- RAILS=4
- RAILS=5
notifications: notifications:
email: false email: false

Просмотреть файл

@@ -1,3 +1,43 @@
# 0.8.0
- Fixed `#reply_with`, now it sets `reply_to_message_id` as it's supposed to.
Added `#respond_with` which works the same way, but doesn't set `reply_to_message_id`.
Please, replace all occurrences of `reply_with` to `respond_with` to
keep it working the old way.
- Fixes for Rails 5:
- Controller callbacks
- Middleware
- Setup travis builds
# 0.7.4
- Rails 5 support by @dreyks (#4).
# 0.7.3
- Fixed issues with poller in production (#3)
# 0.7.2
- Bot API 2.1
- Fixed possible crashes when payload type is not supported.
Provides empty session when neither `from` nor `chat` is defined.
# 0.7.0
- New Bot API methods.
- Helpers for inline keyboards, support for callback_query (with contextual actions).
- Changed action methods signature
- `#inline_query(payload) -> #inline_query(query, offset)`
- `#chosen_inline_result(payload)` -> `#chosen_inline_result(result_id, query)`
- MessageContext doesn't use second #process call to run contextual action.
- Botan.io metrics.
# 0.6.0
- StaleChat error.
- Encode arrays as json in request body.
# 0.5.0 # 0.5.0
- MessageContext. - MessageContext.

Просмотреть файл

@@ -1,6 +1,13 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gemspec gemspec
case ENV['RAILS']
when '5'
gem 'actionpack', '5.0.0.rc1'
when '4'
gem 'actionpack', '~> 4.2'
end
group :development do group :development do
gem 'sdoc', '~> 0.4.1' gem 'sdoc', '~> 0.4.1'
gem 'pry', '~> 0.10.1' gem 'pry', '~> 0.10.1'

101
README.md
Просмотреть файл

@@ -17,6 +17,10 @@ Package contains:
- Poller with automatic source-reloader for development env. - Poller with automatic source-reloader for development env.
- Rake tasks to update webhook urls. - Rake tasks to update webhook urls.
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!
## Installation ## Installation
Add this line to your application's Gemfile: Add this line to your application's Gemfile:
@@ -89,6 +93,9 @@ bot.extend Telegram::Bot::Client::TypedResponse
bot.get_me.class # => Telegram::Bot::Types::User 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 ### Controller
```ruby ```ruby
@@ -96,7 +103,8 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
# use callbacks like in any other controllers # use callbacks like in any other controllers
around_action :with_locale around_action :with_locale
# Every update can have one of: message, inline_query & chosen_inline_result. # Every update can have one of: message, inline_query, chosen_inline_result,
# callback_query.
# Define method with same name to respond to this updates. # Define method with same name to respond to this updates.
def message(message) def message(message)
# message can be also accessed via instance method # message can be also accessed via instance method
@@ -104,6 +112,13 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
# store_message(message['text']) # store_message(message['text'])
end 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. # Define public methods to respond to commands.
# Command arguments will be parsed and passed to the method. # Command arguments will be parsed and passed to the method.
# Be sure to use splat args and default values to not get errors when # Be sure to use splat args and default values to not get errors when
@@ -116,9 +131,10 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
# There are `chat` & `from` shortcut methods. # There are `chat` & `from` shortcut methods.
response = from ? "Hello #{from['username']}!" : 'Hi there!' response = from ? "Hello #{from['username']}!" : 'Hi there!'
# There is `reply_with` helper to set basic fields # There is `respond_with` helper to set `chat_id` from received message:
# like `reply_to_message` & `chat_id`. respond_with :message, text: response
reply_with :message, text: response # `reply_with` also sets `reply_to_message_id`:
reply_with :photo, photo: File.open('party.jpg')
end end
private private
@@ -137,6 +153,8 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
``` ```
#### Optional typecasting
You can enable typecasting of `update` with `telegram-bot-types` by including You can enable typecasting of `update` with `telegram-bot-types` by including
`Telegram::Bot::UpdatesPoller::TypedUpdate`: `Telegram::Bot::UpdatesPoller::TypedUpdate`:
@@ -150,6 +168,8 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
``` ```
#### Session
There is support for sessions using `ActiveSupport::Cache` stores. There is support for sessions using `ActiveSupport::Cache` stores.
```ruby ```ruby
@@ -169,7 +189,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
def read def read
reply_with :message, text: session[:text] respond_with :message, text: session[:text]
end end
private private
@@ -183,6 +203,8 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
``` ```
#### Message context
It's usual to support chain of messages like BotFather: after receiving command It's usual to support chain of messages like BotFather: after receiving command
it asks you for additional argument. There is `MessageContext` for this: it asks you for additional argument. There is `MessageContext` for this:
@@ -193,23 +215,23 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
def rename(*) def rename(*)
# set context for the next message # set context for the next message
save_context :rename save_context :rename
reply_with :message, text: 'What name do you like?' respond_with :message, text: 'What name do you like?'
end end
# register context handlers to handle this context # register context handlers to handle this context
context_handler :rename do |message| context_handler :rename do |*words|
update_name message[:text] update_name words[0]
reply_with :message, text: 'Renamed!' respond_with :message, text: 'Renamed!'
end end
# You can do it in other way: # You can do it in other way:
def rename(name = nil, *) def rename(name = nil, *)
if name if name
update_name name update_name name
reply_with :message, text: 'Renamed!' respond_with :message, text: 'Renamed!'
else else
save_context :rename save_context :rename
reply_with :message, text: 'What name do you like?' respond_with :message, text: 'What name do you like?'
end end
end end
@@ -222,6 +244,14 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
end end
``` ```
You can use `CallbackQueyContext` 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: To process update run:
```ruby ```ruby
@@ -263,7 +293,8 @@ telegram_webhooks bot => TelegramChatController,
# You can override this options or specify others: # You can override this options or specify others:
telegram_webhooks TelegramController, as: :my_webhook telegram_webhooks TelegramController, as: :my_webhook
telegram_webhooks bot => [TelegramChatController, as: :chat_webhook], telegram_webhooks bot => [TelegramChatController, as: :chat_webhook],
other_bot => [TelegramAuctionController, other_bot => TelegramAuctionController,
admin_chat: TelegramAdminChatController
``` ```
For Rack applications you can also use `Telegram::Bot::Middleware` or just For Rack applications you can also use `Telegram::Bot::Middleware` or just
@@ -306,6 +337,42 @@ Check out `telegram/bot/updates_controller/rspec_helpers` and
Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots. Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots.
Certificate can be specified with `CERT=path/to/cert`. 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::UpdatesController::Botan`:
```ruby
class Telegram::WebhookController < Telegram::Bot::UpdatesController
include Telegram::Bot::UpdatesController::Botan
# 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.
## Development ## Development
After checking out the repo, run `bin/setup` to install dependencies. After checking out the repo, run `bin/setup` to install dependencies.
@@ -317,6 +384,16 @@ 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, 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). 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 ## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/telegram-bot-rb/telegram-bot. Bug reports and pull requests are welcome on GitHub at https://github.com/telegram-bot-rb/telegram-bot.

Просмотреть файл

@@ -1,9 +1,14 @@
namespace :telegram do namespace :telegram do
namespace :bot do namespace :bot do
desc 'Run poller' desc 'Run poller. It broadcasts Rails.logger to STDOUT in dev like `rails s` do. ' \
task poller: :environment do 'Use LOG_TO_STDOUT to enable/disable broadcasting.'
console = ActiveSupport::Logger.new(STDERR) task :poller do
Rails.logger.extend ActiveSupport::Logger.broadcast console ENV['BOT_POLLER_MODE'] = 'true'
Rake::Task['environment'].invoke
if ENV.fetch('LOG_TO_STDOUT') { Rails.env.development? }.present?
console = ActiveSupport::Logger.new(STDERR)
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
Telegram::Bot::UpdatesPoller.start(ENV['BOT'].try!(:to_sym) || :default) Telegram::Bot::UpdatesPoller.start(ENV['BOT'].try!(:to_sym) || :default)
end end

Просмотреть файл

@@ -7,6 +7,26 @@ module Telegram
class Error < StandardError; end class Error < StandardError; end
class NotFound < Error; end class NotFound < Error; end
# Error class for events when chat is not available anymore for bot.
# While Telegram has same error codes for different messages and there is no
# official docs for this error codes it uses `description` to
# check response.
class StaleChat < Error
DESCRIPTIONS = [
'bot was kicked',
"can't write to",
'group chat is deactivated',
].freeze
class << self
def match_response?(response)
description = response['description'].to_s
DESCRIPTIONS.any? { |x| description[x] }
end
end
end
autoload :Botan, 'telegram/bot/botan'
autoload :Client, 'telegram/bot/client' autoload :Client, 'telegram/bot/client'
autoload :ClientStub, 'telegram/bot/client_stub' autoload :ClientStub, 'telegram/bot/client_stub'
autoload :Middleware, 'telegram/bot/middleware' autoload :Middleware, 'telegram/bot/middleware'

36
lib/telegram/bot/botan.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,36 @@
module Telegram
module Bot
class Botan
TRACK_URI = 'https://api.botan.io/track'.freeze
class Error < Bot::Error; end
include DebugClient
attr_reader :client, :token
def initialize(token)
@client = HTTPClient.new
@token = token
end
def track(event, uid, payload = {})
res = http_request(
:post,
TRACK_URI,
{token: token, name: event, uid: uid},
payload.to_json,
)
status = res.status
return JSON.parse(res.body) if 300 > status
result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier
err_msg = "#{res.reason}: #{result && result['info'] || '-'}"
raise Error, err_msg
end
def http_request(method, uri, query, body)
client.request(method, uri, query, body)
end
end
end
end

Просмотреть файл

@@ -2,14 +2,16 @@ require 'json'
require 'httpclient' require 'httpclient'
require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/keys'
require 'telegram/bot/debug_client'
module Telegram module Telegram
module Bot module Bot
class Client class Client
autoload :TypedResponse, 'telegram/bot/client/typed_response'
URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze URL_TEMPLATE = 'https://api.telegram.org/bot%s/'.freeze
autoload :TypedResponse, 'telegram/bot/client/typed_response'
include DebugClient
class << self class << self
# Accepts different options to initialize bot. # Accepts different options to initialize bot.
def wrap(input) def wrap(input)
@@ -18,7 +20,7 @@ module Telegram
when Array then input.map(&method(__callee__)) when Array then input.map(&method(__callee__))
when Hash then when Hash then
input = input.stringify_keys input = input.stringify_keys
new input['token'], input['username'] new input['token'], input['username'], botan: input['botan']
when Symbol when Symbol
Telegram.bots[input] or Telegram.bots[input] or
raise "Bot #{input} not configured, check Telegram.bots_config." raise "Bot #{input} not configured, check Telegram.bots_config."
@@ -34,67 +36,64 @@ module Telegram
# Encodes nested hashes as json. # Encodes nested hashes as json.
def prepare_body(body) def prepare_body(body)
body = body.dup
body.each do |k, val| body.each do |k, val|
body[k] = val.to_json if val.is_a?(Hash) body[k] = val.to_json if val.is_a?(Hash) || val.is_a?(Array)
end end
end end
end end
attr_reader :client, :token, :username, :base_uri attr_reader :client, :token, :username, :base_uri, :botan
def initialize(token, username = nil) def initialize(token, username = nil, botan: nil)
@client = HTTPClient.new @client = HTTPClient.new
@token = token @token = token
@username = username @username = username
@base_uri = format URL_TEMPLATE, token @base_uri = format URL_TEMPLATE, token
@botan = Botan.new(botan) if botan
end end
def debug!(dev = STDOUT) def request(action, body = {}) # rubocop:disable PerceivedComplexity
if block_given?
begin
old_dev = client.debug_dev
client.debug_dev = dev
yield
ensure
client.debug_dev = old_dev
end
else
client.debug_dev = dev
end
end
def debug_off!
client.debug_dev = nil
end
def request(action, body = {})
res = http_request("#{base_uri}#{action}", self.class.prepare_body(body)) res = http_request("#{base_uri}#{action}", self.class.prepare_body(body))
status = res.status status = res.status
return JSON.parse(res.body) if 300 > status return JSON.parse(res.body) if 300 > status
result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier result = JSON.parse(res.body) rescue nil # rubocop:disable RescueModifier
err_msg = "#{res.reason}: #{result && result['description'] || '-'}" err_msg = "#{res.reason}: #{result && result['description'] || '-'}"
# NotFound is raised only for valid responses from Telegram if result
raise NotFound, err_msg if 404 == status && result # NotFound is raised only for valid responses from Telegram
raise NotFound, err_msg if 404 == status
raise StaleChat, err_msg if StaleChat.match_response?(result)
end
raise Error, err_msg raise Error, err_msg
end end
%w( %w(
answerCallbackQuery
answerInlineQuery answerInlineQuery
forwardMessage forwardMessage
getChat
getChatAdministrators
getChatMember
getChatMembersCount
getFile getFile
getMe getMe
getUpdates getUpdates
getUserProfilePhotos getUserProfilePhotos
kickChatMember
leaveChat
sendAudio sendAudio
sendChatAction sendChatAction
sendContact
sendDocument sendDocument
sendLocation sendLocation
sendMessage sendMessage
sendPhoto sendPhoto
sendSticker sendSticker
sendVenue
sendVideo sendVideo
sendVoice sendVoice
setWebhook setWebhook
unbanChatMember
).each do |method| ).each do |method|
define_method(method.underscore) { |*args| request(method, *args) } define_method(method.underscore) { |*args| request(method, *args) }
end end

Просмотреть файл

@@ -15,7 +15,8 @@ module Telegram
end end
class << self class << self
# Makes all # Any call to Client.new will return ClientStub instance when `enabled` is true.
# Can be used with a block.
def stub_all!(enabled = true) def stub_all!(enabled = true)
Client.extend(StubbedConstructor) unless Client < StubbedConstructor Client.extend(StubbedConstructor) unless Client < StubbedConstructor
return @_stub_all = enabled unless block_given? return @_stub_all = enabled unless block_given?

Просмотреть файл

@@ -14,9 +14,14 @@ module Telegram
# It just tells routes helpers whether to add routed bots to # It just tells routes helpers whether to add routed bots to
# Bot::UpdatesPoller, so their config will be available by bot key in # Bot::UpdatesPoller, so their config will be available by bot key in
# Bot::UpdatesPoller.start. # Bot::UpdatesPoller.start.
#
# It's enabled by default in Rails dev environment and `rake telegram:bot:poller`
# task. Use `BOT_POLLER_MODE=true` envvar to set it manually.
def bot_poller_mode? def bot_poller_mode?
return @bot_poller_mode if defined?(@bot_poller_mode) return @bot_poller_mode if defined?(@bot_poller_mode)
Rails.env.development? if defined?(Rails) @bot_poller_mode = ENV.fetch('BOT_POLLER_MODE') do
Rails.env.development? if defined?(Rails)
end
end end
# Hash of bots made with bots_config. # Hash of bots made with bots_config.

23
lib/telegram/bot/debug_client.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,23 @@
module Telegram
module Bot
module DebugClient
def debug!(dev = STDOUT)
if block_given?
begin
old_dev = client.debug_dev
client.debug_dev = dev
yield
ensure
client.debug_dev = old_dev
end
else
client.debug_dev = dev
end
end
def debug_off!
client.debug_dev = nil
end
end
end
end

Просмотреть файл

@@ -1,6 +1,8 @@
require 'active_support/concern' require 'active_support/concern'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/json'
require 'action_dispatch/http/mime_type' require 'action_dispatch/http/mime_type'
require 'action_dispatch/middleware/params_parser' require 'action_dispatch/http/request'
module Telegram module Telegram
module Bot module Bot
@@ -13,7 +15,8 @@ module Telegram
end end
def call(env) def call(env)
update = env['action_dispatch.request.request_parameters'] request = ActionDispatch::Request.new(env)
update = request.request_parameters
controller.dispatch(bot, update) controller.dispatch(bot, update)
[200, {}, ['']] [200, {}, ['']]
end end

Просмотреть файл

@@ -21,7 +21,7 @@ module Telegram
# end # end
# #
# def help(*) # def help(*)
# reply_with :message, text: # respond_with :message, text:
# end # end
# #
# To process plain text messages (without commands) or other updates just # To process plain text messages (without commands) or other updates just
@@ -29,7 +29,7 @@ module Telegram
# as an argument. # as an argument.
# #
# def message(message) # def message(message)
# reply_with :message, text: "Echo: #{message['text']}" # respond_with :message, text: "Echo: #{message['text']}"
# end # end
# #
# def inline_query(query) # def inline_query(query)
@@ -50,17 +50,20 @@ module Telegram
# ControllerClass.new(bot, from: telegram_user, chat: telegram_chat). # ControllerClass.new(bot, from: telegram_user, chat: telegram_chat).
# process(:help, *args) # process(:help, *args)
# #
class UpdatesController < AbstractController::Base class UpdatesController < AbstractController::Base # rubocop:disable ClassLength
abstract! abstract!
require 'telegram/bot/updates_controller/session' require 'telegram/bot/updates_controller/session'
require 'telegram/bot/updates_controller/log_subscriber' require 'telegram/bot/updates_controller/log_subscriber'
require 'telegram/bot/updates_controller/instrumentation' require 'telegram/bot/updates_controller/instrumentation'
require 'telegram/bot/updates_controller/reply_helpers'
autoload :CallbackQueyContext, 'telegram/bot/updates_controller/callback_query_context'
autoload :MessageContext, 'telegram/bot/updates_controller/message_context' autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
autoload :Botan, 'telegram/bot/updates_controller/botan'
include AbstractController::Callbacks include AbstractController::Callbacks
# Redefine callbacks with default terminator. # Redefine callbacks with default terminator.
if ActiveSupport.gem_version >= Gem::Version.new('5') if ActiveSupport::VERSION::MAJOR >= 5
define_callbacks :process_action, define_callbacks :process_action,
skip_after_callbacks_if_terminated: true skip_after_callbacks_if_terminated: true
else else
@@ -70,6 +73,7 @@ module Telegram
end end
include AbstractController::Translation include AbstractController::Translation
include ReplyHelpers
prepend Instrumentation prepend Instrumentation
extend Session::ConfigMethods extend Session::ConfigMethods
@@ -79,6 +83,8 @@ module Telegram
message message
inline_query inline_query
chosen_inline_result chosen_inline_result
callback_query
edited_message
).freeze ).freeze
CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)") CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
@@ -138,13 +144,13 @@ module Telegram
# Accessor to `'chat'` field of payload. Can be overriden with `chat` option # Accessor to `'chat'` field of payload. Can be overriden with `chat` option
# for #initialize. # for #initialize.
def chat def chat
@_chat || payload && payload['chat'] @_chat ||= payload && payload['chat']
end end
# Accessor to `'from'` field of payload. Can be overriden with `from` option # Accessor to `'from'` field of payload. Can be overriden with `from` option
# for #initialize. # for #initialize.
def from def from
@_from || payload && payload['from'] @_from ||= payload && payload['from']
end end
# Processes current update. # Processes current update.
@@ -154,16 +160,42 @@ module Telegram
end end
# Calculates action name and args for payload. # Calculates action name and args for payload.
# If payload is a message with command, then returned action is an # Uses `action_for_#{payload_type}` methods.
# action for this command. Otherwise it's the same as payload type. # If this method doesn't return anything
# it uses fallback with action same as payload type.
# Returns array `[is_command?, action, args]`. # Returns array `[is_command?, action, args]`.
def action_for_payload def action_for_payload
case payload_type if payload_type
when 'message' send("action_for_#{payload_type}") || [false, payload_type, [payload]]
cmd, args = self.class.command_from_text(payload['text'], bot_username) else
cmd &&= self.class.action_for_command(cmd) [false, :unsupported_payload_type, []]
[true, cmd, args] if cmd end
end || [false, payload_type, [payload]] end
# If payload is a message with command, then returned action is an
# action for this command.
# Separate method, so it can be easily overriden (ex. MessageContext).
def action_for_message
cmd, args = self.class.command_from_text(payload['text'], bot_username)
cmd &&= self.class.action_for_command(cmd)
[true, cmd, args] if cmd
end
# It doesn't extract commands from edited messages. Just process
# them as usual ones.
def action_for_edited_message
end
def action_for_inline_query
[false, payload_type, [payload['query'], payload['offset']]]
end
def action_for_chosen_inline_result
[false, payload_type, [payload['result_id'], payload['query']]]
end
def action_for_callback_query
[false, payload_type, [payload['data']]]
end end
# Silently ignore unsupported messages. # Silently ignore unsupported messages.
@@ -171,32 +203,6 @@ module Telegram
def action_missing(*) def action_missing(*)
end end
# Helper to call bot's `send_#{type}` method with already set `chat_id` and
# `reply_to_message_id`:
#
# reply_with :message, text: 'Hello!'
# reply_with :message, text: '__Hello!__', parse_mode: :Markdown
# reply_with :photo, photo: File.open(photo_to_send), caption: "It's incredible!"
def reply_with(type, params)
method = "send_#{type}"
chat = self.chat
payload = self.payload
params = params.merge(
chat_id: (chat && chat['id'] or raise 'Can not reply_with when chat is not present'),
reply_to_message: payload && payload['message_id'],
)
bot.public_send(method, params)
end
# Same as reply_with, but for inline queries.
def answer_inline_query(results, params = {})
params = params.merge(
inline_query_id: payload['id'],
results: results,
)
bot.answer_inline_query(params)
end
ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self) ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self)
end end
end end

Просмотреть файл

@@ -0,0 +1,33 @@
module Telegram
module Bot
class UpdatesController
# Helpers for botan.io metrics.
module Botan
class MissingFrom < Error; end
protected
def botan
@botan ||= bot.try!(:botan)
end
# Track custom event for user taken from `from` field:
#
# botan_track :my_event, {data: :val}
#
def botan_track(event, data = {})
raise MissingFrom, 'Can not track without user' unless from
botan.try! { |x| x.track(event, from['id'], data) }
end
# Track current action and payload for current user. Best used with `before_action`:
#
# before_action :botan_track_action
#
def botan_track_action
botan_track(action_name, payload)
end
end
end
end
end

Просмотреть файл

@@ -0,0 +1,34 @@
module Telegram
module Bot
class UpdatesController
# Use separate actions for different callback queries.
# It doesn't require session support. Simply add `%{context}:` prefix to data.
module CallbackQueyContext
protected
# Uses #context_from_callback_query as context name.
# If context is present checks if `%context%_callback_query` is valid
# action method and returns it if so. Context is stripped from data
# in this case. Otherwise returns `super`.
#
# It wont raise ActionNotFound as MessageContext does,
# because `data` param is controlled by client.
def action_for_callback_query
context, new_data = context_from_callback_query
# binding.pry
if context
action_name = "#{context}_callback_query"
[false, action_name, [new_data]] if action_method?(action_name)
end || super
end
def context_from_callback_query
data = payload['data']
return unless data
parts = data.split(':', 2)
parts if parts.size > 1
end
end
end
end
end

Просмотреть файл

@@ -6,7 +6,7 @@ module Telegram
module Instrumentation module Instrumentation
class << self class << self
def prepended(base) def prepended(base)
base.config_accessor :logger base.send :config_accessor, :logger
base.extend ClassMethods base.extend ClassMethods
end end
@@ -35,8 +35,14 @@ module Telegram
end end
end end
def reply_with(type, *) def respond_with(type, *)
Instrumentation.instrument(:reply_with, type: type) { super } Instrumentation.instrument(:respond_with, type: type) { super }
end
%i(answer_callback_query answer_inline_query).each do |type|
define_method(type) do |*args|
Instrumentation.instrument(:respond_with, type: type) { super(*args) }
end
end end
private private

Просмотреть файл

@@ -22,8 +22,8 @@ module Telegram
end end
end end
def reply_with(event) def respond_with(event)
info { "Replied with #{event.payload[:type]}" } info { "Responded with #{event.payload[:type]}" }
end end
def halted_callback(event) def halted_callback(event)

Просмотреть файл

@@ -10,7 +10,6 @@ module Telegram
included do included do
# As we use before_action context is cleared anyway, # As we use before_action context is cleared anyway,
# no matter we used it or not. # no matter we used it or not.
before_action :fetch_context
singleton_class.send :attr_reader, :context_handlers, :context_to_action singleton_class.send :attr_reader, :context_handlers, :context_to_action
@context_handlers = {} @context_handlers = {}
end end
@@ -18,26 +17,26 @@ module Telegram
module ClassMethods module ClassMethods
# Registers handler for context. # Registers handler for context.
# #
# context_handler :rename do |message| # context_handler :rename do |*|
# resource.update!(name: message['text']) # resource.update!(name: payload['text'])
# end # end
# #
# # To run other action with all the callbacks: # # To run other action with all the callbacks:
# context_handler :rename do |message| # context_handler :rename do |*words|
# process(:rename, *m['text'].split) # process(:rename, *words)
# end # end
# #
# # Or just # # Or just
# context_handler :rename, :your_action_to_call # context_handler :rename, :your_action_to_call
# context_handler :rename # to call :rename # context_handler :rename # to call :rename
# #
# # For messages without context use this instead of `message` method:
# context_handle do |message|
# end
#
def context_handler(context = nil, action = nil, &block) def context_handler(context = nil, action = nil, &block)
context &&= context.to_sym context &&= context.to_sym
context_handlers[context] = block || action || context if block
action = "_context_handler_#{context}"
define_method(action, &block)
end
context_handlers[context] = action || context
end end
# Use it to use context value as action name for all contexts # Use it to use context value as action name for all contexts
@@ -49,20 +48,9 @@ module Telegram
end end
end end
# Finds handler for current context and processes message with it.
def message(message)
handler = handler_for_context
return unless handler
if handler.respond_to?(:call)
instance_exec(message, &handler)
else
process(handler, *message['text'].split)
end
end
# Action to clear context. # Action to clear context.
def cancel def cancel
# Context is already cleared in before_action # Context is already cleared in action_for_message
end end
private private
@@ -71,11 +59,15 @@ module Telegram
# according to previous request. # according to previous request.
attr_reader :context attr_reader :context
# Fetches and removes context from session. # Fetches context and finds handler for it. If message has new command,
def fetch_context # it has higher priority than contextual action.
def action_for_message
val = session.delete(:context) val = session.delete(:context)
@context = val && val.to_sym @context = val && val.to_sym
true # TODO: remove in Rails 5.0 super || context && begin
handler = handler_for_context
[true, handler, payload['text'].try!(:split) || []] if handler
end
end end
# Save context for the next request. # Save context for the next request.

Просмотреть файл

@@ -0,0 +1,44 @@
module Telegram
module Bot
class UpdatesController
module ReplyHelpers
# Helper to call bot's `send_#{type}` method with already set `chat_id`:
#
# respond_with :message, text: 'Hello!'
# respond_with :message, text: '__Hello!__', parse_mode: :Markdown
# respond_with :photo, photo: File.open(photo_to_send), caption: "It's incredible!"
def respond_with(type, params)
chat = self.chat
chat_id = chat && chat['id'] or raise 'Can not respond_with when chat is not present'
bot.public_send("send_#{type}", params.merge(chat_id: chat_id))
end
# Same as respond_with but also sets `reply_to_message_id`.
def reply_with(type, params)
payload = self.payload
message_id = payload && payload['message_id']
params = params.merge(reply_to_message_id: message_id) if message_id
respond_with(type, params)
end
# Same as reply_with, but for inline queries.
def answer_inline_query(results, params = {})
params = params.merge(
inline_query_id: payload['id'],
results: results,
)
bot.answer_inline_query(params)
end
# Same as reply_with, but for callback queries.
def answer_callback_query(text, params = {})
params = params.merge(
callback_query_id: payload['id'],
text: text,
)
bot.answer_callback_query(params)
end
end
end
end
end

Просмотреть файл

@@ -17,11 +17,15 @@ module Telegram
protected protected
def session def session
@_session ||= SessionHash.new(self.class.session_store, session_key) @_session ||= begin
key = session_key
key ? SessionHash.new(self.class.session_store, key) : NullSessionHash.new
end
end end
def session_key def session_key
"#{bot.username}:#{from ? "from:#{from['id']}" : "chat:#{chat['id']}"}" subject = from || chat
"#{bot.username}:#{subject['id']}" if subject
end end
# Rack::Session::Abstract::SessionHash is taken to provide lazy loading. # Rack::Session::Abstract::SessionHash is taken to provide lazy loading.
@@ -59,6 +63,18 @@ module Telegram
end end
end end
class NullSessionHash < Session::SessionHash
def initialize
@data = {}
@loaded = true
@exists = true
end
alias_method :destroy, :clear
alias_method :load!, :id
alias_method :commit, :id
end
module ConfigMethods module ConfigMethods
delegate :session_store, to: :config delegate :session_store, to: :config

Просмотреть файл

@@ -27,19 +27,7 @@ module Telegram
# Stubs session. # Stubs session.
def session def session
@_session ||= Testing::SessionHash.new @_session ||= Session::NullSessionHash.new
end
class SessionHash < Session::SessionHash
def initialize
@data = {}
@loaded = true
@exists = true
end
alias_method :destroy, :clear
alias_method :load!, :id
alias_method :commit, :id
end end
end end
end end

Просмотреть файл

@@ -77,11 +77,25 @@ module Telegram
def reload! def reload!
return yield unless reload return yield unless reload
ActionDispatch::Reloader.prepare! reloading_code do
if controller.is_a?(Class) && controller.name if controller.is_a?(Class) && controller.name
@controller = Object.const_get(controller.name) @controller = Object.const_get(controller.name)
end
yield
end
end
if defined?(Rails) && Rails.application.respond_to?(:reloader)
def reloading_code
Rails.application.reloader.wrap do
yield
end
end
else
def reloading_code
ActionDispatch::Reloader.prepare!
yield.tap { ActionDispatch::Reloader.cleanup! }
end end
yield.tap { ActionDispatch::Reloader.cleanup! }
end end
end end
end end

Просмотреть файл

@@ -1,6 +1,6 @@
module Telegram module Telegram
module Bot module Bot
VERSION = '0.5.0'.freeze VERSION = '0.8.0'.freeze
def self.gem_version def self.gem_version
Gem::Version.new VERSION Gem::Version.new VERSION

Просмотреть файл

@@ -1,10 +1,12 @@
RSpec.describe Telegram::Bot::Client do RSpec.describe Telegram::Bot::Client do
let(:instance) { described_class.new 'token' }
let(:token) { 'token' }
let(:botan_token) { double(:botan_token) }
describe '.wrap' do describe '.wrap' do
subject { described_class.wrap(input) } subject { described_class.wrap(input) }
let(:result) { double(:result) } let(:result) { double(:result) }
let(:token) { 'token' }
let(:username) { 'username' } let(:username) { 'username' }
let(:instance) { described_class.new 'token' }
context 'when input is a string' do context 'when input is a string' do
let(:input) { token } let(:input) { token }
@@ -19,9 +21,20 @@ RSpec.describe Telegram::Bot::Client do
let(:input) { {token: token, username: username, ignore: :ignore} } let(:input) { {token: token, username: username, ignore: :ignore} }
it 'extracts token & username' do it 'extracts token & username' do
expect(described_class).to receive(:new).with(token, username) { result } expect(described_class).to receive(:new).
with(token, username, botan: nil) { result }
should eq result should eq result
end end
context 'when `botan` is given' do
let(:input) { super().merge(botan: botan_token) }
it 'passes it to initializer' do
expect(described_class).to receive(:new).
with(token, username, botan: botan_token) { result }
should eq result
end
end
end end
context 'when input is an instance of described_class' do context 'when input is an instance of described_class' do
@@ -50,7 +63,7 @@ RSpec.describe Telegram::Bot::Client do
it 'calls wrap for every element' do it 'calls wrap for every element' do
expect(described_class).to receive(:new).with('other_token') { result } expect(described_class).to receive(:new).with('other_token') { result }
expect(described_class).to receive(:new).with(token, username) { result_2 } expect(described_class).to receive(:new).with(token, username, botan: nil) { result_2 }
should eq [result, instance, result_2] should eq [result, instance, result_2]
end end
end end
@@ -60,7 +73,7 @@ RSpec.describe Telegram::Bot::Client do
subject { described_class.prepare_body(input) } subject { described_class.prepare_body(input) }
context 'when plain hash is given' do context 'when plain hash is given' do
let(:input) { {a: 1, b: '2', c: [1, 2]} } let(:input) { {a: 1, b: '2', c: nil} }
it { should eq input } it { should eq input }
end end
@@ -69,9 +82,20 @@ RSpec.describe Telegram::Bot::Client do
it 'encodes nested hashes to json' do it 'encodes nested hashes to json' do
expected = input.dup expected = input.dup
%i(d e).each { |x| expected[x] = expected[x].to_json } %i(c d e).each { |x| expected[x] = expected[x].to_json }
should eq expected should eq expected
end end
end end
end end
describe '#botan' do
subject { instance.botan }
it { should eq nil }
context 'when botan token is set' do
let(:instance) { described_class.new token, botan: botan_token }
it { should be_instance_of Telegram::Bot::Botan }
its(:token) { should eq botan_token }
end
end
end end

Просмотреть файл

@@ -1,3 +1,5 @@
require 'rack/mock'
RSpec.describe Telegram::Bot::Middleware do RSpec.describe Telegram::Bot::Middleware do
let(:instance) { described_class.new bot, controller } let(:instance) { described_class.new bot, controller }
let(:bot) { double(:bot) } let(:bot) { double(:bot) }
@@ -6,10 +8,25 @@ RSpec.describe Telegram::Bot::Middleware do
describe '#call' do describe '#call' do
subject { instance.call(env) } subject { instance.call(env) }
let(:env) { {'action_dispatch.request.request_parameters' => json_body} } let(:env) { {'action_dispatch.request.request_parameters' => json_body} }
let(:json_body) { double(:json_body) } let(:update) { {'message' => {'id' => 1}} }
let(:env) do
Rack::MockRequest.env_for('/',
method: :post,
input: JSON.dump(update),
'CONTENT_TYPE' => 'application/json',
)
end
require 'action_pack/version'
if ActionPack::VERSION::MAJOR < 5
# Before Rails 5, params are parsed in middleware.
# In Rails 5, they are parsed in Request#request_parameters.
require 'action_dispatch/middleware/params_parser'
let(:instance) { ActionDispatch::ParamsParser.new(super()) }
end
it 'calls dispatch on controller' do it 'calls dispatch on controller' do
expect(controller).to receive(:dispatch).with(bot, json_body) expect(controller).to receive(:dispatch).with(bot, update)
subject subject
end end

Просмотреть файл

@@ -0,0 +1,54 @@
RSpec.describe Telegram::Bot::UpdatesController::Botan do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
described_class = self.described_class
Class.new(Telegram::Bot::UpdatesController) do
include described_class
end
end
let(:botan) { double(:botan) }
let(:result) { double(:result) }
let(:payload) { {from: {id: user_id}} }
let(:payload_type) { :message }
let(:user_id) { double(:user_id) }
before { allow(bot).to receive(:botan) { botan } }
shared_examples 'basic tracking' do
context 'when botan is not configured' do
let(:botan) {}
it { should eq nil }
end
context 'when `from` is empty' do
let(:payload) { {text: 'test'} }
it { expect { subject }.to raise_error described_class::MissingFrom }
end
end
describe '#botan_track' do
subject { controller.send(:botan_track, event, data) }
let(:event) { double(:event) }
let(:data) { double(:data) }
it 'calls botan#track' do
expect(botan).to receive(:track).with(event, user_id, data) { result }
should eq result
end
include_examples 'basic tracking'
end
describe '#botan_track_action' do
subject { controller.send(:botan_track_action) }
let(:action_name) { double(:action_name) }
it 'calls botan#track with current action and payload' do
expect(controller).to receive(:action_name) { action_name }
expect(botan).to receive(:track).
with(action_name, user_id, deep_stringify(payload)) { result }
should eq result
end
include_examples 'basic tracking'
end
end

Просмотреть файл

@@ -0,0 +1,65 @@
RSpec.describe Telegram::Bot::UpdatesController::CallbackQueyContext do
include_context 'telegram/bot/updates_controller'
let(:controller_class) do
described_class = self.described_class
Class.new(Telegram::Bot::UpdatesController) do
include described_class
attr_accessor :filter_done
before_action only: :ctx2_callback_query do
self.filter_done = true
end
def callback_query(data)
[:no_context, data]
end
def ctx1_callback_query(data)
[:ctx1, data]
end
def ctx2_callback_query(data)
[:ctx2, data]
end
end
end
describe '#dispatch' do
subject { -> { dispatch } }
let(:payload_type) { :callback_query }
let(:payload) { {'data' => data} }
let(:data) { text }
let(:text) { 'asd qwe zxc' }
context 'when context is not set' do
its(:call) { should eq [:no_context, text] }
it { should_not change(controller, :filter_done) }
end
context 'when context is set' do
let(:data) { "#{ctx}:#{text}" }
context 'and valid' do
let(:ctx) { 'ctx1' }
its(:call) { should eq [:ctx1, text] }
it { should_not change(controller, :filter_done) }
context 'and context has callback' do
let(:ctx) { 'ctx2' }
its(:call) { should eq [:ctx2, text] }
it { should change(controller, :filter_done) }
end
context 'and data has multiple colons' do
let(:text) { super().tr(' ', ':') }
its(:call) { should eq [:ctx1, text] }
end
end
context 'and invalid' do
let(:ctx) { 'missing' }
its(:call) { should eq [:no_context, data] }
end
end
end
end

Просмотреть файл

@@ -10,7 +10,10 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
self.filter_done = true self.filter_done = true
end end
context_handler do |*args| attr_reader :callbacks_runs
before_action { @callbacks_runs = (@callbacks_runs || 0) + 1 }
def message(*args)
[:no_context, *args] [:no_context, *args]
end end
@@ -51,9 +54,20 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
context 'when context is handled by block' do context 'when context is handled by block' do
before { session[:context] = :block } before { session[:context] = :block }
its(:call) { should eq [:block_result, payload] } its(:call) { should eq [:block_result, *text.split] }
it { should_not change(controller, :filter_done) } it { should_not change(controller, :filter_done) }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
context 'when message has no text' do
let(:payload) { {'audio' => {'file_id' => 123}} }
its(:call) { should eq [:block_result] }
end
context 'when message has new command' do
let(:text) { '/action a s d' }
its(:call) { should eq [:action_result, *%w(a s d)] }
it { should change { session[:context] }.to nil }
end
end end
context 'when context is handled by short redirect' do context 'when context is handled by short redirect' do
@@ -61,6 +75,14 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
its(:call) { should eq [:method_result, *text.split] } its(:call) { should eq [:method_result, *text.split] }
it { should change(controller, :filter_done).to true } it { should change(controller, :filter_done).to true }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
it { should change(controller, :callbacks_runs).to 1 }
context 'when message has no text' do
let(:payload) { {'audio' => {'file_id' => 123}} }
its(:call) { should eq [:method_result] }
it { should change(controller, :filter_done).to true }
it { should change { session[:context] }.to nil }
end
end end
context 'when context is handled by custom redirect' do context 'when context is handled by custom redirect' do
@@ -72,7 +94,7 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
context 'when context is action`s name but not mapped' do context 'when context is action`s name but not mapped' do
before { session[:context] = :action } before { session[:context] = :action }
its(:call) { should eq nil } its(:call) { should eq [:no_context, payload] }
it { should_not change(controller, :filter_done) } it { should_not change(controller, :filter_done) }
it { should change { session[:context] }.to nil } it { should change { session[:context] }.to nil }
end end
@@ -80,6 +102,12 @@ RSpec.describe Telegram::Bot::UpdatesController::MessageContext do
context 'when context_to_action is true' do context 'when context_to_action is true' do
before { controller_class.context_to_action! } before { controller_class.context_to_action! }
context 'when context is not set' do
its(:call) { should eq [:no_context, payload] }
it { should_not change(controller, :filter_done) }
it { should_not change { session[:context] } }
end
context 'when context is action`s name but not mapped' do context 'when context is action`s name but not mapped' do
before { session[:context] = :action } before { session[:context] = :action }
its(:call) { should eq [:action_result, *text.split] } its(:call) { should eq [:action_result, *text.split] }

Просмотреть файл

@@ -0,0 +1,51 @@
RSpec.describe Telegram::Bot::UpdatesController do
include_context 'telegram/bot/updates_controller'
let(:params) { {arg: 1, 'other_arg' => 2} }
let(:respond_type) { :photo }
let(:result) { double(:result) }
let(:payload_type) { :message }
let(:payload) { {message_id: double(:message_id)} }
let(:chat) { {'id' => double(:chat_id)} }
shared_examples 'missing chat' do
context 'when chat is missing' do
let(:payload_type) { :some_type }
it { expect { subject }.to raise_error(/chat/) }
end
end
describe '#respond_with' do
subject { controller.respond_with respond_type, params }
include_examples 'missing chat'
it 'sets chat_id & reply_to_message_id' do
expect(controller).to receive(:chat) { chat }
expect(bot).to receive("send_#{respond_type}").
with(params.merge(chat_id: chat['id'])) { result }
should eq result
end
end
describe '#reply_with' do
subject { controller.reply_with respond_type, params }
include_examples 'missing chat'
it 'sets chat_id & reply_to_message_id' do
expect(controller).to receive(:chat) { chat }
expect(bot).to receive("send_#{respond_type}").with(params.merge(
chat_id: chat['id'],
reply_to_message_id: payload[:message_id],
)) { result }
should eq result
end
context 'when update is not set' do
let(:update) { {chat: chat} }
it 'sets chat_id' do
expect(bot).to receive("send_#{respond_type}").
with(params.merge(chat_id: chat['id'])) { result }
should eq result
end
end
end
end

Просмотреть файл

@@ -26,6 +26,12 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
def read def read
session[:text] session[:text]
end end
def action_missing(*)
[:action_missing, session[:text]].tap do
session[:text] = 'test'
end
end
end end
end end
@@ -39,5 +45,13 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do
expect(subject.call(bot, build_message('/read', id: 2))).to eq nil expect(subject.call(bot, build_message('/read', id: 2))).to eq nil
expect(subject.call(other_bot, build_message('/read', id: 1))).to eq nil expect(subject.call(other_bot, build_message('/read', id: 1))).to eq nil
end end
context 'payload is not supported' do
let(:payload_type) { '_unsupported_' }
it 'provides empty session' do
2.times { expect(subject.call(bot)).to eq [:action_missing, nil] }
expect(subject.call(other_bot)).to eq [:action_missing, nil]
end
end
end end
end end

Просмотреть файл

@@ -80,11 +80,36 @@ RSpec.describe Telegram::Bot::UpdatesController do
describe '#action_for_payload' do describe '#action_for_payload' do
subject { controller.action_for_payload } subject { controller.action_for_payload }
(described_class::PAYLOAD_TYPES - %w(message)).each do |type| def stub_payload(*fields)
context "when payload is #{type}" do Hash[fields.map { |x| [x, double(x)] }]
let(:payload_type) { type } end
it { should eq [false, type, [payload]] }
end context 'when payload is inline_query' do
let(:payload_type) { 'inline_query' }
let(:payload) { stub_payload(:id, :from, :location, :query, :offset) }
it { should eq [false, payload_type, payload.values_at(:query, :offset)] }
end
context 'when payload is chosen_inline_result' do
let(:payload_type) { 'chosen_inline_result' }
let(:payload) { stub_payload(:result_id, :from, :location, :inline_message_id, :query) }
it { should eq [false, payload_type, payload.values_at(:result_id, :query)] }
end
context 'when payload is callback_query' do
let(:payload_type) { 'callback_query' }
let(:payload) { stub_payload(:id, :from, :message, :inline_message_id, :data) }
it { should eq [false, payload_type, payload.values_at(:data)] }
end
context 'when payload is edited_message' do
let(:payload_type) { 'edited_message' }
it { should eq [false, payload_type, [payload]] }
end
context 'when payload is not supported' do
let(:payload_type) { '_unsupported_' }
it { should eq [false, :unsupported_payload_type, []] }
end end
context 'when payload is message' do context 'when payload is message' do
@@ -109,6 +134,11 @@ RSpec.describe Telegram::Bot::UpdatesController do
it { should eq [false, 'message', [payload]] } it { should eq [false, 'message', [payload]] }
end end
end end
context 'without text' do
let(:payload) { {'audio' => {'file_id' => 123}} }
it { should eq [false, payload_type, [payload]] }
end
end end
end end
@@ -195,12 +225,12 @@ RSpec.describe Telegram::Bot::UpdatesController do
it { should change(controller, :acted).to true } it { should change(controller, :acted).to true }
its(:call) { should eq [nil, nil, args] } its(:call) { should eq [nil, nil, args] }
context 'when callback returns false' do context 'when callback halts chain' do
before do before do
controller_class.prepend(Module.new do controller_class.prepend(Module.new do
def hook def hook
super super
false ActiveSupport::VERSION::MAJOR >= 5 ? throw(:abort) : false
end end
end) end)
end end
@@ -263,39 +293,4 @@ RSpec.describe Telegram::Bot::UpdatesController do
end end
end end
end end
describe '#reply_with' do
subject { controller.reply_with type, params }
let(:params) { {arg: 1, 'other_arg' => 2} }
let(:type) { :photo }
let(:result) { double(:result) }
let(:payload_type) { :message }
let(:payload) { {message_id: double(:message_id)} }
let(:chat) { {'id' => double(:chat_id)} }
it 'sets chat_id & reply_to_message' do
expect(controller).to receive(:chat) { chat }
expect(bot).to receive("send_#{type}").with(params.merge(
chat_id: chat['id'],
reply_to_message: payload[:message_id],
)) { result }
should eq result
end
context 'when chat is missing' do
let(:payload_type) { :some_type }
it { expect { subject }.to raise_error(/chat/) }
end
context 'when update is not set' do
let(:update) { {chat: chat} }
it 'sets chat_id & reply_to_message' do
expect(bot).to receive("send_#{type}").with(params.merge(
chat_id: chat['id'],
reply_to_message: nil,
)) { result }
should eq result
end
end
end
end end

Просмотреть файл

@@ -2,4 +2,22 @@ RSpec.describe Telegram::Bot do
it 'has a version number' do it 'has a version number' do
expect(described_class::VERSION).not_to be nil expect(described_class::VERSION).not_to be nil
end end
describe described_class::StaleChat do
describe '.match_response?' do
subject { ->(val) { described_class.match_response?(val) } }
it 'returns true for specific errors' do
expect(subject.call({})).to eq false
expect(subject.call('description' => 'test')).to eq false
expect(subject.call('description' => 'Error: bot was kicked from')).to eq true
expect(subject.call(
'description' => "Forbidden: can't write to private chat with deleted user"
)).to eq true
expect(subject.call(
'description' => 'Bad request: group chat is deactivated'
)).to eq true
end
end
end
end end

Просмотреть файл

@@ -20,8 +20,8 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = '~> 2.0' spec.required_ruby_version = '~> 2.0'
spec.add_dependency 'activesupport', '~> 4.0' spec.add_dependency 'activesupport', '>= 4.0', '< 5.1'
spec.add_dependency 'actionpack', '~> 4.0' spec.add_dependency 'actionpack', '>= 4.0', '< 5.1'
spec.add_dependency 'httpclient', '~> 2.7' spec.add_dependency 'httpclient', '~> 2.7'
spec.add_development_dependency 'bundler', '~> 1.11' spec.add_development_dependency 'bundler', '~> 1.11'
spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rake', '~> 10.0'