telegram-bot 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d72dfb00c1dab4bf5b80276354ff102737b537a02c1ba40656bb9ab6eb28288
4
- data.tar.gz: ed15a4fa045c319c035ab4b5ec9b06169b4a7455004de59fbaeac7656de7c795
3
+ metadata.gz: a35f4302d428cf24b66fa6122bcb5852b8680b85259bd2dd070a6a8e9aff5b74
4
+ data.tar.gz: 53fa7c7a14b637cbcae403b87786e93c2b685fa2461cbef9edd9e68c869f9206
5
5
  SHA512:
6
- metadata.gz: d2cd894d342bb01a901b2c24a243165b580adfd5bcbaef632d3d272693424b5b2f13b7236edcf32dcb897d123f68410c97e0c3235106e31f4de06ace5a712406
7
- data.tar.gz: cb95a89bcf60e2993d6011203c7f6e4e56f725e2455cbe31e351f60fd72f9e2e40296da284c1147f36f4dcb5f9d004efa2f99aa8791563e3282e8ece64c29499
6
+ metadata.gz: 7d788d67fcc120b5f4ce0ac6299a3e6ab1015213b1a8710f1f7ea4311deca27381befae700ff708813f1c2d47dfbc9144c6cd901b5a39d8ca4843b405c7b2124
7
+ data.tar.gz: '09b511503e001cd6fe9a25072a384ac04b7c0de2a4e728894d3c4c10a5badcbb7872072fb9f3d10cf3c5cbae0edc14d7bb3ee70966ecaa01f09d6c8844edbf01'
@@ -1,3 +1,25 @@
1
+ # Unreleased
2
+
3
+ # 0.14.0
4
+
5
+ - Make integration & controller specs consistent.
6
+ __Breaking changes__ for controller specs:
7
+ - Changed signature `dispatch(bot, update) => dispatch(update, bot)`.
8
+ - `update` helper is symbolized by default.
9
+ - `build_update(type, data)` is dropped in favor of `deep_stringify(type => data)`.
10
+ - Provide support for integration testing of bots in poller mode and non-Rails apps.
11
+ __Breaking changes__:
12
+ - Requiring `telegram/bot/rspec/integration` is deprecated in favor of
13
+ `telegram/bot/rspec/integration/rails`.
14
+ - `:telegram_bot` rspec tag is replaced with `telegram_bot: :rails`.
15
+ - __Breaking change__. Use bang-methods as actions for commands.
16
+ This prevents calling context contextual actions and payload specific actions with commands.
17
+ Translation helper strips `!` from action name for lazy translations.
18
+ - __Breaking change__. Drop `.context_handler`, `.context_to_action!` methods.
19
+ Use pass action name directly to `#save_context`.
20
+ It's the same as `.context_to_action!` is enabled by default.
21
+ - Class-level helper for lazy translations.
22
+
1
23
  # 0.13.1
2
24
 
