lita 2.7.2 → 3.0.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop.yml +26 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +6 -470
  6. data/Rakefile +3 -3
  7. data/lib/lita.rb +27 -19
  8. data/lib/lita/adapter.rb +46 -5
  9. data/lib/lita/adapters/shell.rb +18 -13
  10. data/lib/lita/authorization.rb +1 -1
  11. data/lib/lita/cli.rb +37 -23
  12. data/lib/lita/common.rb +35 -0
  13. data/lib/lita/config.rb +33 -13
  14. data/lib/lita/daemon.rb +15 -12
  15. data/lib/lita/handler.rb +49 -9
  16. data/lib/lita/handlers/authorization.rb +47 -47
  17. data/lib/lita/handlers/help.rb +16 -17
  18. data/lib/lita/handlers/info.rb +38 -0
  19. data/lib/lita/handlers/room.rb +32 -0
  20. data/lib/lita/http_route.rb +30 -19
  21. data/lib/lita/message.rb +3 -6
  22. data/lib/lita/rack_app.rb +11 -89
  23. data/lib/lita/response.rb +5 -15
  24. data/lib/lita/robot.rb +26 -10
  25. data/lib/lita/rspec.rb +6 -8
  26. data/lib/lita/rspec/handler.rb +49 -121
  27. data/lib/lita/rspec/matchers/event_subscription_matcher.rb +67 -0
  28. data/lib/lita/rspec/matchers/http_route_matcher.rb +72 -0
  29. data/lib/lita/rspec/matchers/route_matcher.rb +69 -0
  30. data/lib/lita/source.rb +5 -18
  31. data/lib/lita/timer.rb +45 -0
  32. data/lib/lita/user.rb +51 -4
  33. data/lib/lita/util.rb +5 -5
  34. data/lib/lita/version.rb +1 -1
  35. data/lita.gemspec +6 -3
  36. data/spec/lita/adapter_spec.rb +10 -2
  37. data/spec/lita/adapters/shell_spec.rb +3 -3
  38. data/spec/lita/authorization_spec.rb +11 -11
  39. data/spec/lita/config_spec.rb +8 -0
  40. data/spec/lita/daemon_spec.rb +65 -0
  41. data/spec/lita/handler_spec.rb +50 -11
  42. data/spec/lita/handlers/authorization_spec.rb +1 -1
  43. data/spec/lita/handlers/info_spec.rb +31 -0
  44. data/spec/lita/handlers/room_spec.rb +20 -0
  45. data/spec/lita/logger_spec.rb +1 -1
  46. data/spec/lita/message_spec.rb +4 -4
  47. data/spec/lita/rack_app_spec.rb +92 -0
  48. data/spec/lita/response_spec.rb +17 -8
  49. data/spec/lita/robot_spec.rb +23 -14
  50. data/spec/lita/rspec_spec.rb +1 -1
  51. data/spec/lita/source_spec.rb +0 -16
  52. data/spec/lita/timer_spec.rb +30 -0
  53. data/spec/lita/user_spec.rb +66 -6
  54. data/spec/lita_spec.rb +37 -0
  55. data/spec/spec_helper.rb +11 -0
  56. data/templates/locales/en.yml +90 -0
  57. data/templates/plugin/Rakefile +1 -1
  58. data/templates/plugin/lib/lita/plugin_type/plugin.tt +4 -0
  59. data/templates/plugin/locales/en.yml.tt +4 -0
  60. metadata +77 -18
  61. data/lib/lita/handlers/web.rb +0 -25
  62. data/spec/lita/handlers/web_spec.rb +0 -19
@@ -19,19 +19,14 @@ module Lita
19
19
  Process.daemon(true)
20
20
  File.open(@pid_path, "w") { |f| f.write(Process.pid) }
21
21
  set_up_logs
22
- at_exit { FileUtils.rm(@pid_path) if File.exist?(@pid_path) }
22
+ at_exit { FileUtils.rm(@pid_path) if File.exists?(@pid_path) }
23
23
  end
24
24
 
25
25
  private
26
26
 
27
27
  # Abort if Lita is already running.
28
28
  def ensure_not_running
29
- if File.exist?(@pid_path)
30
- abort <<-FATAL.chomp
31
- PID file exists at #{@pid_path}. Lita may already be running. \
32
- Kill the existing process or remove the PID file and then start Lita.
33
- FATAL
34
- end
29
+ abort I18n.t("lita.daemon.pid_exists", path: @pid_path) if File.exists?(@pid_path)
35
30
  end
36
31
 
37
32
  # Call the appropriate method depending on kill mode.
