lita 3.3.1 → 4.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.travis.yml +3 -0
  4. data/lib/lita.rb +45 -97
  5. data/lib/lita/adapter.rb +38 -17
  6. data/lib/lita/adapters/shell.rb +5 -3
  7. data/lib/lita/authorization.rb +109 -60
  8. data/lib/lita/builder.rb +38 -0
  9. data/lib/lita/callback.rb +37 -0
  10. data/lib/lita/cli.rb +2 -0
  11. data/lib/lita/config.rb +1 -18
  12. data/lib/lita/configurable.rb +29 -0
  13. data/lib/lita/configuration.rb +179 -0
  14. data/lib/lita/configuration_validator.rb +66 -0
  15. data/lib/lita/daemon.rb +4 -11
  16. data/lib/lita/default_configuration.rb +146 -0
  17. data/lib/lita/errors.rb +9 -0
  18. data/lib/lita/handler.rb +5 -264
  19. data/lib/lita/handler/chat_router.rb +130 -0
  20. data/lib/lita/handler/common.rb +114 -0
  21. data/lib/lita/handler/event_router.rb +77 -0
  22. data/lib/lita/handler/http_router.rb +26 -0
  23. data/lib/lita/handlers/authorization.rb +13 -18
  24. data/lib/lita/handlers/deprecation_check.rb +24 -0
  25. data/lib/lita/handlers/help.rb +5 -3
  26. data/lib/lita/handlers/info.rb +2 -2
  27. data/lib/lita/http_callback.rb +29 -0
  28. data/lib/lita/http_route.rb +41 -26
  29. data/lib/lita/namespace.rb +23 -0
  30. data/lib/lita/rack_app.rb +29 -2
  31. data/lib/lita/registry.rb +133 -0
  32. data/lib/lita/robot.rb +58 -20
  33. data/lib/lita/route_validator.rb +12 -4
  34. data/lib/lita/rspec.rb +23 -14
  35. data/lib/lita/rspec/handler.rb +93 -23
  36. data/lib/lita/rspec/matchers/chat_route_matcher.rb +48 -0
  37. data/lib/lita/rspec/matchers/deprecated.rb +36 -0
  38. data/lib/lita/rspec/matchers/event_route_matcher.rb +27 -0
  39. data/lib/lita/rspec/matchers/http_route_matcher.rb +18 -60
  40. data/lib/lita/user.rb +0 -6
  41. data/lib/lita/util.rb +1 -8
  42. data/lib/lita/version.rb +1 -1
  43. data/lita.gemspec +1 -0
  44. data/spec/lita/adapter_spec.rb +25 -7
  45. data/spec/lita/adapters/shell_spec.rb +24 -4
  46. data/spec/lita/authorization_spec.rb +57 -38
  47. data/spec/lita/builder_spec.rb +39 -0
  48. data/spec/lita/config_spec.rb +0 -24
  49. data/spec/lita/configuration_spec.rb +222 -0
  50. data/spec/lita/configuration_validator_spec.rb +112 -0
  51. data/spec/lita/daemon_spec.rb +2 -2
  52. data/spec/lita/default_configuration_spec.rb +254 -0
  53. data/spec/lita/handler/chat_router_spec.rb +192 -0
  54. data/spec/lita/handler/common_spec.rb +272 -0
  55. data/spec/lita/handler/event_router_spec.rb +54 -0
  56. data/spec/lita/handler/http_router_spec.rb +106 -0
  57. data/spec/lita/handler_spec.rb +20 -291
  58. data/spec/lita/handlers/authorization_spec.rb +9 -11
  59. data/spec/lita/handlers/deprecation_check_spec.rb +21 -0
  60. data/spec/lita/handlers/help_spec.rb +31 -9
  61. data/spec/lita/handlers/info_spec.rb +2 -2
  62. data/spec/lita/handlers/room_spec.rb +5 -3
  63. data/spec/lita/robot_spec.rb +30 -11
  64. data/spec/lita/rspec_spec.rb +71 -31
  65. data/spec/lita/user_spec.rb +2 -2
  66. data/spec/lita_spec.rb +62 -4
  67. data/spec/spec_helper.rb +7 -0
  68. data/templates/locales/en.yml +56 -4
  69. data/templates/plugin/gemspec.tt +1 -0
  70. data/templates/plugin/spec/spec_helper.tt +4 -0
  71. metadata +54 -8
  72. data/lib/lita/rspec/matchers/event_subscription_matcher.rb +0 -67
  73. data/lib/lita/rspec/matchers/route_matcher.rb +0 -69
  74. data/spec/lita/rack_app_spec.rb +0 -92
