telegram-bot 0.4.2 → 0.5.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
  SHA1:
3
- metadata.gz: 968133c5cb3e68b5898700cb9ec18b6d56691658
4
- data.tar.gz: 1df3660c88d5faf9da1548a3496f518616cdd83c
3
+ metadata.gz: a045d870c213b5136f08e93b6f6e243b9276c6bb
4
+ data.tar.gz: f83f3bfd325fbb5014939ea0c4db049c2d8a228a
5
5
  SHA512:
6
- metadata.gz: 7c1d1409286e9a0d455d13b46920acd0de1f8f2de4d94313480aca5e9185759265b901802853e3ec520ddb5e056002ef6692321661b77bb24e6d28e5ac718543
7
- data.tar.gz: b1f815b8f1e7a0c7aa994f66b398615ac9dc6acbd9355b85c2467bca2a9ba2fb873db41db1ab430808e68a9988828ed33015ce7260417a03e169d94b17125d76
6
+ metadata.gz: ed51cc7ddcb72060182cc6f22ef8e0ab11c7ac07223a03c6c24b301db7a46bab88800b1a00044558d40d17a87fde09bdfa0b7c310039c97a31b7bb172d9f0e56
7
+ data.tar.gz: 5617505f1d36c7967777bb27c0b1fd1025f7f0617629c884a67ca8092702642760526295ea21683b7d924b2d9f2e5f8350b847600a3506f289b225511fc73d5f
data/.rubocop.yml CHANGED
@@ -8,9 +8,11 @@ Style/AlignParameters:
8
8
  # EnforcedStyle:
9
9
  # - with_first_parameter
10
10
  # - with_fixed_indentation
11
+ Style/AndOr: {EnforcedStyle: conditionals}
11
12
  Style/ClosingParenthesisIndentation: {Enabled: false}
12
13
  Style/Documentation: {Enabled: false}
13
14
  Style/DotPosition: {EnforcedStyle: trailing}
15
+ Style/FirstParameterIndentation: {EnforcedStyle: consistent}
14
16
  Style/IfUnlessModifier: {Enabled: false}
15
17
  Style/ModuleFunction: {Enabled: false}
16
18
  Style/MultilineOperationIndentation: {EnforcedStyle: indented}
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # 0.5.0
2
+
3
+ - MessageContext.
4
+ - Running controller action without update.
5
+ - Client.wrap supports symbols.
6
+ - Improved testing utils: ability to process multiple updates on same controller instance,
7
+ stubbing all clients in application.
data/README.md CHANGED
@@ -142,7 +142,7 @@ You can enable typecasting of `update` with `telegram-bot-types` by including
142
142
 
143
143
  ```ruby
144
144
  class Telegram::WebhookController < Telegram::Bot::UpdatesController
145
- include Telegram::Bot::UpdatesPoller::TypedUpdate
145
+ include Telegram::Bot::UpdatesController::TypedUpdate
146
146
 
147
147
  def message(message)
148
148
  message.class # => Telegram::Bot::Types::Message
@@ -158,6 +158,9 @@ config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.
158
158
 
159
159
  class Telegram::WebhookController < Telegram::Bot::UpdatesController
160
160
  include Telegram::Bot::UpdatesController::Session
161
+ # or just shortcut:
162
+ use_session!
163
+
161
164
  # You can override global config
162
165
  self.session_store = :file_store
163
166
 
@@ -166,7 +169,7 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
166
169
  end
167
170
 
168
171
  def read
169
- session[:text]
172
+ reply_with :message, text: session[:text]
170
173
  end
171
174
 
172
175
  private
@@ -180,6 +183,59 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController
180
183
  end
181
184
  ```
182
185
 
