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,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