@@ -45,18 +40,26 @@ FATAL
45
40
 
46
41
  # Try to kill an existing process.
47
42
  def kill_existing_process
48
- pid = File.read(@pid_path).strip.to_i
43
+ pid = File.read(@pid_path).to_s.strip.to_i
49
44
  Process.kill("TERM", pid)
50
45
  rescue Errno::ESRCH, RangeError, Errno::EPERM
51
- abort "Failed to kill existing Lita process #{pid}."
46
+ abort I18n.t("lita.daemon.kill_failure", pid: pid)
52
47
  end
53
48
 
54
49
  # Redirect the standard streams to a log file.
55
50
  def set_up_logs
56
51
  log_file = File.new(@log_path, "a")
57
- $stdout.reopen(log_file)
58
- $stderr.reopen(log_file)
59
- $stderr.sync = $stdout.sync = true
52
+ stdout.reopen(log_file)
53
+ stderr.reopen(log_file)
54
+ stderr.sync = stdout.sync = true
55
+ end
56
+
57
+ def stdout
58
+ $stdout
59
+ end
60
+
61
+ def stderr
62
+ $stderr
60
63
  end
61
64
  end
62
65
  end
@@ -91,7 +91,7 @@ module Lita
91
91
  if name
92
92
  Util.underscore(name.split("::").last)
93
93
  else
94
- raise "Handlers that are anonymous classes must define self.name."
94
+ raise I18n.t("lita.handler.name_required")
95
95
  end
96
96
  end
97
97
 
@@ -108,6 +108,17 @@ module Lita
108
108
  event_subscriptions[normalize_event(event_name)] << method_name
109
109
  end
110
110
 
111
+ # Returns the translation for a key, automatically namespaced to the handler.
112
+ # @param key [String] The key of the translation.
113
+ # @param hash [Hash] An optional hash of values to be interpolated in the string.
114
+ # @return [String] The translated string.
115
+ # @since 3.0.0
116
+ def translate(key, hash = {})
117
+ I18n.translate("lita.handlers.#{namespace}.#{key}", hash)
118
+ end
119
+
120
+ alias_method :t, :translate
121
+
111
122
  # Triggers an event, invoking methods previously registered with {on} and
112
123
  # passing them a payload hash with any arbitrary data.
113
124
  # @param robot [Lita::Robot] The currently running robot instance.
@@ -159,17 +170,20 @@ module Lita
159
170
 
160
171
  # Logs the dispatch of message.
161
172
  def log_dispatch(route)
162
- Lita.logger.debug <<-LOG.chomp
163
- Dispatching message to #{self}##{route.method_name}.
164
- LOG
173
+ Lita.logger.debug I18n.t(
174
+ "lita.handler.dispatch",
175
+ handler: name,
176
+ method: route.method_name
177
+ )
165
178
  end
166
179
 
167
180
  def log_dispatch_error(e)
168
- Lita.logger.error <<-ERROR.chomp
169
- #{name} crashed. The exception was:
170
- #{e.message}
171
- #{e.backtrace.join("\n")}
172
- ERROR
181
+ Lita.logger.error I18n.t(
182
+ "lita.handler.exception",
183
+ handler: name,
184
+ message: e.message,
185
+ backtrace: e.backtrace.join("\n")
186
+ )
173
187
  end
174
188
 
175
189
  def normalize_event(event_name)
@@ -183,6 +197,25 @@ ERROR
183
197
  @redis = Redis::Namespace.new(redis_namespace, redis: Lita.redis)
184
198
  end
185
199
 
200
+ # Invokes the given block after the given number of seconds.
201
+ # @param interval [Integer] The number of seconds to wait before invoking the block.
202
+ # @yieldparam timer [Lita::Timer] The current {Lita::Timer} instance.
203
+ # @since 3.0.0
204
+ def after(interval, &block)
205
+ Timer.new(interval: interval, &block).start
206
+ end
207
+
208
+ # Invokes the given block repeatedly, waiting the given number of seconds between each
209
+ # invocation.
210
+ # @param interval [Integer] The number of seconds to wait before each invocation of the block.
211
+ # @yieldparam timer [Lita::Timer] The current {Lita::Timer} instance.
212
+ # @note The block should call {Lita::Timer#stop} at a terminating condition to avoid infinite
213
+ # recursion.
214
+ # @since 3.0.0
215
+ def every(interval, &block)
216
+ Timer.new(interval: interval, recurring: true, &block).start
217
+ end
218
+
186
219
  # Creates a new +Faraday::Connection+ for making HTTP requests.
187
220
  # @param options [Hash] A set of options passed on to Faraday.
