lita 1.1.2 → 2.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.
@@ -1,10 +1,18 @@
1
1
  module Lita
2
+ # Adapters are the glue between Lita's API and a chat service.
2
3
  class Adapter
4
+ # The instance of {Lita::Robot}.
5
+ # @return [Lita::Robot]
3
6
  attr_reader :robot
4
7
 
5
8
  class << self
9
+ # A list of configuration keys that are required for the adapter to boot.
10
+ # @return [Array]
6
11
  attr_reader :required_configs
7
12
 
13
+ # Defines configuration keys that are requried for the adapter to boot.
14
+ # @param keys [String, Symbol] The required keys.
15
+ # @return [void]
8
16
  def require_config(*keys)
9
17
  @required_configs ||= []
10
18
  @required_configs.concat(keys.flatten.map(&:to_sym))
@@ -13,11 +21,37 @@ module Lita
13
21
  alias_method :require_configs, :require_config
14
22
  end
15
23
 
24
+ # @param robot [Lita::Robot] The currently running robot.
16
25
  def initialize(robot)
17
26
  @robot = robot
18
27
  ensure_required_configs
19
28
  end
20
29
 
30
+ # @!method run
31
+ # The main loop. Should connect to the chat service, listen for incoming
32
+ # messages, create {Lita::Message} objects from them, and dispatch them to
33
+ # the robot by calling {Lita::Robot#receive}.
34
+ # @return [void]
35
+ # @abstract This should be implemented by the adapter.
36
+
37
+ # @!method send_messages(target, strings)
38
+ # Sends one or more messages to a user or room.
39
+ # @param target [Lita::Source] The user or room to send messages to.
40
+ # @param strings [Array<String>] An array of messages to send.
41
+ # @return [void]
42
+ # @abstract This should be implemented by the adapter.
43
+
44
+ # @!method set_topic(target, topic)
45
+ # Sets the topic for a room.
46
+ # @param target [Lita::Source] The room to change the topic for.
47
+ # @param topic [String] The new topic.
48
+ # @return [void]
49
+ # @abstract This should be implemented by the adapter.
50
+
51
+ # @!method shut_down
52
+ # Performs any clean up necessary when disconnecting from the chat service.
53
+ # @return [void]
54
+ # @abstract This should be implemented by the adapter.
21
55
  [:run, :send_messages, :set_topic, :shut_down].each do |method|
22
56
  define_method(method) do |*args|
23
57
  Lita.logger.warn("This adapter has not implemented ##{method}.")
@@ -26,6 +60,7 @@ module Lita
26
60
 
27
61
  private
28
62
 
63
+ # Logs a fatal message and aborts if a required config key is not set.
29
64
  def ensure_required_configs
30
65
  required_configs = self.class.required_configs
31
66
  return if required_configs.nil?
@@ -1,8 +1,12 @@
1
1
  module Lita
2
2
  module Adapters
3
+ # An adapter that runs Lita in a UNIX shell.
3
4
  class Shell < Adapter
5
+ # Creates a "Shell User" and then loops a prompt and input, passing the
6
+ # incoming messages to the robot.
7
+ # @return [void]
4
8
  def run
5
- user = User.new(1, name: "Shell User")
9
+ user = User.create(1, name: "Shell User")
6
10
  source = Source.new(user)
7
11
  puts 'Type "exit" or "quit" to end the session.'
8
12
 
@@ -11,15 +15,22 @@ module Lita
11
15
  input = $stdin.gets.chomp.strip
12
16
  break if input == "exit" || input == "quit"
13
17
  message = Message.new(robot, input, source)
14
- Thread.new { robot.receive(message) }
18
+ message.command! if Lita.config.adapter.private_chat
19
+ robot.receive(message)
15
20
  end
16
21
  end
17
22
 
23
+ # Outputs outgoing messages to the shell.
24
+ # @param target [Lita::Source] Unused, since there is only one user in the
25
+ # shell environment.
26
+ # @param strings [Array<String>] An array of strings to output.
27
+ # @return [void]
18
28
  def send_messages(target, strings)