@@ -0,0 +1,9 @@
1
+ module Lita
2
+ # The root exception class that all Lita-specific exceptions inherit from.
3
+ # @since 4.0.0
4
+ class Error < StandardError; end
5
+
6
+ # An exception raised when a configuration attribute is invalid.
7
+ # @since 4.0.0
8
+ class ValidationError < Error; end
9
+ end
@@ -1,268 +1,9 @@
1
1
  module Lita
2
- # Base class for objects that add new behavior to Lita.
2
+ # Base class for objects that add new behavior to Lita. {Handler} is simply a class with all
3
+ # types of routers mixed in.
3
4
  class Handler
4
- extend Forwardable
5
-
6
- # A Redis::Namespace scoped to the handler.
7
- # @return [Redis::Namespace]
8
- attr_reader :redis
9
-
10
- # The running {Lita::Robot} instance.
11
- # @return [Lita::Robot]
12
- attr_reader :robot
13
-
14
- # A Struct representing a chat route defined by a handler.
15
- class Route < Struct.new(
16
- :pattern,
17
- :method_name,
18
- :command,
19
- :required_groups,
20
- :help,
21
- :extensions
22
- )
23
- alias_method :command?, :command
24
- end
25
-
26
- class << self
27
- # Creates a chat route.
28
- # @param pattern [Regexp] A regular expression to match incoming messages
29
- # against.
30
- # @param method [Symbol, String] The name of the method to trigger.
31
- # @param command [Boolean] Whether or not the message must be directed at
32
- # the robot.
33
- # @param restrict_to [Array<Symbol, String>, nil] A list of authorization
34
- # groups the user must be in to trigger the route.
35
- # @param help [Hash] A map of example invocations to descriptions.
36
- # @param extensions [Hash] Aribtrary additional data that can be used by Lita extensions.
37
- # @return [void]
38
- def route(pattern, method, **options)
39
- options = default_route_options.merge(options)
40
- options[:restrict_to] = options[:restrict_to].nil? ? nil : Array(options[:restrict_to])
41
- routes << Route.new(
42
- pattern,
43
- method,
44
- options.delete(:command),
45
- options.delete(:restrict_to),
46
- options.delete(:help),
47
- options
48
- )
49
- end
50
-
51
- # A list of chat routes defined by the handler.
52
- # @return [Array<Lita::Handler::Route>]
53
- def routes
54
- @routes ||= []
55
- end
56
-
57
- # The main entry point for the handler at runtime. Checks if the message
58
- # matches any of the routes and invokes the route's method if it does.
59
- # Called by {Lita::Robot#receive}.
60
- # @param robot [Lita::Robot] The currently running robot.
61
- # @param message [Lita::Message] The incoming message.
62
- # @return [void]
63
- def dispatch(robot, message)
64
- routes.each do |route|
65
- next unless route_applies?(route, message, robot)
66
- log_dispatch(route)
67
- dispatch_to_route(route, robot, message)
68
- end
69
- end
70
-
71
- # Dispatch directly to a {Route}, ignoring route conditions.
72
- # @param route [Route] The route to invoke.
73
- # @param robot [Lita::Robot] The currently running robot.
74
- # @param message [Lita::Message] The incoming message.
75
- # @return [void]
76
- # @since 3.3.0
77
- def dispatch_to_route(route, robot, message)
78
- response = Response.new(message, route.pattern)
79
- Lita.hooks[:trigger_route].each { |hook| hook.call(response: response, route: route) }
80
- new(robot).public_send(route.method_name, response)
81
- rescue Exception => e
82
- log_dispatch_error(e)
83
- raise e if rspec_loaded?
84
- end
85
-
86
- # Creates a new {Lita::HTTPRoute} which is used to define an HTTP route
87
- # for the built-in web server.
88
- # @see Lita::HTTPRoute
89
- # @return [Lita::HTTPRoute] The new {Lita::HTTPRoute}.
90
- def http
91
- HTTPRoute.new(self)
92
- end
93
-
94
- # An array of all HTTP routes defined for the handler.
95
- # @return [Array<Lita::HTTPRoute>] The array of routes.
96
- def http_routes
97
- @http_routes ||= []
98
- end
99
-
100
- # The namespace for the handler, used for its configuration object and
101
- # Redis store. If the handler is an anonymous class, it must explicitly
102
- # define +self.name+.
103
- # @return [String] The handler's namespace.
104
- # @raise [RuntimeError] If +self.name+ is not defined.
105
- def namespace
106
- if name
107
- Util.underscore(name.split("::").last)
108
- else
109
- raise I18n.t("lita.handler.name_required")
110
- end
111
- end
112
-
113
- # Registers an event subscription. When an event is triggered with
114
- # {trigger}, a new instance of the handler will be created and the
115
- # instance method name supplied to {on} will be invoked with a payload
116
- # (a hash of arbitrary keys and values).
117
- # @param event_name [String, Symbol] The name of the event to subscribe
118
- # to.
119
- # @param method_name [String, Symbol] The name of the instance method on
120
- # the handler that should be invoked when the event is triggered.
121
- # @return [void]
122
- def on(event_name, method_name)
123
- event_subscriptions[normalize_event(event_name)] << method_name
124
- end
125
-
126
- # Returns the translation for a key, automatically namespaced to the handler.
127
- # @param key [String] The key of the translation.
128
- # @param hash [Hash] An optional hash of values to be interpolated in the string.
129
- # @return [String] The translated string.
130
- # @since 3.0.0
131
- def translate(key, hash = {})
132
- I18n.translate("lita.handlers.#{namespace}.#{key}", hash)
133
- end
134
-
135
- alias_method :t, :translate
136
-
137
- # Triggers an event, invoking methods previously registered with {on} and
138
- # passing them a payload hash with any arbitrary data.
139
- # @param robot [Lita::Robot] The currently running robot instance.
140
- # @param event_name [String, Symbol], The name of the event to trigger.
141
- # @param payload [Hash] An optional hash of arbitrary data.
142
- # @return [void]
143
- def trigger(robot, event_name, payload = {})
144
- event_subscriptions[normalize_event(event_name)].each do |method_name|
145
- new(robot).public_send(method_name, payload)
146
- end
147
- end
148
-
149
- private
150
-
151
- def default_route_options
152
- {
153
- command: false,
154
- restrict_to: nil,
155
- help: {}
156
- }
157
- end
158
-
159
- # A hash of arrays used to store event subscriptions registered with {on}.
160
- def event_subscriptions
161
- @event_subscriptions ||= Hash.new { |h, k| h[k] = [] }
162
- end
163
-
164
- # Determines whether or not an incoming messages should trigger a route.
165
- def route_applies?(route, message, robot)
166
- RouteValidator.new(self, route, message, robot).call
167
- end
168
-
169
- # Checks if RSpec is loaded. If so, assume we are testing and let handler
170
- # exceptions bubble up.
171
- def rspec_loaded?
172
- defined?(::RSpec)
173
- end
174
-
175
- # Logs the dispatch of message.
176
- def log_dispatch(route)
177
- Lita.logger.debug I18n.t(
178
- "lita.handler.dispatch",
179
- handler: name,
180
- method: route.method_name
181
- )
182
- end
183
-
184
- def log_dispatch_error(e)
185
- Lita.logger.error I18n.t(
186
- "lita.handler.exception",
187
- handler: name,
188
- message: e.message,
189
- backtrace: e.backtrace.join("\n")
190
- )
191
- end
192
-
193
- def normalize_event(event_name)
194
- event_name.to_s.downcase.strip.to_sym
195
- end
196
- end
197
-
198
- # @param robot [Lita::Robot] The currently running robot.
199
- def initialize(robot)
200
- @robot = robot
201
- @redis = Redis::Namespace.new(redis_namespace, redis: Lita.redis)
202
- end
203
-
204
- # Invokes the given block after the given number of seconds.
205
- # @param interval [Integer] The number of seconds to wait before invoking the block.
206
- # @yieldparam timer [Lita::Timer] The current {Lita::Timer} instance.
207
- # @return [void]
208
- # @since 3.0.0
209
- def after(interval, &block)
210
- Thread.new { Timer.new(interval: interval, &block).start }
211
- end
212
-
213
- # The handler's config object.
214
- # @return [Lita::Config] The handler's config object.
215
- # @since 3.2.0
216
- def config
217
- Lita.config.handlers[self.class.namespace]
218
- end
219
-
220
- # Invokes the given block repeatedly, waiting the given number of seconds between each
221
- # invocation.
222
- # @param interval [Integer] The number of seconds to wait before each invocation of the block.
223
- # @yieldparam timer [Lita::Timer] The current {Lita::Timer} instance.
224
- # @return [void]
225
- # @note The block should call {Lita::Timer#stop} at a terminating condition to avoid infinite
226
- # recursion.
227
- # @since 3.0.0
228
- def every(interval, &block)
229
- Thread.new { Timer.new(interval: interval, recurring: true, &block).start }
230
- end
231
-
232
- # Creates a new +Faraday::Connection+ for making HTTP requests.
233
- # @param options [Hash] A set of options passed on to Faraday.
234
- # @yield [builder] A Faraday builder object for adding middleware.
235
- # @return [Faraday::Connection] The new connection object.
236
- def http(options = {}, &block)
237
- options = default_faraday_options.merge(options)
238
- Faraday::Connection.new(nil, options, &block)
239
- end
240
-
241
- # The Lita logger.
242
- # @return [Lita::Logger] The Lita logger.
243
- # @since 3.2.0
244
- def log
245
- Lita.logger
246
- end
247
-
248
- # @see .translate
249
- def translate(*args)
250
- self.class.translate(*args)
251
- end
252
-
253
- alias_method :t, :translate
254
-
255
- private
256
-
257
- # Default options for new Faraday connections. Sets the user agent to the
258
- # current version of Lita.
259
- def default_faraday_options
260
- { headers: { "User-Agent" => "Lita v#{VERSION}" } }
261
- end
262
-
263
- # The handler's namespace for Redis.
264
- def redis_namespace
265
- "handlers:#{self.class.namespace}"
266
- end
5
+ extend ChatRouter
6
+ extend HTTPRouter
7
+ extend EventRouter
267
8
  end