3
25
  - Extracted typed response mappings to telegram-bot-types gem.
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  [![Code Climate](https://codeclimate.com/github/telegram-bot-rb/telegram-bot/badges/gpa.svg)](https://codeclimate.com/github/telegram-bot-rb/telegram-bot)
5
5
  [![Build Status](https://travis-ci.org/telegram-bot-rb/telegram-bot.svg)](https://travis-ci.org/telegram-bot-rb/telegram-bot)
6
6
 
7
+ __Breaking changes in v0.14!__ See [upgrading guide](https://github.com/telegram-bot-rb/telegram-bot/wiki/Upgrading-to-0.14).
8
+
7
9
  Tools for developing Telegram bots. Best used with Rails, but can be used in
8
10
  [standalone app](https://github.com/telegram-bot-rb/telegram-bot/wiki/Not-rails-application).
9
11
  Supposed to be used in webhook-mode in production, and poller-mode
@@ -154,14 +156,11 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
154
156
  # chosen_inline_result(result_id, query)
155
157
  # callback_query(data)
156
158
 
157
- # Define public methods to respond to commands.
159
+ # Define public methods ending with `!` to handle commands.
158
160
  # Command arguments will be parsed and passed to the method.
159
161
  # Be sure to use splat args and default values to not get errors when
160
162
  # someone passed more or less arguments in the message.
161
- #
162
- # For some commands like /message or /123 method names should start with
163
- # `on_` to avoid conflicts.
164
- def start(data = nil, *)
163
+ def start!(data = nil, *)
165
164
  # do_smth_with(data)
166
165
 
167
166
  # There are `chat` & `from` shortcut methods.
@@ -192,8 +191,9 @@ end
192
191
  #### Reply helpers
193
192
 
194
193
  There are helpers to respond for basic actions. They just set chat/message/query
195
- identifiers from update. See [`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb) module for more information.
196
- Here are this methods signatures:
194
+ identifiers from update. See
195
+ [`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb)
196
+ module for more information. Here are this methods signatures:
197
197
 
198
198
  ```ruby
199
199
  def respond_with(type, params); end
@@ -256,11 +256,11 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
256
256
  # You can override global config for this controller.
257
257
  self.session_store = :file_store
258
258
 
259
- def write(text = nil, *)
259
+ def write!(text = nil, *)
260
260
  session[:text] = text
261
261
  end
262
262
 
263
- def read(*)
263
+ def read!(*)
264
264
  respond_with :message, text: session[:text]
265
265
  end
266
266
 
@@ -283,35 +283,28 @@ it asks you for additional argument. There is `MessageContext` for this:
283
283
  class Telegram::WebhookController < Telegram::Bot::UpdatesController
284
284
  include Telegram::Bot::UpdatesController::MessageContext
285
285
 
286
- def rename(*)
286
+ def rename!(*)
287
287
  # set context for the next message
288
- save_context :rename
288
+ save_context :rename_from_message
289
289
  respond_with :message, text: 'What name do you like?'
290
290
  end
291
291
 
292
292
  # register context handlers to handle this context
293
- context_handler :rename do |*words|
293
+ def rename_from_message(*words)
294
294
  update_name words[0]
295
295
  respond_with :message, text: 'Renamed!'
296
296
  end
297
297
 
298
- # You can do it in other way:
299
- def rename(name = nil, *)
298
+ # You can use same action name as context name:
299
+ def rename!(name = nil, *)
300
300
  if name
301
301
  update_name name
302
302
  respond_with :message, text: 'Renamed!'
303
303
  else
304
- save_context :rename
304
+ save_context :rename!
305
305
  respond_with :message, text: 'What name do you like?'
306
306
  end
307
307
  end
308
-
309
- # This will call #rename like if it is called with message '/rename %text%'
310
- context_handler :rename
311
-
312
- # If you have a lot of such methods you can call this method
313
- # to use context value as action name for all contexts which miss handlers:
314
- context_to_action!
315
308
  end
316
309
  ```
317
310
 
@@ -394,11 +387,11 @@ Telegram::Bot::UpdatesPoller.start(bot, controller_class)
394
387
 
395
388
  ### Testing
396
389
 
397
- There is `Telegram::Bot::ClientStub` class to stub client for tests.
398
- Instead of performing API requests it stores them in `requests` hash.
390
+ There is a `Telegram::Bot::ClientStub` class to stub client for tests.
391
+ Instead of performing API requests it stores them in a `requests` hash.
399
392
 
400
393
  To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before
401
- initializing clients. Here is template for RSpec:
394
+ initializing clients. Here is a template for RSpec:
402
395
 
403
396
  ```ruby
404
397
  # environments/test.rb
@@ -416,48 +409,83 @@ RSpec.configure do |config|
416
409
  end
417
410
  ```
418
411
 
419
- There are integration and controller contexts for RSpec and some built-in matchers:
412
+ RSpec contexts and helpers are included automatically for groups and examples with matching
413
+ tags. In RSpec < 3.4 it's required to use `include_context` explicitly.
414
+ See [list of available helpers](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec)
415
+ for details.
416
+
417
+ There are 3 types of integration tests:
418
+
419
+ - `:rails` - for testing bot in webhooks-mode in Rails application.
420
+ It simulates webhook requests POSTing data to controller's endpoint.
421
+ It works on the top of requests specs, so `rspec-rails` gem is required.
422
+ - `:rack` - For testing bot in webhooks-mode in non-Rails application.
423
+ It uses `rack-test` gem to POST requests to bot's endpoint.
424
+ - `:poller` - Calls `.dispatch` directly on controller class.
425
+
426
+ Pick the appropriate one, then require `telegram/bot/rspec/integration/#{type}`
427
+ and mark spec group with tag `telegram_bot: type`. See configuration options
428
+ for each type in
429
+ [telegram/bot/rspec/integration/](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec/integration).
430
+
431
+ Here is an example test for a Rails app:
420
432
 
421
433
  ```ruby
422
434
  # spec/requests/telegram_webhooks_spec.rb
423
- require 'telegram/bot/rspec/integration'
435
+ require 'telegram/bot/rspec/integration/rails'
436
+
437
+ RSpec.describe TelegramWebhooksController, telegram_bot: :rails do
438
+ # for old RSpec:
439
+ # include_context 'telegram/bot/integration/rails'
440
+
441
+ # Main method is #dispatch(update). Some helpers are:
442
+ # dispatch_message(text, options = {})
443
+ # dispatch_command(cmd, *args)
424
444
 
425
- RSpec.describe TelegramWebhooksController, :telegram_bot do
426
- # for old rspec add:
427
- # include_context 'telegram/bot/integration'
445
+ # Available matchers can be found in Telegram::Bot::RSpec::ClientMatchers.
446
+ it 'shows usage of basic matchers'
447
+ # The most basic one is #make_telegram_request(bot, endpoint, params_matcher)
448
+ expect { dispatch_command(:start) }.
449
+ to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text'))
428
450
 
429
- describe '#start' do
451
+ # There are some shortcuts for dispatching basic updates and testing responses.
452
+ expect { dispatch_message('Hi') }.to send_telegram_message(bot, /msg regexp/, some: :option)
453
+ end
454
+
455
+ describe '#start!' do
430
456
  subject { -> { dispatch_command :start } }
457
+ # Using built in matcher for `respond_to`:
431
458
  it { should respond_with_message 'Hi there!' }
432
459
  end
433
460
 
434
- # There is context for callback queries with related matchers.
461
+ # There is context for callback queries with related matchers,
462
+ # use :callback_query tag to include it.
435
463
  describe '#hey_callback_query', :callback_query do
436
464
  let(:data) { "hey:#{name}" }
437
465
  let(:name) { 'Joe' }
438
466
  it { should answer_callback_query('Hey Joe') }
439
467
  it { should edit_current_message :text, text: 'Done' }
468
+ end
440
469
  end
470
+ ```
471
+
472
+ There is a context for testing bot controller in the way similar to Rails controller tests.
473
+ It's supposed to be a low-level alternative for integration tests. Among the differences is
474
+ that controller tests use a single controller instance for all dispatches in specific exaple,
475
+ session is stubbed (does not use configured store engine), and update is not serialized
476
+ so it also supports mocks. This can be useful for unit testing, but should not be used as
477
+ the default way to test the bot.
441
478
 
442
- # For controller specs use
479
+ ```ruby
443
480
  require 'telegram/bot/updates_controller/rspec_helpers'
444
481
  RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do
445
- # for old rspec add:
482
+ # for old RSpec:
446
483
  # include_context 'telegram/bot/updates_controller'
447
- end
448
-
449
- # Matchers are available for custom specs:
450
- include Telegram::Bot::RSpec::ClientMatchers
451
484
 
452
- expect(&process_update).to send_telegram_message(bot, /msg regexp/, some: :option)
453
- expect(&process_update).
454
- to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text'))
485
+ # Same helpers and matchers like dispatch_command, answer_callback_query are available here.
486
+ end
455
487
  ```
456
488
 
457
- Place integration tests inside `spec/requests`
458
- when using RSpec's `infer_spec_type_from_file_location!`,
459
- or just add `type: :request` to `describe`.
460
-
461
489
  See sample app for more examples.
462
490
 
463
491
  ### Deployment
@@ -14,10 +14,10 @@ module Telegram
14
14
 
15
15
  module_function
16
16
 
17
- def deprecation_0_14
17
+ def deprecation_0_15
18
18
  @deprecation ||= begin
19
19
  require 'active_support/deprecation'
20
- ActiveSupport::Deprecation.new('0.14', 'Telegram::Bot')
20
+ ActiveSupport::Deprecation.new('0.15', 'Telegram::Bot')
21
21
  end
22
22
  end
23
23
 
@@ -1,8 +1,7 @@
1
1
  require 'active_support/concern'
2
2
  require 'active_support/core_ext/hash/indifferent_access'
3
3
  require 'active_support/json'
4
- require 'action_dispatch/http/mime_type'
5
- require 'action_dispatch/http/request'
4
+ require 'action_dispatch'
6
5
 
7
6
  module Telegram
8
7
  module Bot
@@ -24,33 +24,6 @@ module Telegram
24
24
  end
25
25
  end
26
26
 
27
- # # Create routes for all Telegram.bots to use same controller:
28
- # telegram_webhooks TelegramController
29
- #
30
- # # Or pass custom bots usin any of supported config options:
31
- # telegram_webhooks TelegramController, [
32
- # bot,
33
- # {token: token, username: username},
34
- # other_bot_token,
35
- # ]
36
- def telegram_webhooks(controllers, bots = nil, **options)
37
- Bot.deprecation_0_14.deprecation_warning(:telegram_webhooks, <<-TXT.strip_heredoc)
38
- It brings unnecessary complexity and encourages writeng less readable code.
39
- Please use telegram_webhook method instead.
40
- It's signature `telegram_webhook(controller, bot = :default, **options)`.
41
- Multiple-bot environments now requires calling this method in a loop
42
- or using statement for each bot.
43
- TXT
44
- unless controllers.is_a?(Hash)
45
- bots = bots ? Array.wrap(bots) : Telegram.bots.values
46
- controllers = Hash[bots.map { |x| [x, controllers] }]
47
- end
48
- controllers.each do |bot, controller|
49
- controller, bot_options = controller if controller.is_a?(Array)
50
- telegram_webhook(controller, bot, options.merge(bot_options || {}))
51
- end
52
- end
53
-
54
27
  # Define route which processes requests using given controller and bot.
55
28
  #
56
29
  # telegram_webhook TelegramController, bot
@@ -2,6 +2,15 @@ module Telegram
2
2
  module Bot
3
3
  module RSpec
4
4
  autoload :ClientMatchers, 'telegram/bot/rspec/client_matchers'
5
+
6
+ module_function
7
+
8
+ # Yelds a block if `include_context` is supported.
9
+ def with_include_context
10
+ ::RSpec.configure do |config|
11
+ yield(config) if config.respond_to?(:include_context)
12
+ end
13
+ end
5
14
  end
6
15
  end
7
16
  end
@@ -0,0 +1,39 @@
1
+ require 'telegram/bot/rspec'
2
+ require 'telegram/bot/rspec/message_helpers'
3
+
4
+ # Shared helpers for testing callback query updates.
5
+ RSpec.shared_context 'telegram/bot/callback_query' do
6
+ include_context 'telegram/bot/message_helpers'
7
+
8
+ subject { -> { dispatch callback_query: payload } }
9
+ let(:payload) { {id: callback_query_id, from: from, message: message, data: data} }
10
+ let(:callback_query_id) { 11 }
11
+ let(:message_id) { 22 }
12
+ let(:message) { {message_id: message_id, chat: chat, text: 'message text'} }
13
+ let(:data) { raise '`let(:data) { "callback query data here" }` is required' }
14
+
15
+ # Matcher to check that origin message got edited.
16
+ def edit_current_message(type, options = {})
17
+ description = 'edit current message'
18
+ options = options.merge(
19
+ message_id: message[:message_id],
20
+ chat_id: chat_id,
21
+ )
22
+ Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new(
23
+ bot, :"editMessage#{type.to_s.camelize}", description: description
24
+ ).with(hash_including(options))
25
+ end
26
+
27
+ # Matcher to check that callback query is answered.
28
+ def answer_callback_query(text = Regexp.new(''), options = {})
29
+ description = "answer callback query with #{text.inspect}"
30
+ text = a_string_matching(text) if text.is_a?(Regexp)
31
+ options = options.merge(
32
+ callback_query_id: payload[:id],
33
+ text: text,
34
+ )
35
+ Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new(
36
+ bot, :answerCallbackQuery, description: description
37
+ ).with(hash_including(options))
38
+ end
39
+ end
@@ -1,85 +1,10 @@
1
- RSpec.shared_context 'telegram/bot/integration' do
2
- let(:bot) { Telegram.bot }
3
- let(:default_message_options) { {from: from, chat: chat} }
4
- let(:from) { {id: from_id} }
5
- let(:from_id) { 123 }
6
- let(:chat) { {id: chat_id} }
7
- let(:chat_id) { 456 }
8
- let(:controller_path) do
9
- route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
10
- Rails.application.routes.url_helpers.public_send("#{route_name}_path")
11
- end
12
- let(:request_headers) do
13
- {
14
- 'ACCEPT' => 'application/json',
15
- 'Content-Type' => 'application/json',
16
- }
17
- end
18
- let(:clear_session?) { described_class.respond_to?(:session_store) }
19
- before { described_class.session_store.try!(:clear) if clear_session? }
20
-
21
- include Telegram::Bot::RSpec::ClientMatchers
22
-
23
- def dispatch(update)
24
- if ActionPack::VERSION::MAJOR >= 5
25
- post(controller_path, params: update.to_json, headers: request_headers)
26
- else
27
- post(controller_path, update.to_json, request_headers)
28
- end
29
- end
30
-
31
- def dispatch_message(text, options = {})
32
- dispatch message: default_message_options.merge(options).merge(text: text)
33
- end
34
-
35
- def dispatch_command(*args)
36
- options = args.last.is_a?(Hash) ? args.pop : {}
37
- dispatch_message("/#{args.join ' '}", options)
38
- end
39
-
40
- # Matcher to check response. Make sure to define `let(:chat_id)`.
41
- def respond_with_message(expected = Regexp.new(''))
42
- raise 'Define chat_id to use respond_with_message' unless defined?(chat_id)
43
- send_telegram_message(bot, expected, chat_id: chat_id)
44
- end
45
- end
46
-
47
- RSpec.shared_context 'telegram/bot/callback_query', callback_query: true do
48
- include_context 'telegram/bot/integration'
49
-
50
- subject { -> { dispatch callback_query: payload } }
51
- let(:payload) { {id: 11, from: from, message: message, data: data} }
52
- let(:message) { {message_id: 22, chat: chat, text: 'message text'} }
53
-
54
- # Matcher to check that origin message got edited.
55
- def edit_current_message(type, options = {})
56
- description = 'edit current message'
57
- options = options.merge(
58
- message_id: message[:message_id],
59
- chat_id: chat_id,
60
- )
61
- Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new(
62
- bot, :"editMessage#{type.to_s.camelize}", description: description
63
- ).with(hash_including(options))
64
- end
65
-
66
- # Matcher to check that callback query is answered.
67
- def answer_callback_query(text = Regexp.new(''), options = {})
68
- description = "answer callback query with #{text.inspect}"
69
- text = a_string_matching(text) if text.is_a?(Regexp)
70
- options = options.merge(
71
- callback_query_id: payload[:id],
72
- text: text,
73
- )
74
- Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new(
75
- bot, :answerCallbackQuery, description: description
76
- ).with(hash_including(options))
77
- end
78
- end
79
-
80
- RSpec.configure do |config|
81
- if config.respond_to?(:include_context)
82
- config.include_context 'telegram/bot/integration', :telegram_bot
83
- config.include_context 'telegram/bot/callback_query', :telegram_bot, :callback_query
84
- end
1
+ require 'telegram/bot'
2
+ Telegram::Bot.deprecation_0_15.warn(
3
+ "`require 'telegram/bot/rspec/integration'` is deprecated in favor of " \
4
+ "`require 'telegram/bot/rspec/integration/rails'`"
5
+ )
6
+ require 'telegram/bot/rspec/integration/rails'
7
+
8
+ Telegram::Bot::RSpec.with_include_context do |config|
9
+ config.include_context 'telegram/bot/integration/rails', telegram_bot: true
85
10
  end
@@ -0,0 +1,14 @@
1
+ require 'telegram/bot/rspec/integration/shared'
2
+
3
+ RSpec.shared_context 'telegram/bot/integration/poller' do
4
+ include_context 'telegram/bot/integration/shared'
5
+ let(:controller_class) { described_class }
6
+
7
+ def dispatch(update)
8
+ controller_class.dispatch(bot, update.as_json)
9
+ end
10
+ end
11
+
12
+ Telegram::Bot::RSpec.with_include_context do |config|
13
+ config.include_context 'telegram/bot/integration/poller', telegram_bot: :poller
14
+ end
@@ -0,0 +1,24 @@
1
+ require 'telegram/bot/rspec/integration/shared'
2
+ require 'rack/test'
3
+
4
+ RSpec.shared_context 'telegram/bot/integration/rack' do
5
+ include_context 'telegram/bot/integration/shared'
6
+ include Rack::Test::Methods
7
+
8
+ let(:request_path) { raise '`let(:request_path) { path to bot }` is required' }
9
+ let(:app) { raise '`let(:app) { your rack app here }` is required' }
10
+ let(:request_headers) do
11
+ {
12
+ 'ACCEPT' => 'application/json',
13
+ 'CONTENT_TYPE' => 'application/json',
14
+ }
15
+ end
16
+
17
+ def dispatch(update)
18
+ post request_path, update.to_json, request_headers
19
+ end
20
+ end
21
+
22
+ Telegram::Bot::RSpec.with_include_context do |config|
23
+ config.include_context 'telegram/bot/integration/rack', telegram_bot: :rack
24
+ end
@@ -0,0 +1,28 @@
1
+ require 'telegram/bot/rspec/integration/shared'
2
+
3
+ RSpec.shared_context 'telegram/bot/integration/rails', type: :request do
4
+ include_context 'telegram/bot/integration/shared'
5
+
6
+ let(:controller_path) do
7
+ route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot)
8
+ Rails.application.routes.url_helpers.public_send("#{route_name}_path")
9
+ end
10
+ let(:request_headers) do
11
+ {
12
+ 'Accept' => 'application/json',
13
+ 'Content-Type' => 'application/json',
14
+ }
15
+ end
16
+
17
+ def dispatch(update)
18
+ if ActionPack::VERSION::MAJOR >= 5
19
+ post(controller_path, params: update.to_json, headers: request_headers)
20
+ else
21
+ post(controller_path, update.to_json, request_headers)
22
+ end
23
+ end
24
+ end
25
+
26
+ Telegram::Bot::RSpec.with_include_context do |config|
27
+ config.include_context 'telegram/bot/integration/rails', telegram_bot: :rails
28
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_support/json'
2
+ require 'telegram/bot'
3
+ require 'telegram/bot/rspec/message_helpers'
4
+ require 'telegram/bot/rspec/callback_query_helpers'
5
+
6
+ RSpec.shared_context 'telegram/bot/integration/shared' do
7
+ include Telegram::Bot::RSpec::ClientMatchers
8
+ include_context 'telegram/bot/message_helpers'
9
+ include_context 'telegram/bot/callback_query', :callback_query
10
+
11
+ let(:bot) { Telegram.bot }
12
+ let(:clear_session?) { described_class.respond_to?(:session_store) }
13
+ before { described_class.session_store.try!(:clear) if clear_session? }
14
+ end
@@ -0,0 +1,26 @@
1
+ # Shared helpers for testing message updates.
2
+ RSpec.shared_context 'telegram/bot/message_helpers' do
3
+ let(:default_message_options) { {from: from, chat: chat} }
4
+ let(:from) { {id: from_id} }
5
+ let(:from_id) { 123 }
6
+ let(:chat) { {id: chat_id} }
7
+ let(:chat_id) { 456 }
8
+
9
+ # Shortcut for dispatching messages with default params.
10
+ def dispatch_message(text, options = {})
11
+ dispatch message: default_message_options.merge(options).merge(text: text)
12
+ end
13
+
14
+ # Dispatch command message.
15
+ def dispatch_command(cmd, *args)
16
+ options = args.last.is_a?(Hash) ? args.pop : {}
17
+ args.unshift("/#{cmd}")
18
+ dispatch_message(args.join(' '), options)
19
+ end
20
+
21
+ # Matcher to check response. Make sure to define `let(:chat_id)`.
22
+ def respond_with_message(expected = Regexp.new(''))
23
+ raise 'Define chat_id to use respond_with_message' unless defined?(chat_id)
24
+ send_telegram_message(bot, expected, chat_id: chat_id)
25
+ end
26
+ end
@@ -1,4 +1,5 @@
1
1
  require 'abstract_controller'
2
+ require 'active_support/core_ext/string/inflections'
2
3
  require 'active_support/callbacks'
3
4
  require 'active_support/version'
4
5
 
@@ -54,12 +55,14 @@ module Telegram
54
55
  abstract!
55
56
 
56
57
  %w[
57
- instrumentation
58
- log_subscriber
59
- reply_helpers
60
- rescue
61
- session
62
- ].each { |file| require "telegram/bot/updates_controller/#{file}" }
58
+ Commands
59
+ Instrumentation
60
+ LogSubscriber
61
+ ReplyHelpers
62
+ Rescue
63
+ Session
64
+ Translation
65
+ ].each { |name| require "telegram/bot/updates_controller/#{name.underscore}" }
63
66
 
64
67
  %w[
65
68
  CallbackQueryContext
@@ -78,9 +81,12 @@ module Telegram
78
81
  skip_after_callbacks_if_terminated: true
79
82
  end
80
83
 
81
- include AbstractController::Translation
84
+ include Commands
82
85
  include Rescue
83
86
  include ReplyHelpers
87
+ include Translation
88
+ # Add instrumentations hooks at the bottom, to ensure they instrument
89
+ # all the methods properly.
84
90
  include Instrumentation
85
91
 
86
92
  extend Session::ConfigMethods
@@ -96,8 +102,6 @@ module Telegram
96
102
  shipping_query
97
103
  pre_checkout_query
98
104
  ].freeze
99
- CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
100
- CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
101
105
 
102
106
  class << self
103
107
  # Initialize controller and process update.
@@ -105,27 +109,6 @@ module Telegram
105
109
  new(*args).dispatch
106
110
  end
107
111
 
108
- # Overrid it to filter or transform commands.
109
- # Default implementation is to convert to downcase and add `on_` prefix
110
- # for conflicting commands.
111
- def action_for_command(cmd)
112
- cmd.downcase!
113
- cmd.match(CONFLICT_CMD_REGEX) ? "on_#{cmd}" : cmd
114
- end
115
-
116
- # Fetches command from text message. All subsequent words are returned
117
- # as arguments.
118
- # If command has mention (eg. `/test@SomeBot`), it returns commands only
119
- # for specified username. Set `username` to `true` to accept
120
- # any commands.
121
- def command_from_text(text, username = nil)
122
- return unless text
123
- match = text.match(CMD_REGEX)
124
- return unless match
125
- mention = match[3]
126
- [match[1], text.split.drop(1)] if username == true || !mention || mention == username
127
- end
128
-
129
112
  def payload_from_update(update)
130
113
  update && PAYLOAD_TYPES.find do |type|
131
114
  item = update[type]
@@ -134,8 +117,7 @@ module Telegram
134
117
  end
135
118
  end
136
119
 
137
- attr_internal_reader :update, :bot, :payload, :payload_type, :is_command
138
- alias_method :command?, :is_command
120
+ attr_internal_reader :update, :bot, :payload, :payload_type
139
121
  delegate :username, to: :bot, prefix: true, allow_nil: true
140
122
 
141
123
  # Second argument can be either update object with hash access & string
@@ -175,54 +157,64 @@ module Telegram
175
157
 
176
158
  # Processes current update.
177
159
  def dispatch
178
- @_is_command, action, args = action_for_payload
160
+ action, args = action_for_payload
179
161
  process(action, *args)
180
162
  end
181
163
 
164
+ attr_internal_reader :action_options
165
+
166
+ # It provides support for passing array as action, where first vaule
167
+ # is action name and second is action metadata.
168
+ # This metadata is stored inside action_options
169
+ def process(action, *args)
170
+ action, options = action if action.is_a?(Array)
171
+ @_action_options = options || {}
172
+ super
173
+ end
174
+
175
+ # There are multiple ways how action name is calculated for update
176
+ # (see Commands, MessageContext, etc.). This method represents the
177
+ # way how action was calculated for current udpate.
178
+ #
179
+ # Some of possible values are `:payload, :command, :message_context`.
180
+ def action_type
181
+ action_options[:type] || :payload
182
+ end
183
+
182
184
  # Calculates action name and args for payload.
183
185
  # Uses `action_for_#{payload_type}` methods.
184
186
  # If this method doesn't return anything
185
187
  # it uses fallback with action same as payload type.
186
- # Returns array `[is_command?, action, args]`.
188
+ # Returns array `[action, args]`.
187
189
  def action_for_payload
188
190
  if payload_type
189
191
  send("action_for_#{payload_type}") || action_for_default_payload
190
192
  else
191
- [false, :unsupported_payload_type, []]
193
+ [:unsupported_payload_type, []]
192
194
  end
193
195
  end
194
196
 
195
197
  def action_for_default_payload
196
- [false, payload_type, [payload]]
197
- end
198
-
199
- # If payload is a message with command, then returned action is an
200
- # action for this command.
201
- # Separate method, so it can be easily overriden (ex. MessageContext).
202
- #
203
- # This is not used for edited messages/posts. It process them as basic updates.
204
- def action_for_message
205
- cmd, args = self.class.command_from_text(payload['text'], bot_username)
206
- cmd &&= self.class.action_for_command(cmd)
207
- [true, cmd, args] if cmd
198
+ [payload_type, [payload]]
208
199
  end
209
- alias_method :action_for_channel_post, :action_for_message
210
200
 
211
201
  def action_for_inline_query
212
- [false, payload_type, [payload['query'], payload['offset']]]
202
+ [payload_type, [payload['query'], payload['offset']]]
213
203
  end
214
204
 
215
205
  def action_for_chosen_inline_result
216
- [false, payload_type, [payload['result_id'], payload['query']]]
206
+ [payload_type, [payload['result_id'], payload['query']]]
217
207
  end
218
208
 
219
209
  def action_for_callback_query
220
- [false, payload_type, [payload['data']]]
210
+ [payload_type, [payload['data']]]
221
211
  end
222
212
 
223
- # Silently ignore unsupported messages.
224
- # Params are `action, *args`.
225
- def action_missing(*)
213
+ # Silently ignore unsupported messages to not fail when user crafts
214
+ # an update with usupported command, callback query context, etc.
215
+ def action_missing(action, *_args)
216
+ logger.debug { "The action '#{action}' is not defined in #{self.class.name}" } if logger
217
+ nil
226
218
  end
227
219
 
228
220
  PAYLOAD_TYPES.each do |type|
@@ -17,8 +17,12 @@ module Telegram
17
17
  context, new_data = context_from_callback_query
18
18
  if context
19
19
  action_name = "#{context}_callback_query"
20
- [false, action_name, [new_data]] if action_method?(action_name)
21
- end || super
20
+ if action_method?(action_name)
21
+ action_options = {type: :callback_query_context, context: context}
22
+ return [[action_name, action_options], [new_data]]
23
+ end
24
+ end
25
+ super
22
26
  end
23
27
 
24
28
  def context_from_callback_query
@@ -0,0 +1,44 @@
1
+ module Telegram
2
+ module Bot
3
+ class UpdatesController
4
+ # Support for parsing commands
5
+ module Commands
6
+ CMD_REGEX = %r{\A/([a-z\d_]{,31})(@(\S+))?(\s|$)}i
7
+
8
+ class << self
9
+ # Fetches command from text message. All subsequent words are returned
10
+ # as arguments.
11
+ # If command has mention (eg. `/test@SomeBot`), it returns commands only
12
+ # for specified username. Set `username` to `true` to accept
13
+ # any commands.
14
+ def command_from_text(text, username = nil)
15
+ return unless text
16
+ match = text.match(CMD_REGEX)
17
+ return unless match
18
+ mention = match[3]
19
+ [match[1], text.split.drop(1)] if username == true || !mention || mention == username
20
+ end
21
+ end
22
+
23
+ # Override it to filter or transform commands.
24
+ # Default implementation is to downcase and add `!` suffix.
25
+ def action_for_command(cmd)
26
+ "#{cmd.downcase}!"
27
+ end
28
+
29
+ # If payload is a message with command, then returned action is an
30
+ # action for this command.
31
+ # Separate method, so it can be easily overriden (ex. MessageContext).
32
+ #
33
+ # This is not used for edited messages/posts. It process them as basic updates.
34
+ def action_for_message
35
+ cmd, args = Commands.command_from_text(payload['text'], bot_username)
36
+ return unless cmd
37
+ [[action_for_command(cmd), type: :command, command: cmd], args]
38
+ end
39
+
40
+ alias_method :action_for_channel_post, :action_for_message
41
+ end
42
+ end
43
+ end
44
+ end
@@ -2,62 +2,42 @@ module Telegram
2
2
  module Bot
3
3
  class UpdatesController
4
4
  # Allows to store context in session and treat next message according to this context.
5
+ #
6
+ # It provides `save_context` method to store method name
7
+ # to be used as action for next update:
8
+ #
9
+ # def set_location!(*)
10
+ # save_context(:set_location_from_message)
11
+ # respond_with :message, text: 'Where are you?'
12
+ # end
13
+ #
14
+ # def set_location_from_messge(city = nil, *)
15
+ # # update
16
+ # end
17
+ #
18
+ # # OR
19
+ # # This will support both `/set_location city_name`, and `/set_location`
20
+ # # with subsequent refinement.
21
+ # def set_location!(city = nil, *)
22
+ # if city
23
+ # # update
24
+ # else
25
+ # save_context(:set_location!)
26
+ # respond_with :message, text: 'Where are you?'
27
+ # end
28
+ # end
5
29
  module MessageContext
6
30
  extend ActiveSupport::Concern
7
31
 
8
32
  include Session
9
33
 
10
- module ClassMethods
11
- def context_handlers
12
- @_context_handlers ||= {}
13
- end
14
-
15
- # Registers handler for context.
16
- #
17
- # context_handler :rename do |*|
18
- # resource.update!(name: payload['text'])
19
- # end
20
- #
21
- # # To run other action with all the callbacks:
22
- # context_handler :rename do |*words|
23
- # process(:rename, *words)
24
- # end
25
- #
26
- # # Or just
27
- # context_handler :rename, :your_action_to_call
28
- # context_handler :rename # to call :rename
29
- #
30
- def context_handler(context = nil, action = nil, &block)
31
- context &&= context.to_sym
32
- if block
33
- action = "_context_handler_#{context}"
34
- define_method(action, &block)
35
- end
36
- context_handlers[context] = action || context
37
- end
38
-
39
- attr_reader :context_to_action
40
-
41
- # Use it to use context value as action name for all contexts
42
- # which miss handlers.
43
- # For security reasons it supports only action methods and will
44
- # raise AbstractController::ActionNotFound if context is invalid.
45
- def context_to_action!
46
- @context_to_action = true
47
- end
48
- end
49
-
50
34
  # Action to clear context.
51
- def cancel
35
+ def cancel!
52
36
  # Context is already cleared in action_for_message
53
37
  end
54
38
 
55
39
  private
56
40
 
57
- # Context is read from the session to treat messages
58
- # according to previous request.
59
- attr_reader :context
60
-
61
41
  # Controller may have multiple sessions, let it be possible
62
42
  # to select session for message context.
63
43
  def message_context_session
@@ -68,10 +48,11 @@ module Telegram
68
48
  # it has higher priority than contextual action.
69
49
  def action_for_message
70
50
  val = message_context_session.delete(:context)
71
- @context = val && val.to_sym
51
+ context = val && val.to_s
72
52
  super || context && begin
73
- handler = handler_for_context
74
- [true, handler, payload['text'].try!(:split) || []] if handler
53
+ args = payload['text'].try!(:split) || []
54
+ action = action_for_message_context(context)
55
+ [[action, type: :message_context, context: context], args]
75
56
  end
76
57
  end
77
58
 
@@ -80,18 +61,16 @@ module Telegram
80
61
  message_context_session[:context] = context
81
62
  end
82
63
 
83
- def handler_for_context
84
- self.class.context_handlers[context] || self.class.context_to_action && begin
85
- action_name = context.to_s
86
- unless action_method?(action_name)
87
- raise AbstractController::ActionNotFound,
88
- "The action '#{action_name}' could not be set from context " \
89
- "for #{self.class.name}. " \
90
- 'context_to_action! supports only action methods for security reasons. ' \
91
- 'If you need to call this action use context_handler for it.'
92
- end
93
- action_name
94
- end
64
+ # Returns action name for message context. By default it's the same as context name.
65
+ # Raises AbstractController::ActionNotFound if action is not available.
66
+ # This differs from other cases where invalid actions are silently ignored,
67
+ # because message context is controlled by developer, and users are not able
68
+ # to construct update to run any specific context.
69
+ def action_for_message_context(context)
70
+ action = context.to_s
71
+ return action if action_method?(action)
72
+ raise AbstractController::ActionNotFound,
73
+ "The context action '#{action}' is not found in #{self.class.name}"
95
74
  end
96
75
  end
97
76
  end
@@ -1,37 +1,32 @@
1
1
  require 'telegram/bot/updates_controller/testing'
2
+ require 'telegram/bot/rspec/message_helpers'
3
+ require 'telegram/bot/rspec/callback_query_helpers'
2
4
 
3
5
  RSpec.shared_context 'telegram/bot/updates_controller' do
6
+ include Telegram::Bot::RSpec::ClientMatchers
7
+ include_context 'telegram/bot/message_helpers'
8
+ include_context 'telegram/bot/callback_query', :callback_query
9
+
4
10
  let(:controller_class) { described_class }
5
11
  let(:controller) do
6
- controller_class.new(bot, update).tap do |x|
12
+ controller_class.new(*controller_args).tap do |x|
7
13
  x.extend Telegram::Bot::UpdatesController::Testing
8
14
  end
9
15
  end
10
- let(:update) { build_update(payload_type, payload) }
16
+ let(:controller_args) { [bot, deep_stringify(update)] }
17
+ let(:update) { {payload_type => payload} }
11
18
  let(:payload_type) { :some_type }
12
19
  let(:payload) { double(:payload) }
13
20
  let(:bot) { Telegram::Bot::ClientStub.new(bot_name) }
14
21
  let(:bot_name) { 'bot' }
15
22
  let(:session) { controller.send(:session) }
16
- let(:from_id) { 123 }
17
- let(:chat_id) { 456 }
18
- let(:default_message_options) { {from: {id: from_id}, chat: {id: chat_id}} }
19
-
20
- include Telegram::Bot::RSpec::ClientMatchers
21
-
22
- def dispatch(bot = self.bot, update = self.update)
23
- controller.dispatch_again(bot, update)
24
- end
25
23
 
26
- def dispatch_message(text, options = {})
27
- update = build_update :message, default_message_options.merge(options).merge(text: text)
28
- dispatch bot, update
29
- end
30
-
31
- def build_update(type, content)
32
- deep_stringify type => content
24
+ # Process update.
25
+ def dispatch(update = self.update, bot = self.bot)
26
+ controller.dispatch_again(bot, deep_stringify(update))
33
27
  end
34
28
 
29
+ # Same as `.as_json` but mocks-friendly.
35
30
  def deep_stringify(input)
36
31
  case input
37
32
  when Array then input.map(&method(__callee__))
@@ -39,15 +34,8 @@ RSpec.shared_context 'telegram/bot/updates_controller' do
39
34
  else input
40
35
  end
41
36
  end
42
-
43
- # Matcher to check response. Make sure to define `let(:chat_id)`.
44
- def respond_with_message(expected)
45
- send_telegram_message(bot, expected, chat_id: chat_id)
46
- end
47
37
  end
48
38
 
49
- RSpec.configure do |config|
50
- if config.respond_to?(:include_context)
51
- config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller
52
- end
39
+ Telegram::Bot::RSpec.with_include_context do |config|
40
+ config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller
53
41
  end
@@ -0,0 +1,47 @@
1
+ module Telegram
2
+ module Bot
3
+ class UpdatesController
4
+ # Provides helpers similar to AbstractController::Translation
5
+ # but by default uses `action_name_i18n_key` in lazy translation keys
6
+ # which strips `!` from action names by default. This makes translating
7
+ # strings for commands more convenient.
8
+ #
9
+ # To disable this behaviour use `alias_method :action_name_i18n_key, :action_name`.
10
+ module Translation
11
+ extend ActiveSupport::Concern
12
+
13
+ module ClassMethods
14
+ # Class-level helper for lazy translations.
15
+ def translate(key, options = {})
16
+ key = "#{controller_path.tr('/', '.')}#{key}" if key.to_s.start_with?('.')
17
+ I18n.translate(key, options)
18
+ end
19
+ alias :t :translate
20
+ end
21
+
22
+ # See toplevel description.
23
+ def translate(key, options = {})
24
+ if key.to_s.start_with?('.')
25
+ path = controller_path.tr('/', '.')
26
+ defaults = [:"#{path}#{key}"]
27
+ defaults << options[:default] if options[:default]
28
+ options[:default] = defaults.flatten
29
+ key = "#{path}.#{action_name_i18n_key}#{key}"
30
+ end
31
+ I18n.translate(key, options)
32
+ end
33
+ alias :t :translate
34
+
35
+ # Strips trailing `!` from action_name.
36
+ def action_name_i18n_key
37
+ action_name.chomp('!')
38
+ end
39
+
40
+ def localize(*args)
41
+ I18n.localize(*args)
42
+ end
43
+ alias :l :localize
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,6 +1,6 @@
1
1
  module Telegram
2
2
  module Bot
3
- VERSION = '0.13.1'.freeze
3
+ VERSION = '0.14.0'.freeze
4
4
 
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION
@@ -17,6 +17,9 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ['lib']
19
19
 
20
+ spec.post_install_message = 'Breaking changes in v0.14! ' \
21
+ 'See upgrade guide at https://github.com/telegram-bot-rb/telegram-bot/wiki/Upgrading-to-0.14'
22
+
20
23
  spec.required_ruby_version = '~> 2.0'
21
24
 
22
25
  spec.add_dependency 'actionpack', '>= 4.0', '< 6.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telegram-bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Melentiev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-28 00:00:00.000000000 Z
11
+ date: 2018-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -138,10 +138,17 @@ files:
138
138
  - lib/telegram/bot/railtie.rb
139
139
  - lib/telegram/bot/routes_helper.rb
140
140
  - lib/telegram/bot/rspec.rb
141
+ - lib/telegram/bot/rspec/callback_query_helpers.rb
141
142
  - lib/telegram/bot/rspec/client_matchers.rb
142
143
  - lib/telegram/bot/rspec/integration.rb
144
+ - lib/telegram/bot/rspec/integration/poller.rb
145
+ - lib/telegram/bot/rspec/integration/rack.rb
146
+ - lib/telegram/bot/rspec/integration/rails.rb
147
+ - lib/telegram/bot/rspec/integration/shared.rb
148
+ - lib/telegram/bot/rspec/message_helpers.rb
143
149
  - lib/telegram/bot/updates_controller.rb
144
150
  - lib/telegram/bot/updates_controller/callback_query_context.rb
151
+ - lib/telegram/bot/updates_controller/commands.rb
145
152
  - lib/telegram/bot/updates_controller/instrumentation.rb
146
153
  - lib/telegram/bot/updates_controller/log_subscriber.rb
147
154
  - lib/telegram/bot/updates_controller/message_context.rb
@@ -150,6 +157,7 @@ files:
150
157
  - lib/telegram/bot/updates_controller/rspec_helpers.rb
151
158
  - lib/telegram/bot/updates_controller/session.rb
152
159
  - lib/telegram/bot/updates_controller/testing.rb
160
+ - lib/telegram/bot/updates_controller/translation.rb
153
161
  - lib/telegram/bot/updates_controller/typed_update.rb
154
162
  - lib/telegram/bot/updates_poller.rb
155
163
  - lib/telegram/bot/version.rb
@@ -158,7 +166,7 @@ homepage: https://github.com/telegram-bot-rb/telegram-bot
158
166
  licenses:
159
167
  - MIT
160
168
  metadata: {}
161
- post_install_message:
169
+ post_install_message: Breaking changes in v0.14! See upgrade guide at https://github.com/telegram-bot-rb/telegram-bot/wiki/Upgrading-to-0.14
162
170
  rdoc_options: []
163
171
  require_paths:
164
172
  - lib