lita 2.7.2 → 3.0.0

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