186
+ It's usual to support chain of messages like BotFather: after receiving command
187
+ it asks you for additional argument. There is `MessageContext` for this:
188
+
189
+ ```ruby
190
+ class Telegram::WebhookController < Telegram::Bot::UpdatesController
191
+ include Telegram::Bot::UpdatesController::MessageContext
192
+
193
+ def rename(*)
194
+ # set context for the next message
195
+ save_context :rename
196
+ reply_with :message, text: 'What name do you like?'
197
+ end
198
+
199
+ # register context handlers to handle this context
200
+ context_handler :rename do |message|
201
+ update_name message[:text]
202
+ reply_with :message, text: 'Renamed!'
203
+ end
204
+
205
+ # You can do it in other way:
206
+ def rename(name = nil, *)
207
+ if name
208
+ update_name name
209
+ reply_with :message, text: 'Renamed!'
210
+ else
211
+ save_context :rename
212
+ reply_with :message, text: 'What name do you like?'
213
+ end
214
+ end
215
+
216
+ # This will call #rename like if it is called with message '/rename %text%'
217
+ context_handler :rename
218
+
219
+ # If you have a lot of such methods you can use
220
+ context_to_action!
221
+ # It'll use context value as action name for all contexts which miss handlers.
222
+ end
223
+ ```
224
+
225
+ To process update run:
226
+
227
+ ```ruby
228
+ ControllerClass.dispatch(bot, update)
229
+ ```
230
+
231
+ There is also ability to run action without update:
232
+
233
+ ```ruby
234
+ # Most likely you'll want to pass :from and :chat
235
+ controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat)
236
+ controller.process(:help, *args)
237
+ ```
238
+
183
239
  ### Routes
184
240
 
185
241
  Use `telegram_webhooks` helper to add routes. It will create routes for bots
@@ -215,13 +271,36 @@ call `.dispatch(bot, update)` on controller.
215
271
 
216
272
  ### Development & Debugging
217
273
 
218
- Use `rake telegram:bot:poller BOT=chat` to run poller. It'll automatically load
219
- changes without restart in development env. This task will not if you don't use
220
- `telegram_webhooks`.
274
+ Use `rake telegram:bot:poller` to run poller. It'll automatically load
275
+ changes without restart in development env. Optionally specify bot to run poller for
276
+ with `BOT` envvar (`BOT=chat`).
221
277
 
278
+ This task will not work if you don't use `telegram_webhooks`.
222
279
  You can run poller manually with
223
280
  `Telegram::Bot::UpdatesPoller.start(bot, controller_class)`.
224
281
 
282
+ ### Testing
283
+
284
+ There is `Telegram::Bot::ClientStub` class to stub client for tests.
285
+ Instead of performing API requests it stores them in `requests` hash.
286
+
287
+ To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before
288
+ initializing clients. Most likely you'll want something like this:
289
+
290
+ ```ruby
291
+ RSpec.configure do |config|
292
+ # ...
293
+ Telegram.reset_bots
294
+ Telegram::Bot::ClientStub.stub_all!
295
+ config.after { Telegram.bot.reset }
296
+ # ...
297
+ end
298
+ ```
299
+
300
+ There are also some helpers for controller tests.
301
+ Check out `telegram/bot/updates_controller/rspec_helpers` and
302
+ `telegram/bot/updates_controller/testing`.
303
+
225
304
  ### Deploying
226
305
 
227
306
  Use `rake telegram:bot:set_webhook` to update webhook url for all configured bots.
@@ -19,6 +19,9 @@ module Telegram
19
19
  when Hash then
20
20
  input = input.stringify_keys
21
21
  new input['token'], input['username']
22
+ when Symbol
23
+ Telegram.bots[input] or
24
+ raise "Bot #{input} not configured, check Telegram.bots_config."
22
25
  else
23
26
  new(input)
24
27
  end
@@ -104,7 +107,7 @@ module Telegram
104
107
  end
105
108
 
106
109
  def inspect
107
- "#<Telegram::Bot::Client##{object_id}(#{@username})>"
110
+ "#<#{self.class.name}##{object_id}(#{@username})>"
108
111
  end
109
112
  end
110
113
  end
@@ -4,6 +4,35 @@ module Telegram
4
4
  class ClientStub < Client
5
5
  attr_reader :requests
6
6
 