19
- puts
20
29
  puts strings
21
30
  end
22
31
 
32
+ # Adds a blank line for a nice looking exit.
33
+ # @return [void]
23
34
  def shut_down
24
35
  puts
25
36
  end
@@ -1,30 +1,54 @@
1
1
  module Lita
2
+ # Methods for querying and manipulating authorization groups.
2
3
  module Authorization
3
4
  class << self
5
+ # Adds a user to an authorization group.
6
+ # @param requesting_user [Lita::User] The user who sent the command.
7
+ # @param user [Lita::User] The user to add to the group.
8
+ # @param group [Symbol, String] The name of the group.
9
+ # @return [Symbol] :unauthorized if the requesting user is not authorized.
10
+ # @return [Boolean] true if the user was added. false if the user was
11
+ # already in the group.
4
12
  def add_user_to_group(requesting_user, user, group)
5
13
  return :unauthorized unless user_is_admin?(requesting_user)
6
14
  redis.sadd(normalize_group(group), user.id)
7
15
  end
8
16
 
17
+ # Removes a user from an authorization group.
18
+ # @param requesting_user [Lita::User] The user who sent the command.
19
+ # @param user [Lita::User] The user to remove from the group.
20
+ # @param group [Symbol, String] The name of the group.
21
+ # @return [Symbol] :unauthorized if the requesting user is not authorized.
22
+ # @return [Boolean] true if the user was removed. false if the user was
23
+ # not in the group.
9
24
  def remove_user_from_group(requesting_user, user, group)
10
25
  return :unauthorized unless user_is_admin?(requesting_user)
11
26
  redis.srem(normalize_group(group), user.id)
12
27
  end
13
28
 
29
+ # Checks if a user is in an authorization group.
30
+ # @param user [Lita::User] The user.
31
+ # @param group [Symbol, String] The name of the group.
32
+ # @return [Boolean] Whether or not the user is in the group.
14
33
  def user_in_group?(user, group)
15
34
  redis.sismember(normalize_group(group), user.id)
16
35
  end
17
36
 
37
+ # Checks if a user is an administrator.
38
+ # @param user [Lita::User] The user.
39
+ # @return [Boolean] Whether or not the user is an administrator.
18
40
  def user_is_admin?(user)
19
41
  Array(Lita.config.robot.admins).include?(user.id)
20
42
  end
21
43
 
22
44
  private
23
45
 
46
+ # Ensures that group names are stored consistently in Redis.
24
47
  def normalize_group(group)
25
48
  group.to_s.downcase.strip
26
49
  end
27
50
 
51
+ # A Redis::Namespace for authorization data.
28
52
  def redis
29
53
  @redis ||= Redis::Namespace.new("auth", redis: Lita.redis)
30
54
  end
@@ -1,6 +1,7 @@
1
1
  require "thor"
2
2
 
3
3
  module Lita
4
+ # The command line interface for Lita.
4
5
  class CLI < Thor
5
6
  include Thor::Actions
6
7
 
@@ -1,41 +1,74 @@
1
1
  module Lita
2
+ # An object that stores various user settings that affect Lita's behavior.
2
3
  class Config < Hash
3
- def self.default_config
4
- new.tap do |c|
5
- c.robot = new
6
- c.robot.name = "Lita"
7
- c.robot.adapter = :shell
8
- c.robot.log_level = :info
9
- c.robot.admins = nil
10
- c.redis = new
11
- c.adapter = new
12
- c.handlers = new
4
+ class << self
5
+ # Initializes a new Config object with the default settings.
6
+ # @return [Lita::Config] The default configuration.
7
+ def default_config
8
+ config = new.tap do |c|
9
+ c.robot = new
10
+ c.robot.name = "Lita"
11
+ c.robot.adapter = :shell
12
+ c.robot.log_level = :info
13
+ c.robot.admins = nil
14
+ c.redis = new
15
+ c.http = new
16
+ c.http.port = 8080
17
+ c.http.debug = false
18
+ c.adapter = new
19
+ c.handlers = new
20
+ end
21
+ load_handler_configs(config)
22
+ config
13
23
  end