188
221
  # @yield [builder] A Faraday builder object for adding middleware.
@@ -192,6 +225,13 @@ ERROR
192
225
  Faraday::Connection.new(nil, options, &block)
193
226
  end
194
227
 
228
+ # @see .translate
229
+ def translate(*args)
230
+ self.class.translate(*args)
231
+ end
232
+
233
+ alias_method :t, :translate
234
+
195
235
  private
196
236
 
197
237
  # Default options for new Faraday connections. Sets the user agent to the
@@ -1,4 +1,5 @@
1
1
  module Lita
2
+ # A namespace to hold all subclasses of {Handler}.
2
3
  module Handlers
3
4
  # Provides a chat interface for administering authorization groups.
4
5
  class Authorization < Handler
@@ -7,28 +8,17 @@ module Lita
7
8
  :add,
8
9
  command: true,
9
10
  restrict_to: :admins,
10
- help: {
11
- "auth add USER GROUP" => <<-HELP.chomp
12
- Add USER to authorization group GROUP. Requires admin privileges.
13
- HELP
14
- }
11
+ help: { t("help.add_key") => t("help.add_value") }
15
12
  )
16
13
  route(
17
14
  /^auth\s+remove/,
18
15
  :remove,
19
16
  command: true,
20
17
  restrict_to: :admins,
21
- help: {
22
- "auth remove USER GROUP" => <<-HELP.chomp
23
- Remove USER from authorization group GROUP. Requires admin privileges.
24
- HELP
25
- }
18
+ help: { t("help.remove_key") => t("help.remove_value") }
26
19
  )
27
20
  route(/^auth\s+list/, :list, command: true, restrict_to: :admins, help: {
28
- "auth list [GROUP]" => <<-HELP.chomp
29
- List authorization groups and the users in them. If GROUP is supplied, only \
30
- lists that group.
31
- HELP
21
+ t("help.list_key") => t("help.list_value")
32
22
  })
33
23
 
34
24
  # Adds a user to an authorization group.
@@ -38,9 +28,9 @@ HELP
38
28
  return unless valid_message?(response)
39
29
 
40
30
  if Lita::Authorization.add_user_to_group(response.user, @user, @group)
41
- response.reply "#{@user.name} was added to #{@group}."
31
+ response.reply t("user_added", user: @user.name, group: @group)
42
32
  else
43
- response.reply "#{@user.name} was already in #{@group}."
33
+ response.reply t("user_already_in", user: @user.name, group: @group)
44
34
  end
45
35
  end
46
36
 
@@ -50,14 +40,13 @@ HELP
50
40
  def remove(response)
51
41
  return unless valid_message?(response)
52
42
 
53
- if Lita::Authorization.remove_user_from_group(
54
- response.user,
55
- @user,
56
- @group
57
- )
58
- response.reply "#{@user.name} was removed from #{@group}."
43
+ if Lita::Authorization.remove_user_from_group(response.user, @user, @group)
44
+ response.reply t("user_removed",
45
+ user: @user.name,
46
+ group: @group
47
+ )
59
48
  else
60
- response.reply "#{@user.name} was not in #{@group}."
49
+ response.reply t("user_not_in", user: @user.name, group: @group)
61
50
  end
62
51
  end
63
52
 
@@ -67,15 +56,9 @@ HELP
67
56
  # @return [void]
68
57
  def list(response)
69
58
  requested_group = response.args[1]
70
- output = get_groups_list(requested_group)
59
+ output = get_groups_list(response.args[1])
71
60
  if output.empty?
72
- if requested_group
73
- response.reply(
74
- "There is no authorization group named #{requested_group}."
75
- )
76
- else
77
- response.reply("There are no authorization groups yet.")
78
- end
61
+ response.reply(empty_state_for_list(requested_group))
79
62
  else
80
63
  response.reply(output.join("\n"))
81
64
  end
@@ -83,6 +66,14 @@ HELP
83
66
 
84
67
  private
85
68
 
69
+ def empty_state_for_list(requested_group)
70
+ if requested_group
71
+ t("empty_state_group", group: requested_group)
72
+ else
73
+ t("empty_state")
74
+ end
75
+ end
76
+
86
77
  def get_groups_list(requested_group)
87
78
  groups_with_users = Lita::Authorization.groups_with_users
88
79
  if requested_group
@@ -95,34 +86,43 @@ HELP
95
86
  end
96
87
  end
97
88
 
98
- # Validates that incoming messages have the right format and a valid user.
99
- # Also assigns the user and group to instance variables for the main
100
- # methods to use later.
101
- def valid_message?(response)
102
- command, identifier, @group = response.args
103
-
89
+ def valid_group?(response, identifier)
104
90
  unless identifier && @group