7
+ module StubbedConstructor
8
+ def new(*args)
9
+ if self == ClientStub || !ClientStub.stub_all?
10
+ super
11
+ else
12
+ ClientStub.new(args[1])
13
+ end
14
+ end
15
+ end
16
+
17
+ class << self
18
+ # Makes all
19
+ def stub_all!(enabled = true)
20
+ Client.extend(StubbedConstructor) unless Client < StubbedConstructor
21
+ return @_stub_all = enabled unless block_given?
22
+ begin
23
+ old = @_stub_all
24
+ stub_all!(enabled)
25
+ yield
26
+ ensure
27
+ stub_all!(old)
28
+ end
29
+ end
30
+
31
+ def stub_all?
32
+ @_stub_all
33
+ end
34
+ end
35
+
7
36
  def initialize(username = nil)
8
37
  @username = username
9
38
  reset
@@ -41,6 +41,13 @@ module Telegram
41
41
  config[:default] = default if default
42
42
  end
43
43
  end
44
+
45
+ # Resets all cached bots and their configs.
46
+ def reset_bots
47
+ @bots = nil
48
+ @bot = nil
49
+ @bots_config = nil
50
+ end
44
51
  end
45
52
  end
46
53
  end
@@ -42,7 +42,9 @@ module Telegram
42
42
  # # You can override this options or specify others:
43
43
  # telegram_webhooks TelegramController, as: :my_webhook
44
44
  # telegram_webhooks bot => [TelegramChatController, as: :chat_webhook],
45
- # other_bot => [TelegramAuctionController,
45
+ # other_bot => TelegramAuctionController,
46
+ # admin_chat: TelegramAdminChatController
47
+ #
46
48
  def telegram_webhooks(controllers, bots = nil, **options)
47
49
  unless controllers.is_a?(Hash)
48
50
  bots = bots ? Array.wrap(bots) : Telegram.bots.values
@@ -4,12 +4,59 @@ require 'active_support/version'
4
4
 
5
5
  module Telegram
6
6
  module Bot
7
+ # Base class to create update processors. With callbacks, session and helpers.
8
+ #
9
+ # Define public methods for each command and they will be called when
10
+ # update has this command. Message is automatically parsed and
11
+ # words are passed as method arguments. Be sure to use default values and
12
+ # splat arguments in every action method to not get errors, when user
13
+ # sends command without necessary args / with extra args.
14
+ #
15
+ # def start(token = nil, *)
16
+ # if token
17
+ # # ...
18
+ # else
19
+ # # ...
20
+ # end
21
+ # end
22
+ #
23
+ # def help(*)
24
+ # reply_with :message, text:
25
+ # end
26
+ #
27
+ # To process plain text messages (without commands) or other updates just
28
+ # define public method with name of payload type. They will receive payload
29
+ # as an argument.
30
+ #
31
+ # def message(message)
32
+ # reply_with :message, text: "Echo: #{message['text']}"
33
+ # end
34
+ #
35
+ # def inline_query(query)
36
+ # answer_inline_query results_for_query(query), is_personal: true
37
+ # end
38
+ #
39
+ # # To process conflicting commands (`/message args`) just use `on_` prefix:
40
+ # def on_message(*args)
41
+ # # ...
42
+ # end
43
+ #
44
+ # To process update run:
45
+ #
46
+ # ControllerClass.dispatch(bot, update)
47
+ #
48
+ # There is also ability to run action without update:
49
+ #
50
+ # ControllerClass.new(bot, from: telegram_user, chat: telegram_chat).
51
+ # process(:help, *args)
52
+ #
7
53
  class UpdatesController < AbstractController::Base
8
54
  abstract!
9
55
 
10
56
  require 'telegram/bot/updates_controller/session'
11
57
  require 'telegram/bot/updates_controller/log_subscriber'
12
58
  require 'telegram/bot/updates_controller/instrumentation'
59
+ autoload :MessageContext, 'telegram/bot/updates_controller/message_context'
13
60
 
14
61
  include AbstractController::Callbacks
15
62
  # Redefine callbacks with default terminator.
@@ -37,6 +84,7 @@ module Telegram
37
84
  CONFLICT_CMD_REGEX = Regexp.new("^(#{PAYLOAD_TYPES.join('|')}|\\d)")
38
85
 
39
86
  class << self
