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,37 +1,40 @@
1
1
  module Lita
2
2
  module Handlers
3
+ # Provides online help about Lita commands for users.
3
4
  class Help < Handler
4
- route(/^help\s*(.+)?/, to: :help, command: true)
5
+ route(/^help\s*(.+)?/, :help, command: true, help: {
6
+ "help" => "Lists help information for terms and command the robot will respond to.",
7
+ "help COMMAND" => "Lists help information for terms or commands that begin with COMMAND."
8
+ })
5
9
 
6
- def self.help
7
- robot_name = Lita.config.robot.name
8
-
9
- {
10
- "#{robot_name}: help" => "Lists help information for terms and commands #{robot_name} will respond to.",
11
- "#{robot_name}: help COMMAND" => "Lists help information for terms and commands starting with COMMAND."
12
- }
13
- end
14
-
15
- def help(matches)
16
- commands = {}
10
+ # Outputs help information about Lita commands.
11
+ # @param response [Lita::Response] The response object.
12
+ # @return [void]
13
+ def help(response)
14
+ output = []
17
15
 
18
16
  Lita.handlers.each do |handler|
19
- commands.merge!(handler.help) if handler.respond_to?(:help)
17
+ handler.routes.each do |route|
18
+ route.help.each do |command, description|
19
+ command = "#{name}: #{command}" if route.command?
20
+ output << "#{command} - #{description}"
21
+ end
22
+ end
20
23
  end
21
24
 
22
- filter = matches[0][0]
25
+ filter = response.matches[0][0]
23
26
  if filter