105
- response.reply "Format: #{robot.name} auth add USER GROUP"
91
+ response.reply "#{t('format')}: #{robot.name} auth add USER GROUP"
106
92
  return
107
93
  end
108
94
 
109
95
  if @group.downcase.strip == "admins"
110
- response.reply "Administrators can only be managed via Lita config."
96
+ response.reply t("admin_management")
111
97
  return
112
98
  end
113
99
 
114
- @user = User.find_by_id(identifier)
115
- @user = User.find_by_name(identifier) unless @user
100
+ true
101
+ end
116
102
 
117
- unless @user
118
- response.reply <<-REPLY.chomp
119
- No user was found with the identifier "#{identifier}".
120
- REPLY
121
- return
122
- end
103
+ # Validates that incoming messages have the right format and a valid user.
104
+ # Also assigns the user and group to instance variables for the main
105
+ # methods to use later.
106
+ def valid_message?(response)
107
+ _command, identifier, @group = response.args
108
+
109
+ return unless valid_group?(response, identifier)
110
+
111
+ return unless valid_user?(response, identifier)
123
112
 
124
113
  true
125
114
  end
115
+
116
+ def valid_user?(response, identifier)
117
+ @user = User.fuzzy_find(identifier)
118
+
119
+ if @user
120
+ true
121
+ else
122
+ response.reply t("no_user_found", identifier: identifier)
123
+ return
124
+ end
125
+ end
126
126
  end
127
127
 
128
128
  Lita.register_handler(Authorization)
@@ -1,14 +1,11 @@
1
1
  module Lita
2
+ # A namespace to hold all subclasses of {Handler}.
2
3
  module Handlers
3
4
  # Provides online help about Lita commands for users.
4
5
  class Help < Handler
5
6
  route(/^help\s*(.+)?/, :help, command: true, help: {
6
- "help" => %{
7
- Lists help information for terms and command the robot will respond to.
8
- }.gsub(/\n/, ""),
9
- "help COMMAND" => %{
10
- Lists help information for terms or commands that begin with COMMAND.
11
- }.gsub(/\n/, "")
7
+ "help" => t("help.help_value"),
8
+ t("help.help_command_key") => t("help.help_command_value")
12
9
  })
13
10
 
14
11
  # Outputs help information about Lita commands.
@@ -31,19 +28,15 @@ Lists help information for terms or commands that begin with COMMAND.
31
28
 
32
29
  # Creates an array of help info for all registered routes.
33
30
  def build_help(response)
34
- output = []
35
-
36
- Lita.handlers.each do |handler|
37
- handler.routes.each do |route|
38
- route.help.each do |command, description|
39
- next unless authorized?(response.user, route.required_groups)
40
- command = "#{name}: #{command}" if route.command?
41
- output << "#{command} - #{description}"
31
+ Lita.handlers.map do |handler|
32
+ handler.routes.map do |route|
33
+ route.help.map do |command, description|
34
+ if authorized?(response.user, route.required_groups)
35
+ help_command(route, command, description)
36
+ end
42
37
  end
43
38
  end
44
- end
45
-
46
- output
39
+ end.flatten.compact
47
40
  end
48
41
 
49
42
  # Filters the help output by an optional command.
@@ -57,6 +50,12 @@ Lists help information for terms or commands that begin with COMMAND.
57
50
  end
58
51
  end
59
52
 
53
+ # Formats an individual command's help message.
54
+ def help_command(route, command, description)
55
+ command = "#{name}: #{command}" if route.command?
56
+ "#{command} - #{description}"
57
+ end
58
+
60
59
  # The way the bot should be addressed in order to trigger a command.
61
60
  def name
62
61
  Lita.config.robot.mention_name || Lita.config.robot.name