268
9
  end
@@ -0,0 +1,130 @@
1
+ module Lita
2
+ class Handler
3
+ # A handler mixin that provides the methods necessary for responding to chat messages.
4
+ # @since 4.0.0
5
+ module ChatRouter
6
+ # Includes common handler methods in any class that includes {ChatRouter}.
7
+ def self.extended(klass)
8
+ klass.send(:include, Common)
9
+ end
10
+
11
+ # A Struct representing a chat route defined by a handler.
12
+ class Route < Struct.new(
13
+ :pattern,
14
+ :callback,
15
+ :command,
16
+ :required_groups,
17
+ :help,
18
+ :extensions
19
+ )
20
+ alias_method :command?, :command
21
+ end
22
+
23
+ # @overload route(pattern, method_name, **options)
24
+ # Creates a chat route.
25
+ # @param pattern [Regexp] A regular expression to match incoming messages against.
26
+ # @param method_name [Symbol, String] The name of the instance method to trigger.
27
+ # @param command [Boolean] Whether or not the message must be directed at the robot.
28
+ # @param restrict_to [Array<Symbol, String>, nil] An optional list of authorization
29
+ # groups the user must be in to trigger the route.
30
+ # @param help [Hash] An optional map of example invocations to descriptions.
31
+ # @param options [Hash] Aribtrary additional data that can be used by Lita extensions.
32
+ # @return [void]
33
+ # @overload route(pattern, **options)
34
+ # Creates a chat route.
35
+ # @param pattern [Regexp] A regular expression to match incoming messages against.
36
+ # @param command [Boolean] Whether or not the message must be directed at the robot.
37
+ # @param restrict_to [Array<Symbol, String>, nil] An optional list of authorization
38
+ # groups the user must be in to trigger the route.
39
+ # @param help [Hash] An optional map of example invocations to descriptions.
40
+ # @param options [Hash] Aribtrary additional data that can be used by Lita extensions.
41
+ # @yield The body of the route's callback.
42
+ # @return [void]
43
+ # @since 4.0.0
44
+ def route(pattern, method_name = nil, **options)
45
+ options = default_route_options.merge(options)
46
+ options[:restrict_to] = options[:restrict_to].nil? ? nil : Array(options[:restrict_to])
47
+ routes << Route.new(
48
+ pattern,
49
+ Callback.new(method_name || (proc if block_given?)),
50
+ options.delete(:command),
51
+ options.delete(:restrict_to),
52
+ options.delete(:help),
53
+ options
54
+ )
55
+ end
56
+
57
+ # A list of chat routes defined by the handler.
58
+ # @return [Array<Lita::Handler::Route>]
59
+ def routes
60
+ @routes ||= []
61
+ end
62
+
63
+ # The main entry point for the handler at runtime. Checks if the message
64
+ # matches any of the routes and invokes the route's method if it does.
65
+ # Called by {Lita::Robot#receive}.
66
+ # @param robot [Lita::Robot] The currently running robot.
67
+ # @param message [Lita::Message] The incoming message.
68
+ # @return [Boolean] Whether or not the message matched any routes.
69
+ def dispatch(robot, message)
70
+ routes.map do |route|
71
+ next unless route_applies?(route, message, robot)
72
+ log_dispatch(route)
73
+ dispatch_to_route(route, robot, message)
74
+ true
75
+ end.any?
76
+ end
77
+
78
+ # Dispatch directly to a {Route}, ignoring route conditions.
79
+ # @param route [Route] The route to invoke.
80
+ # @param robot [Lita::Robot] The currently running robot.
81
+ # @param message [Lita::Message] The incoming message.
82
+ # @return [void]
83
+ # @since 3.3.0
84
+ def dispatch_to_route(route, robot, message)
85
+ response = Response.new(message, route.pattern)
86
+ robot.hooks[:trigger_route].each { |hook| hook.call(response: response, route: route) }
87
+ handler = new(robot)
88
+ route.callback.call(handler, response)
89
+ rescue Exception => e
90
+ log_dispatch_error(e)
91
+ raise e if Lita.test_mode?
92
+ end
93
+
94
+ private
95
+
96
+ # The default options for every chat route.
97
+ def default_route_options
98
+ {
99
+ command: false,
100
+ restrict_to: nil,
101
+ help: {}
102
+ }
103
+ end
104
+
105
+ # Determines whether or not an incoming messages should trigger a route.
106
+ def route_applies?(route, message, robot)
107
+ RouteValidator.new(self, route, message, robot).call
108
+ end
109
+
110
+ # Logs the dispatch of message.
111
+ def log_dispatch(route)
112
+ Lita.logger.debug I18n.t(
113
+ "lita.handler.dispatch",
114
+ handler: name,
115
+ method: route.callback.method_name || "(block)"
116
+ )
117
+ end
118
+
119
+ # Logs an error encountered during dispatch.
120
+ def log_dispatch_error(e)
121
+ Lita.logger.error I18n.t(
122
+ "lita.handler.exception",
123
+ handler: name,
124
+ message: e.message,
125
+ backtrace: e.backtrace.join("\n")
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,114 @@
1
+ module Lita
2
+ class Handler
3
+ # Methods included in any class that includes at least one type of router.
4
+ # @since 4.0.0
5
+ module Common
6
+ # Adds common functionality to the class and initializes the handler's configuration.
7
+ def self.included(klass)
8
+ klass.extend(ClassMethods)
9
+ klass.extend(Namespace)
10
+ klass.extend(Configurable)
11
+ klass.configuration = Configuration.new
12
+ end
13
+
14
+ # Common class-level methods for all handlers.
15
+ module ClassMethods
16
+ # Returns the translation for a key, automatically namespaced to the handler.
17
+ # @param key [String] The key of the translation.
18
+ # @param hash [Hash] An optional hash of values to be interpolated in the string.
19
+ # @return [String] The translated string.
20
+ # @since 3.0.0
21
+ def translate(key, hash = {})
22
+ I18n.translate("lita.handlers.#{namespace}.#{key}", hash)
23
+ end
24
+
25
+ alias_method :t, :translate
26
+ end
27
+
28
+ # A Redis::Namespace scoped to the handler.
29
+ # @return [Redis::Namespace]
30
+ attr_reader :redis
31
+
32
+ # The running {Lita::Robot} instance.
33
+ # @return [Lita::Robot]
34
+ attr_reader :robot
35
+
36
+ # @param robot [Lita::Robot] The currently running robot.
37
+ def initialize(robot)
38
+ @robot = robot
39
+ @redis = Redis::Namespace.new(redis_namespace, redis: Lita.redis)
40
+ end
41
+
42
+ # Invokes the given block after the given number of seconds.
43
+ # @param interval [Integer] The number of seconds to wait before invoking the block.
44
+ # @yieldparam timer [Lita::Timer] The current {Lita::Timer} instance.
45
+ # @return [void]
46
+ # @since 3.0.0
47
+ def after(interval, &block)
48
+ Thread.new { Timer.new(interval: interval, &block).start }
49
+ end
50
+
51
+ # The handler's config object.
52
+ # @return [Object, Lita::Config] The handler's configuration object.
53
+ # @since 3.2.0
54
+ def config
55
+ if robot.config.handlers.respond_to?(self.class.namespace)
56
+ robot.config.handlers.public_send(self.class.namespace)
57
+ end
58
+ end
59
+
60
+ # Invokes the given block repeatedly, waiting the given number of seconds between each
61
+ # invocation.
62
+ # @param interval [Integer] The number of seconds to wait before each invocation of the block.
63
+ # @yieldparam timer [Lita::Timer] The current {Lita::Timer} instance.
64
+ # @return [void]
65
+ # @note The block should call {Lita::Timer#stop} at a terminating condition to avoid infinite
66
+ # recursion.
67
+ # @since 3.0.0
68
+ def every(interval, &block)
69
+ Thread.new { Timer.new(interval: interval, recurring: true, &block).start }
70
+ end
71
+
72
+ # Creates a new +Faraday::Connection+ for making HTTP requests.
73
+ # @param options [Hash] A set of options passed on to Faraday.
74
+ # @yield [builder] A Faraday builder object for adding middleware.
75
+ # @return [Faraday::Connection] The new connection object.
76
+ def http(options = {})
77
+ options = default_faraday_options.merge(options)
78
+
79
+ if block_given?
80
+ Faraday::Connection.new(nil, options, &proc)
81
+ else
82
+ Faraday::Connection.new(nil, options)
83
+ end
84
+ end
85
+
86
+ # The Lita logger.
87
+ # @return [Lita::Logger] The Lita logger.
88
+ # @since 3.2.0
89
+ def log
90
+ Lita.logger
91
+ end
92
+
93
+ # @see .translate
94
+ def translate(*args)
95
+ self.class.translate(*args)
96
+ end
97
+
98
+ alias_method :t, :translate
99
+
100
+ private
101
+
102
+ # Default options for new Faraday connections. Sets the user agent to the
103
+ # current version of Lita.
104
+ def default_faraday_options
105
+ { headers: { "User-Agent" => "Lita v#{VERSION}" } }
106
+ end
107
+
108
+ # The handler's namespace for Redis.
109
+ def redis_namespace
110
+ "handlers:#{self.class.namespace}"
111
+ end
112
+ end
113
+ end
114
+ end