24
- robot_name = Lita.config.robot.name
25
- commands.select! do |key, value|
26
- /^#{filter}/i === key.sub(/^\s*@?#{robot_name}[:,]?\s*/, "")
27
- end
27
+ output.select! { |line| /(?:@?#{name}[:,]?)?#{filter}/i === line }
28
28
  end
29
29
 
30
- message = commands.map do |command, description|
31
- "#{command} - #{description}"
32
- end.join("\n")
30
+ response.reply output.join("\n")
31
+ end
32
+
33
+ private
33
34
 
34
- reply message
35
+ # The way the bot should be addressed in order to trigger a command.
36
+ def name
37
+ Lita.config.robot.mention_name || Lita.config.robot.name
35
38
  end
36
39
  end
37
40
 
@@ -0,0 +1,25 @@
1
+ module Lita
2
+ module Handlers
3
+ # Provides an HTTP route with basic information about the running robot.
4
+ class Web < Handler
5
+ http.get "/lita/info", :info
6
+
7
+ # Returns JSON with basic information about the robot.
8
+ # @param request [Rack::Request] The HTTP request.
9
+ # @param response [Rack::Response] The HTTP response.
10
+ # @return [void]
11
+ def info(request, response)
12
+ response.headers["Content-Type"] = "application/json"
13
+ json = MultiJson.dump(
14
+ lita_version: Lita::VERSION,
15
+ adapter: Lita.config.robot.adapter,
16
+ robot_name: robot.name,
17
+ robot_mention_name: robot.mention_name
18
+ )
19
+ response.write(json)
20
+ end
21
+ end
22
+
23
+ Lita.register_handler(Web)
24
+ end
25
+ end
@@ -0,0 +1,64 @@
1
+ module Lita
2
+ # Handlers use this class to define HTTP routes for the built-in web
3
+ # server.
4
+ class HTTPRoute
5
+ # The handler registering the route.
6
+ # @return [Lita::Handler] The handler.
7
+ attr_reader :handler_class
8
+
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
+ # @param handler_class [Lita::Handler] The handler registering the route.
22
+ def initialize(handler_class)
23
+ @handler_class = handler_class
24
+ end
25
+
26
+ class << self
27
+ private
28
+
29
+ # @!macro define_http_method
30
+ # @method $1(path, method_name)
31
+ # Defines a new route with the "$1" HTTP method.
32
+ # @param path [String] The URL path component that will trigger the
33
+ # route.
34
+ # @param method_name [Symbol, String] The name of the instance method in
35
+ # the handler to call for the route.
36
+ # @return [void]
37
+ 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)
40
+ end
41
+ end
42
+ end
43
+
44
+ define_http_method :get
45
+ define_http_method :post
46
+ define_http_method :put
47
+ define_http_method :patch
48
+ define_http_method :delete
49
+ define_http_method :options
50
+ define_http_method :link
51
+ define_http_method :unlink
52
+
53
+ private
54
+
55
+ # 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
60
+
61
+ handler_class.http_routes << self
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
1
+ module Lita
2
+ # Creates a Logger with the proper configuration.
3
+ module Logger
4
+ class << self
5
+ # Creates a new {::Logger} outputting to standard error with the given
6
+ # severity level and a custom format.
7
+ # @param level [Symbol, String] The name of the log level to use.
8
+ # @return [::Logger] The {::Logger} object.
9
+ def get_logger(level)
10
+ logger = ::Logger.new(STDERR)
11
+ logger.level = get_level_constant(level)
12
+ logger.formatter = proc do |severity, datetime, progname, msg|
13
+ "[#{datetime.utc}] #{severity}: #{msg}\n"
14
+ end
15
+ logger
16
+ end
17
+
18
+ private
19
+
20
+ # Gets the Logger constant for the given severity level.
21
+ def get_level_constant(level)
22
+ if level
23
+ begin
24
+ ::Logger.const_get(level.to_s.upcase)
25
+ rescue NameError
26
+ return ::Logger::INFO
27
+ end
28
+ else
29
+ ::Logger::INFO
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,38 +1,72 @@
1
1
  module Lita
2
+ # Represents an incoming chat message.
2
3
  class Message
3
4
  extend Forwardable
4
5
 
5
- attr_reader :body, :source
6
- alias_method :message, :body
6
+ # The body of the message.
7
+ # @return [String] The message body.
8
+ attr_reader :body
7
9
 
8
- def_delegators :@body, :scan
9
- def_delegators :@source, :user
10
+ # The source of the message, which is a user and optional room.
11
+ # @return [Lita::Source] The message source.
12
+ attr_reader :source
10
13
 
14
+ # @!method user
15
+ # The user who sent the message.
16
+ # @return [Lita::User] The user.
17
+ # @see Lita::Source#user
18
+ def_delegators :source, :user
19
+
20
+ # @param robot [Lita::Robot] The currently running robot.
21
+ # @param body [String] The body of the message.
22
+ # @param source [Lita::Source] The source of the message.
11
23
  def initialize(robot, body, source)
12
24
  @robot = robot
13
25
  @body = body
14
26
  @source = source
15
27
 
16
- @command = !!@body.sub!(/^\s*@?#{@robot.mention_name}[:,]?\s*/, "")
28
+ @command = !!@body.sub!(/^\s*@?#{@robot.mention_name}[:,]?\s*/i, "")
17
29
  end
18
30
 
31
+ # An array of arguments created by shellsplitting the message body, as if
32
+ # it were a shell command.
33
+ # @return [Array<String>] The array of arguments.
19
34
  def args
20
35
  begin
21
- command, *args = message.shellsplit
36
+ command, *args = body.shellsplit
22
37
  rescue ArgumentError
23
38
  command, *args =
24
- message.split(/\s+/).map(&:shellescape).join(" ").shellsplit
39
+ body.split(/\s+/).map(&:shellescape).join(" ").shellsplit
25
40
  end
26
41
 
27
42
  args
28
43
  end
29
44
 
45
+ # Marks the message as a command, meaning it was directed at the robot
46
+ # specifically.
47
+ # @return [void]
30
48
  def command!
31
49
  @command = true
32
50
  end
33
51
 
52
+ # A boolean representing whether or not the message was a command.
53
+ # @return [Boolean] +true+ if the message was a command, +false+ if not.
34
54
  def command?
35
55
  @command
36
56
  end
57
+
58
+ # An array of matches against the message body for the given {::Regexp}.
59
+ # @param pattern [Regexp] A pattern to match.
60
+ # @return [Array<String>, Array<Array<String>>] An array of matches.
61
+ def match(pattern)
62
+ body.scan(pattern)
63
+ end
64
+
65
+ # Replies by sending the given strings back to the source of the message.
66
+ # @param strings [String, Array<String>] The strings to send back.
67
+ # @return [void]
68
+ def reply(*strings)
69
+ @robot.send_messages(source, *strings)
70
+ end
37
71
  end
38
72
  end
@@ -0,0 +1,110 @@
1
+ module Lita
2
+ # Creates a +Rack+ application from all the routes registered by handlers.
3
+ class RackAppBuilder
4
+ # The character that separates the pieces of a URL's path component.
5
+ PATH_SEPARATOR = "/"
6
+
7
+ # A +Struct+ representing a route's destination handler and method name.
8
+ RouteMapping = Struct.new(:handler, :method_name)
9
+
10
+ # All registered paths. Used to respond to HEAD requests.
11
+ # @return [Array<String>] The array of paths.
12
+ attr_reader :all_paths
13
+
14
+ # The currently running robot.
15
+ # @return [Lita::Robot] The robot.
16
+ attr_reader :robot
17
+
18
+ # A hash mapping HTTP request methods and paths to handlers and methods.
19
+ # @return [Hash] The mapping.
20
+ attr_reader :routes
21
+
22
+ # @param robot [Lita::Robot] The currently running robot.
23
+ def initialize(robot)
24
+ @robot = robot
25
+ @routes = Hash.new { |h, k| h[k] = {} }
26
+ compile
27
+ end
28
+
29
+ # Creates a +Rack+ application from the compiled routes.
30
+ # @return [Rack::Builder] The +Rack+ application.
31
+ def to_app
32
+ app = Rack::Builder.new
33
+ app.run(app_proc)
34
+ app
35
+ end
36
+
37
+ private
38
+
39
+ # The proc that serves as the +Rack+ application.
40
+ def app_proc
41
+ -> (env) do
42
+ request = Rack::Request.new(env)
43
+
44
+ mapping = routes[request.request_method][request.path]
45
+
46
+ if mapping
47
+ Lita.logger.info <<-LOG.chomp
48
+ Routing HTTP #{request.request_method} #{request.path} to \
49
+ #{mapping.handler}##{mapping.method_name}.
50
+ LOG
51
+ response = Rack::Response.new
52
+ instance = mapping.handler.new(robot)
53
+ instance.public_send(mapping.method_name, request, response)
54
+ response.finish
55
+ elsif request.head? && all_paths.include?(request.path)
56
+ Lita.logger.info "HTTP HEAD #{request.path} was a 204."
57
+ [204, {}, []]
58
+ else
59
+ Lita.logger.info <<-LOG.chomp
60
+ HTTP #{request.request_method} #{request.path} was a 404.
61
+ LOG
62
+ [404, {}, ["Route not found."]]
63
+ end
64
+ end
65
+ end
66
+
67
+ # Collect all registered paths. Used for responding to HEAD requests.
68
+ def collect_paths
69
+ @all_paths = routes.values.map { |hash| hash.keys.first }.uniq
70
+ end
71
+
72
+ # Registers routes in the route mapping for each handler's defined routes.
73
+ def compile
74
+ Lita.handlers.each do |handler|
75
+ handler.http_routes.each { |route| register_route(handler, route) }
76
+ end
77
+ collect_paths
78
+ end
79
+
80
+ # Registers a route.
81
+ def register_route(handler, route)
82
+ cleaned_path = clean_path(route.path)
83
+
84
+ if @routes[route.http_method][cleaned_path]
85
+ Lita.logger.fatal <<-ERR.chomp
86
+ #{handler.name} attempted to register an HTTP route that was already \
87
+ registered: #{route.http_method} "#{cleaned_path}"
88
+ ERR
89
+ abort
90
+ end
91
+
92
+ Lita.logger.debug <<-LOG.chomp
93
+ Registering HTTP route: #{route.http_method} #{cleaned_path} to \
94
+ #{handler}##{route.method_name}.
95
+ LOG
96
+ @routes[route.http_method][cleaned_path] = RouteMapping.new(
97
+ handler,
98
+ route.method_name
99
+ )
100
+ end
101
+
102
+ # Ensures that paths begin with one slash and do not end with one.
103
+ def clean_path(path)
104
+ path.strip!
105
+ path.chop! while path.end_with?(PATH_SEPARATOR)
106
+ path = path[1..-1] while path.start_with?(PATH_SEPARATOR)
107
+ "/#{path}"
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,30 @@
1
+ module Lita
2
+ # A wrapper object that provides the primary interface for handlers to
3
+ # respond to incoming chat messages.
4
+ class Response
5
+ extend Forwardable
6
+
7
+ # The incoming message.
8
+ # @return [Lita::Message] The message.
9
+ attr_accessor :message
10
+
11
+ # An array of matches from running the message against the route pattern.
12
+ # @return [Array<String>, Array<Array<String>>] The array of matches.
13
+ attr_accessor :matches
14
+
15
+ # @!method args
16
+ # @see Lita::Message#args
17
+ # @!method reply
18
+ # @see Lita::Message#reply
19
+ # @!method user
20
+ # @see Lita::Message#user
21
+ def_delegators :message, :args, :reply, :user
22
+
23
+ # @param message [Lita::Message] The incoming message.
24
+ # @param matches [Array<String>, Array<Array<String>>] The Regexp matches.
25
+ def initialize(message, matches: nil)
26
+ self.message = message
27
+ self.matches = matches
28
+ end
29
+ end
30
+ end
@@ -1,39 +1,79 @@
1
1
  module Lita
2
+ # The main object representing a running instance of Lita. Provides a high
3
+ # level API for the underlying adapter. Dispatches incoming messages to
4
+ # registered handlers. Can send outgoing chat messages and set the topic
5
+ # of chat rooms.
2
6
  class Robot
3
- attr_reader :name
7
+ # A +Rack+ application used for the built-in web server.
8
+ # @return [Rack::Builder] The +Rack+ app.
9
+ attr_reader :app
10
+
11
+ # The name the robot will look for in incoming messages to determine if it's
12
+ # being addressed.
13
+ # @return [String] The mention name.
4
14
  attr_accessor :mention_name
5
15
 
16
+ # The name of the robot as it will appear in the chat.
17
+ # @return [String] The robot's name.
18
+ attr_reader :name
19
+
6
20
  def initialize
7
21
  @name = Lita.config.robot.name
8
22
  @mention_name = Lita.config.robot.mention_name || @name
23
+ @app = RackAppBuilder.new(self).to_app
9
24
  load_adapter
10
25
  end
11
26
 
27
+ # The primary entry point from the adapter for an incoming message.
28
+ # Dispatches the message to all registered handlers.
29
+ # @param message [Lita::Message] The incoming message.
30
+ # @return [void]
12
31
  def receive(message)
13
32
  Lita.handlers.each { |handler| handler.dispatch(self, message) }
14
33
  end
15
34
 
35
+ # Starts the robot, booting the web server and delegating to the adapter to
36
+ # connect to the chat service.
37
+ # @return [void]
16
38
  def run
39
+ run_app
17
40
  @adapter.run
18
41
  rescue Interrupt
19
42
  shut_down
20
43
  end
21
44
 
45
+ # Sends one or more messages to a user or room.
46
+ # @param target [Lita::Source] The user or room to send to. If the Source
47
+ # has a room, it will choose the room. Otherwise, it will send to the
48
+ # user.
49
+ # @param strings [String, Array<String>] One or more strings to send.
50
+ # @return [void]
22
51
  def send_messages(target, *strings)
23
52
  @adapter.send_messages(target, strings.flatten)
24
53
  end
25
54
  alias_method :send_message, :send_messages
26
55
 
56
+ # Sets the topic for a chat room.
57
+ # @param target [Lita::Source] A source object specifying the room.
58
+ # @param topic [String] The new topic message to set.
59
+ # @return [void]
27
60
  def set_topic(target, topic)
28
61
  @adapter.set_topic(target, topic)
29
62
  end
30
63
 
64
+ # Gracefully shuts the robot down, stopping the web server and delegating
65
+ # to the adapter to perform any shut down tasks necessary for the chat
66
+ # service.
67
+ # @return [void]
31
68
  def shut_down
69
+ @server.stop if @server
70
+ @server_thread.join if @server_thread
32
71
  @adapter.shut_down
33
72
  end
34
73
 
35
74
  private
36
75
 
76
+ # Loads the selected adapter.
37
77
  def load_adapter
38
78
  adapter_name = Lita.config.robot.adapter
39
79
  adapter_class = Lita.adapters[adapter_name.to_sym]
@@ -45,5 +85,20 @@ module Lita
45
85
 
46
86
  @adapter = adapter_class.new(self)
47
87
  end
88
+
89
+ # Starts the web server.
90
+ def run_app
91
+ @server_thread = Thread.new do
92
+ @server = Thin::Server.new(
93
+ app,
94
+ Lita.config.http.port.to_i,
95
+ signals: false
96
+ )
97
+ @server.silent = true unless Lita.config.http.debug
98
+ @server.start
99
+ end
100
+
101
+ @server_thread.abort_on_exception = true
102
+ end
48
103
  end
49
104
  end