@@ -0,0 +1,38 @@
1
+ module Lita
2
+ # A namespace to hold all subclasses of {Handler}.
3
+ module Handlers
4
+ # Provides information about the currently running robot.
5
+ class Info < Handler
6
+ route(/^info$/i, :chat, command: true, help: {
7
+ "info" => t("help.info_value")
8
+ })
9
+
10
+ http.get "/lita/info", :web
11
+
12
+ # Replies with the current version of the Lita.
13
+ # @param response [Lita::Response] The response object.
14
+ # @return [void]
15
+ # @since 3.0.0
16
+ def chat(response)
17
+ response.reply "Lita #{Lita::VERSION} - http://www.lita.io/"
18
+ end
19
+
20
+ # Returns JSON with basic information about the robot.
21
+ # @param request [Rack::Request] The HTTP request.
22
+ # @param response [Rack::Response] The HTTP response.
23
+ # @return [void]
24
+ def web(request, response)
25
+ response.headers["Content-Type"] = "application/json"
26
+ json = MultiJson.dump(
27
+ lita_version: Lita::VERSION,
28
+ adapter: Lita.config.robot.adapter,
29
+ robot_name: robot.name,
30
+ robot_mention_name: robot.mention_name
31
+ )
32
+ response.write(json)
33
+ end
34
+ end
35
+
36
+ Lita.register_handler(Info)
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ module Lita
2
+ # A namespace to hold all subclasses of {Handler}.
3
+ module Handlers
4
+ # Allows administrators to make Lita join and part from rooms.
5
+ # @since 3.0.0
6
+ class Room < Handler
7
+ route(/^join\s+(.+)$/i, :join, command: true, restrict_to: :admins, help: {
8
+ t("help.join_key") => t("help.join_value")
9
+ })
10
+
11
+ route(/^part\s+(.+)$/i, :part, command: true, restrict_to: :admins, help: {
12
+ t("help.part_key") => t("help.part_value")
13
+ })
14
+
15
+ # Joins the room with the specified ID.
16
+ # @param response [Lita::Response] The response object.
17
+ # @return [void]
18
+ def join(response)
19
+ robot.join(response.args[0])
20
+ end
21
+
22
+ # Parts from the room with the specified ID.
23
+ # @param response [Lita::Response] The response object.
24
+ # @return [void]
25
+ def part(response)
26
+ robot.part(response.args[0])
27
+ end
28
+ end
29
+
30
+ Lita.register_handler(Room)
31
+ end
32
+ end
@@ -2,22 +2,17 @@ module Lita
2
2
  # Handlers use this class to define HTTP routes for the built-in web
3
3
  # server.
4
4
  class HTTPRoute
5
+ # An +HttpRouter::Route+ class used for dispatch.
6
+ # @since 3.0.0
7
+ ExtendedRoute = Class.new(HttpRouter::Route) do
8
+ include HttpRouter::RouteHelper
9
+ include HttpRouter::GenerationHelper
10
+ end
11
+
5
12
  # The handler registering the route.
6
13
  # @return [Lita::Handler] The handler.
7
14
  attr_reader :handler_class
8
15
 
9
- # The HTTP method for the route (GET, POST, etc.).
10
- # @return [String] The HTTP method.
11
- attr_reader :http_method
12
-
13
- # The name of the instance method in the handler to call for the route.
14
- # @return [Symbol, String] The method name.
15
- attr_reader :method_name
16
-
17
- # The URL path component that will trigger the route.
18
- # @return [String] The path.
19
- attr_reader :path
20
-
21
16
  # @param handler_class [Lita::Handler] The handler registering the route.
22
17
  def initialize(handler_class)
23
18
  @handler_class = handler_class
@@ -35,12 +30,13 @@ module Lita
35
30
  # the handler to call for the route.
36
31
  # @return [void]
37
32
  def define_http_method(http_method)
38
- define_method(http_method) do |path, method_name|
39
- route(http_method.to_s.upcase, path, method_name)
33
+ define_method(http_method) do |path, method_name, options = {}|
34
+ create_route(http_method.to_s.upcase, path, method_name, options)
40
35
  end
41
36
  end
42
37
  end
43
38
 
39
+ define_http_method :head
44
40
  define_http_method :get
45
41
  define_http_method :post
46
42
  define_http_method :put
@@ -53,12 +49,27 @@ module Lita
53
49
  private
54
50
 
55
51
  # Creates a new HTTP route.
56
- def route(http_method, path, method_name)
57
- @http_method = http_method
58
- @path = path
59
- @method_name = method_name
52
+ def create_route(http_method, path, method_name, options)
53
+ route = ExtendedRoute.new
54
+ route.path = path
55
+ route.add_match_with(options)
56
+ route.add_request_method(http_method)
57
+ route.add_request_method("HEAD") if http_method == "GET"
58
+
59
+ route.to do |env|
60
+ request = Rack::Request.new(env)
61
+ response = Rack::Response.new
62
+
63
+ if request.head?
64
+ response.status = 204
65
+ else
66
+ handler_class.new(env["lita.robot"]).public_send(method_name, request, response)
67
+ end
68
+
69
+ response.finish
70
+ end
60
71
 
61
- handler_class.http_routes << self
72
+ handler_class.http_routes << route
62
73
  end
63
74
  end
64
75
  end