telegram-bot 0.13.1 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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