14
- end
15
24
 
16
- def self.load_user_config(config_path = nil)
17
- config_path = "lita_config.rb" unless config_path
25
+ # Loads configuration from a user configuration file.
26
+ # @param config_path [String] The path to the configuration file.
27
+ # @return [void]
28
+ def load_user_config(config_path = nil)
29
+ config_path = "lita_config.rb" unless config_path
18
30
 
19
- begin
20
- load(config_path)
21
- rescue Exception => e
22
- Lita.logger.fatal <<-MSG
31
+ begin
32
+ load(config_path)
33
+ rescue Exception => e
34
+ Lita.logger.fatal <<-MSG
23
35
  Lita configuration file could not be processed. The exception was:
24
36
  #{e.message}
25
37
  #{e.backtrace.join("\n")}
26
38
  MSG
27
- abort
28
- end if File.exist?(config_path)
39
+ abort
40
+ end if File.exist?(config_path)
41
+ end
42
+
43
+ private
44
+
45
+ # Adds and populates a Config object to Lita.config.handlers for every
46
+ # registered handler that implements self.default_config.
47
+ def load_handler_configs(config)
48
+ Lita.handlers.each do |handler|
49
+ next unless handler.respond_to?(:default_config)
50
+ handler_config = config.handlers[handler.namespace] = new
51
+ handler.default_config(handler_config)
52
+ end
53
+ end
29
54
  end
30
55
 
56
+ # Sets a config key.
57
+ # @param key [Symbol, String] The key.
58
+ # @param value The value.
59
+ # @return The value.
31
60
  def []=(key, value)
32
61
  super(key.to_sym, value)
33
62
  end
34
63
 
64
+ # Get a config key.
65
+ # @param key [Symbol, String] The key.
66
+ # @return The value.
35
67
  def [](key)
36
68
  super(key.to_sym)
37
69
  end
38
70
 
71
+ # Allows keys to be read and written with struct-like syntax.
39
72
  def method_missing(name, *args)
40
73
  name_string = name.to_s
41
74
  if name_string.chomp!("=")
@@ -1,77 +1,141 @@
1
1
  module Lita
2
+ # Base class for objects that add new behavior to Lita.
2
3
  class Handler
3
4
  extend Forwardable
4
5
 
5
- attr_reader :redis, :robot
6
- private :redis
7
-
8
- def_delegators :@message, :args, :command?, :scan, :user
9
-
10
- class Route < Struct.new(:pattern, :method_name, :command, :required_groups)
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
+ )
11
22
  alias_method :command?, :command
12
23
  end
13
24
 
14
25
  class << self
15
- def route(pattern, to: nil, command: false, restrict_to: nil)
26
+ # Creates a chat route.
27
+ # @param pattern [Regexp] A regular expression to match incoming messages
28
+ # against.
29
+ # @param method [Symbol, String] The name of the method to trigger.
30
+ # @param command [Boolean] Whether or not the message must be directed at
31
+ # the robot.
32
+ # @param restrict_to [Array<Symbol, String>, nil] A list of authorization
33
+ # groups the user must be in to trigger the route.
34
+ # @param help [Hash] A map of example invocations to descriptions.
35
+ # @return [void]
36
+ def route(pattern, method, command: false, restrict_to: nil, help: {})
37
+ groups = restrict_to.nil? ? nil : Array(restrict_to)
38
+ routes << Route.new(pattern, method, command, groups, help)
39
+ end
40
+
41
+ # A list of chat routes defined by the handler.
42
+ # @return [Array<Lita::Handler::Route>]
43
+ def routes
16
44
  @routes ||= []
17
- required_groups = restrict_to.nil? ? nil : Array(restrict_to)
18
- @routes << Route.new(pattern, to, command, required_groups)
19
45
  end
20
46
 
47
+ # The main entry point for the handler at runtime. Checks if the message
48
+ # matches any of the routes and invokes the route's method if it does.
49
+ # Called by {Lita::Robot#receive}.
50
+ # @param robot [Lita::Robot] The currently running robot.
51
+ # @param message [Lita::Message] The incoming message.
52
+ # @return [void]
21
53
  def dispatch(robot, message)