87
+ # Initialize controller and process update.
40
88
  def dispatch(*args)
41
89
  new(*args).dispatch
42
90
  end
@@ -59,7 +107,7 @@ module Telegram
59
107
  match = text.match CMD_REGEX
60
108
  return unless match
61
109
  return if match[3] && username != true && match[3] != username
62
- [match[1], text.split(' ').drop(1)]
110
+ [match[1], text.split.drop(1)]
63
111
  end
64
112
  end
65
113
 
@@ -67,18 +115,39 @@ module Telegram
67
115
  alias_method :command?, :is_command
68
116
  delegate :username, to: :bot, prefix: true, allow_nil: true
69
117
 
118
+ # Second argument can be either update object with hash access & string
119
+ # keys or Hash with `:from` or `:chat` to override this values and assume
120
+ # that update is nil.
70
121
  def initialize(bot = nil, update = nil)
122
+ if update.is_a?(Hash) && (update.key?(:from) || update.key?(:chat))
123
+ options = update
124
+ update = nil
125
+ end
71
126
  @_update = update
72
127
  @_bot = bot
128
+ @_chat, @_from = options && options.values_at(:chat, :from)
73
129
 
130
+ payload_data = nil
74
131
  update && PAYLOAD_TYPES.find do |type|
75
132
  item = update[type]
76
- next unless item
77
- @_payload = item
78
- @_payload_type = type
133
+ payload_data = [item, type] if item
79
134
  end
135
+ @_payload, @_payload_type = payload_data
136
+ end
137
+
138
+ # Accessor to `'chat'` field of payload. Can be overriden with `chat` option
139
+ # for #initialize.
140
+ def chat
141
+ @_chat || payload && payload['chat']
80
142
  end
81
143
 
144
+ # Accessor to `'from'` field of payload. Can be overriden with `from` option
145
+ # for #initialize.
146
+ def from
147
+ @_from || payload && payload['from']
148
+ end
149
+
150
+ # Processes current update.
82
151
  def dispatch
83
152
  @_is_command, action, args = action_for_payload
84
153
  process(action, *args)
@@ -102,19 +171,32 @@ module Telegram
102
171
  def action_missing(*)
103
172
  end
104
173
 
105
- %w(chat from).each do |field|
106
- define_method(field) { payload[field] }
107
- end
108
-
174
+ # Helper to call bot's `send_#{type}` method with already set `chat_id` and
175
+ # `reply_to_message_id`:
176
+ #
177
+ # reply_with :message, text: 'Hello!'
178
+ # reply_with :message, text: '__Hello!__', parse_mode: :Markdown
179
+ # reply_with :photo, photo: File.open(photo_to_send), caption: "It's incredible!"
109
180
  def reply_with(type, params)
110
181
  method = "send_#{type}"
182
+ chat = self.chat
183
+ payload = self.payload
111
184
  params = params.merge(
112
- chat_id: chat['id'],
113
- reply_to_message: payload['message_id'],
185
+ chat_id: (chat && chat['id'] or raise 'Can not reply_with when chat is not present'),
186
+ reply_to_message: payload && payload['message_id'],
114
187
  )
115
188
  bot.public_send(method, params)
116
189
  end
117
190
 
191
+ # Same as reply_with, but for inline queries.
192
+ def answer_inline_query(results, params = {})
193
+ params = params.merge(
194
+ inline_query_id: payload['id'],
195
+ results: results,
196
+ )
197
+ bot.answer_inline_query(params)
198
+ end
199
+
118
200
  ActiveSupport.run_load_hooks('telegram.bot.updates_controller', self)
119
201
  end
120
202
  end
