lita 1.1.2 → 2.0.0

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