22
- instance = new(robot, message)
23
-
24
- @routes.each do |route|
25
- if route_applies?(route, instance)
26
- instance.public_send(
27
- route.method_name,
28
- matches_for_route(route, instance)
29
- )
54
+ routes.each do |route|
55
+ if route_applies?(route, message)
56
+ Lita.logger.debug <<-LOG.chomp
57
+ Dispatching message to #{self}##{route.method_name}.
58
+ LOG
59
+ new(robot).public_send(route.method_name, Response.new(
60
+ message,
61
+ matches: message.match(route.pattern)
62
+ ))
30
63
  end
31
- end if defined?(@routes)
64
+ end
65
+ end
66
+
67
+ # Creates a new {Lita::HTTPRoute} which is used to define an HTTP route
68
+ # for the built-in web server.
69
+ # @see Lita::HTTPRoute
70
+ # @return [Lita::HTTPRoute] The new {Lita::HTTPRoute}.
71
+ def http
72
+ HTTPRoute.new(self)
73
+ end
74
+
75
+ # An array of all HTTP routes defined for the handler.
76
+ # @return [Array<Lita::HTTPRoute>] The array of routes.
77
+ def http_routes
78
+ @http_routes ||= []
79
+ end
80
+
81
+ # The namespace for the handler, used for its configuration object and
82
+ # Redis store. If the handler is an anonymous class, it must explicitly
83
+ # define +self.name+.
84
+ # @return [String] The handler's namespace.
85
+ # @raise [RuntimeError] If +self.name+ is not defined.
86
+ def namespace
87
+ if name
88
+ Util.underscore(name.split("::").last)
89
+ else
90
+ raise "Handlers that are anonymous classes must define self.name."
91
+ end
32
92
  end
33
93
 
34
94
  private
35
95
 
36
- def route_applies?(route, instance)
96
+ # Determines whether or not an incoming messages should trigger a route.
97
+ def route_applies?(route, message)
37
98
  # Message must match the pattern
38
- return unless route.pattern === instance.message_body
99
+ return unless route.pattern === message.body
39
100
 
40
101
  # Message must be a command if the route requires a command
41
- return if route.command? && !instance.command?
102
+ return if route.command? && !message.command?
42
103
 
43
104
  # User must be in auth group if route is restricted
44
105
  return if route.required_groups && route.required_groups.none? do |group|
45
- Authorization.user_in_group?(instance.user, group)
106
+ Authorization.user_in_group?(message.user, group)
46
107
  end
47
108
 
48
109
  true
49
110
  end
50
-
51
- def matches_for_route(route, instance)
52
- instance.scan(route.pattern)
53
- end
54
111
  end
55
112
 
56
- def initialize(robot, message)
113
+ # @param robot [Lita::Robot] The currently running robot.
114
+ def initialize(robot)
57
115
  @robot = robot
58
- @message = message
59
116
  @redis = Redis::Namespace.new(redis_namespace, redis: Lita.redis)
60
117
  end
61
118
 
62
- def reply(*strings)
63
- @robot.send_messages(@message.source, *strings)
64
- end
65
-
66
- def message_body
67
- @message.body
119
+ # Creates a new +Faraday::Connection+ for making HTTP requests.
120
+ # @param options [Hash] A set of options passed on to Faraday.
121
+ # @yield [builder] A Faraday builder object for adding middleware.
122
+ # @return [Faraday::Connection] The new connection object.
123
+ def http(options = {}, &block)
124
+ options = default_faraday_options.merge(options)
125
+ Faraday::Connection.new(nil, options, &block)
68
126
  end
69
127
 
70
128
  private
71
129
 
130
+ # Default options for new Faraday connections. Sets the user agent to the
131
+ # current version of Lita.
132
+ def default_faraday_options
133
+ { headers: { "User-Agent" => "Lita v#{VERSION}" } }
134
+ end
135
+
136
+ # The handler's namespace for Redis.
72
137
  def redis_namespace