@@ -0,0 +1,102 @@
1
+ module Telegram
2
+ module Bot
3
+ class UpdatesController
4
+ # Allows to store context in session and treat next message according to this context.
5
+ module MessageContext
6
+ extend ActiveSupport::Concern
7
+
8
+ include Session
9
+
10
+ included do
11
+ # As we use before_action context is cleared anyway,
12
+ # no matter we used it or not.
13
+ before_action :fetch_context
14
+ singleton_class.send :attr_reader, :context_handlers, :context_to_action
15
+ @context_handlers = {}
16
+ end
17
+
18
+ module ClassMethods
19
+ # Registers handler for context.
20
+ #
21
+ # context_handler :rename do |message|
22
+ # resource.update!(name: message['text'])
23
+ # end
24
+ #
25
+ # # To run other action with all the callbacks:
26
+ # context_handler :rename do |message|
27
+ # process(:rename, *m['text'].split)
28
+ # end
29
+ #
30
+ # # Or just
31
+ # context_handler :rename, :your_action_to_call
32
+ # context_handler :rename # to call :rename
33
+ #
34
+ # # For messages without context use this instead of `message` method:
35
+ # context_handle do |message|
36
+ # end
37
+ #
38
+ def context_handler(context = nil, action = nil, &block)
39
+ context &&= context.to_sym
40
+ context_handlers[context] = block || action || context
41
+ end
42
+
43
+ # Use it to use context value as action name for all contexts
44
+ # which miss handlers.
45
+ # For security reasons it supports only action methods and will
46
+ # raise AbstractController::ActionNotFound if context is invalid.
47
+ def context_to_action!
48
+ @context_to_action = true
49
+ end
50
+ end
51
+
52
+ # Finds handler for current context and processes message with it.
53
+ def message(message)
54
+ handler = handler_for_context
55
+ return unless handler
56
+ if handler.respond_to?(:call)
57
+ instance_exec(message, &handler)
58
+ else
59
+ process(handler, *message['text'].split)
60
+ end
61
+ end
62
+
63
+ # Action to clear context.
64
+ def cancel
65
+ # Context is already cleared in before_action
66
+ end
67
+
68
+ private
69
+
70
+ # Context is read from the session to treat messages
71
+ # according to previous request.
72
+ attr_reader :context
73
+
74
+ # Fetches and removes context from session.
75
+ def fetch_context
76
+ val = session.delete(:context)
77
+ @context = val && val.to_sym
78
+ true # TODO: remove in Rails 5.0
79
+ end
80
+
81
+ # Save context for the next request.
82
+ def save_context(context)
83
+ session[:context] = context
84
+ end
85
+
86
+ def handler_for_context
87
+ self.class.context_handlers[context] || self.class.context_to_action && begin
88
+ action_name = context.to_s
89
+ unless action_method?(action_name)
90
+ raise AbstractController::ActionNotFound,
91
+ "The action '#{action_name}' could not be set from context " \
92
+ "for #{self.class.name}. " \
93
+ 'context_to_action! supports only action methods for security reasons. ' \
94
+ 'If you need to call this action use context_handler for it.'
95
+ end
96
+ action_name
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,23 +1,29 @@
1
+ require 'telegram/bot/updates_controller/testing'
2
+
1
3
  RSpec.shared_context 'telegram/bot/updates_controller' do
2
4
  let(:controller_class) { described_class }
3
- let(:instance) { controller_class.new(bot, update) }
4
- let(:update) { {payload_type => payload} }
5
- let(:payload_type) { 'some_type' }
5
+ let(:controller) do
6
+ controller_class.new(bot, update).tap do |x|
7
+ x.extend Telegram::Bot::UpdatesController::Testing
8
+ end
9
+ end
10
+ let(:update) { build_update(payload_type, payload) }
11
+ let(:payload_type) { :some_type }
6
12
  let(:payload) { double(:payload) }
7
13
  let(:bot) { Telegram::Bot::ClientStub.new(bot_name) }
8
14
  let(:bot_name) { 'bot' }
9
- let(:session) do
10
- session = Telegram::Bot::UpdatesController::Session::TestSessionHash.new
11
- allow_any_instance_of(controller_class).to receive(:session) { session }
12
- session
15
+ let(:session) { controller.send(:session) }
16
+
17
+ def dispatch(bot = self.bot, update = self.update)
18
+ controller.dispatch_again(bot, update)
13
19
  end
14
20
 
15
21
  def dispatch_message(text, options = {})
