telegram-bot 0.4.2 → 0.5.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
  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