73
- name = self.class.name.split("::").last.downcase
74
- "handlers:#{name}"
138
+ "handlers:#{self.class.namespace}"
75
139
  end
76
140
  end
77
141
  end
@@ -1,51 +1,60 @@
1
1
  module Lita
2
2
  module Handlers
3
+ # Provides a chat interface for administering authorization groups.
3
4
  class Authorization < Handler
4
- route(/^auth\s+add/, to: :add, command: true)
5
- route(/^auth\s+remove/, to: :remove, command: true)
5
+ route(/^auth\s+add/, :add, command: true, help: {
6
+ "auth add USER GROUP" => "Add USER to authorization group GROUP. Requires admin privileges."
7
+ })
8
+ route(/^auth\s+remove/, :remove, command: true, help: {
9
+ "auth remove USER GROUP" => "Remove USER from authorization group GROUP. Requires admin privileges."
10
+ })
6
11
 
7
- def self.help
8
- robot_name = Lita.config.robot.name
12
+ # Adds a user to an authorization group.
13
+ # @param response [Lita::Response] The response object.
14
+ # @return [void]
15
+ def add(response)
16
+ return unless valid_message?(response)
9
17
 
10
- {
11
- "#{robot_name}: auth add USER GROUP" => "Add USER to authorization group GROUP. Requires admin privileges.",
12
- "#{robot_name}: auth remove USER GROUP" => "Remove USER from authorization group GROUP. Requires admin privileges."
13
- }
14
- end
15
-
16
- def add(matches)
17
- return unless valid_message?
18
-
19
- case Lita::Authorization.add_user_to_group(user, @user, @group)
18
+ case Lita::Authorization.add_user_to_group(response.user, @user, @group)
20
19
  when :unauthorized
21
- reply "Only administrators can add users to groups."
20
+ response.reply "Only administrators can add users to groups."
22
21
  when true
23
- reply "#{@user.name} was added to #{@group}."
22
+ response.reply "#{@user.name} was added to #{@group}."
24
23
  else
25
- reply "#{@user.name} was already in #{@group}."
24
+ response.reply "#{@user.name} was already in #{@group}."
26
25
  end
27
26
  end
28
27
 
29
- def remove(matches)
30
- return unless valid_message?
28
+ # Removes a user from an authorization group.
29
+ # @param response [Lita::Response] The response object.
30
+ # @return [void]
31
+ def remove(response)
32
+ return unless valid_message?(response)
31
33
 
32
- case Lita::Authorization.remove_user_from_group(user, @user, @group)
34
+ case Lita::Authorization.remove_user_from_group(
35
+ response.user,
36
+ @user,
37
+ @group
38
+ )
33
39
  when :unauthorized
34
- reply "Only administrators can remove users from groups."
40
+ response.reply "Only administrators can remove users from groups."
35
41
  when true
36
- reply "#{@user.name} was removed from #{@group}."
42
+ response.reply "#{@user.name} was removed from #{@group}."
37
43
  else
38
- reply "#{@user.name} was not in #{@group}."
44
+ response.reply "#{@user.name} was not in #{@group}."
39
45
  end
40
46
  end
41
47
 
42
48
  private
43
49
 
44
- def valid_message?
45
- command, identifier, @group = args
50
+ # Validates that incoming messages have the right format and a valid user.
51
+ # Also assigns the user and group to instance variables for the main
52
+ # methods to use later.
53
+ def valid_message?(response)
54
+ command, identifier, @group = response.args
46
55
 
47
56
  unless identifier && @group
48
- reply "Format: #{robot.name} auth add USER GROUP"
57
+ response.reply "Format: #{robot.name} auth add USER GROUP"
49
58
  return
50
59
  end
51
60
 
@@ -53,7 +62,9 @@ module Lita
53
62
  @user = User.find_by_name(identifier) unless @user
54
63
 
55
64
  unless @user
56
- reply %{No user was found with the identifier "#{identifier}".}
65
+ response.reply <<-REPLY.chomp
66
+ No user was found with the identifier "#{identifier}".
67
+ REPLY
57
68
  return
58
69
  end
59
70