16
- payload = build_payload :message, options.merge(text: text)
17
- controller_class.dispatch bot, payload
22
+ update = build_update :message, options.merge(text: text)
23
+ dispatch bot, update
18
24
  end
19
25
 
20
- def build_payload(type, content)
26
+ def build_update(type, content)
21
27
  deep_stringify type => content
22
28
  end
23
29
 
@@ -59,24 +59,16 @@ module Telegram
59
59
  end
60
60
  end
61
61
 
62
- class TestSessionHash < SessionHash
63
- def initialize
64
- @data = {}
65
- @loaded = true
66
- @exists = true
67
- end
68
-
69
- alias_method :destroy, :clear
70
- alias_method :load!, :id
71
- alias_method :commit, :id
72
- end
73
-
74
62
  module ConfigMethods
75
63
  delegate :session_store, to: :config
76
64
 
77
65
  def session_store=(store)
78
66
  config.session_store = ActiveSupport::Cache.lookup_store(store)
79
67
  end
68
+
69
+ def use_session!
70
+ include Session
71
+ end
80
72
  end
81
73
  end
82
74
  end
@@ -0,0 +1,47 @@
1
+ module Telegram
2
+ module Bot
3
+ class UpdatesController
4
+ module Testing
5
+ IVARS_TO_KEEP = %i(@_session).freeze
6
+
7
+ # Perform multiple dispatches on same instance.
8
+ def dispatch_again(bot = nil, update = nil)
9
+ recycle!
10
+ initialize(bot, update)
11
+ dispatch
12
+ end
13
+
14
+ # Cleans controller between dispatches.
15
+ # Seems like there is nothing to clean between requests for now:
16
+ # everything will be rewriten with #initialize.
17
+ #
18
+ # With `full` set to `true` it'll clear all cached instance variables.
19
+ def recycle!(full = false)
20
+ return unless full
21
+ (instance_variables - IVARS_TO_KEEP).each do |ivar|
22
+ remove_instance_variable(ivar)
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ # Stubs session.
29
+ def session
30
+ @_session ||= Testing::SessionHash.new
31
+ end
32
+
33
+ class SessionHash < Session::SessionHash
34
+ def initialize
35
+ @data = {}
36
+ @loaded = true
37
+ @exists = true
38
+ end
39
+
40
+ alias_method :destroy, :clear
41
+ alias_method :load!, :id
42
+ alias_method :commit, :id
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,7 +4,7 @@ module Telegram
4
4
  # Include this module to type cast update to Virtus model
5
5
  # using `telegram-bot-types` gem (install this gem first).
6
6
  module TypedUpdate
7
- def initialize(bot, update)
7
+ def initialize(bot = nil, update = nil)
8
8
  update = Types::Update.new(update) if update && !update.is_a?(Types::Update)
9
9
  super
10
10
  end
@@ -1,6 +1,6 @@
1
1
  module Telegram
2
2
  module Bot
3
- VERSION = '0.4.2'.freeze
3
+ VERSION = '0.5.0'.freeze
4
4
 
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION
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.4.2
4
+ version: 0.5.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: 2016-02-29 00:00:00.000000000 Z
11
+ date: 2016-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -91,6 +91,7 @@ files:
91
91
  - ".rspec"
92
92
  - ".rubocop.yml"
93
93
  - ".travis.yml"
94
+ - CHANGELOG.md
94
95
  - Gemfile
95
96
  - LICENSE.txt
96
97
  - README.md
@@ -111,8 +112,10 @@ files:
111
112
  - lib/telegram/bot/updates_controller.rb
112
113
  - lib/telegram/bot/updates_controller/instrumentation.rb
113
114
  - lib/telegram/bot/updates_controller/log_subscriber.rb
115
+ - lib/telegram/bot/updates_controller/message_context.rb
114
116
  - lib/telegram/bot/updates_controller/rspec_helpers.rb
115
117
  - lib/telegram/bot/updates_controller/session.rb
118
+ - lib/telegram/bot/updates_controller/testing.rb
116
119
  - lib/telegram/bot/updates_controller/typed_update.rb
117
120
  - lib/telegram/bot/updates_poller.rb
118
121
  - lib/telegram/bot/version.rb