lita 1.1.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +202 -45
- data/lib/lita.rb +45 -24
- data/lib/lita/adapter.rb +35 -0
- data/lib/lita/adapters/shell.rb +14 -3
- data/lib/lita/authorization.rb +24 -0
- data/lib/lita/cli.rb +1 -0
- data/lib/lita/config.rb +52 -19
- data/lib/lita/handler.rb +100 -36
- data/lib/lita/handlers/authorization.rb +38 -27
- data/lib/lita/handlers/help.rb +25 -22
- data/lib/lita/handlers/web.rb +25 -0
- data/lib/lita/http_route.rb +64 -0
- data/lib/lita/logger.rb +34 -0
- data/lib/lita/message.rb +41 -7
- data/lib/lita/rack_app_builder.rb +110 -0
- data/lib/lita/response.rb +30 -0
- data/lib/lita/robot.rb +56 -1
- data/lib/lita/rspec.rb +23 -50
- data/lib/lita/rspec/handler.rb +168 -0
- data/lib/lita/source.rb +19 -1
- data/lib/lita/user.rb +37 -1
- data/lib/lita/util.rb +26 -0
- data/lib/lita/version.rb +2 -1
- data/lita.gemspec +5 -0
- data/spec/lita/adapters/shell_spec.rb +12 -4
- data/spec/lita/config_spec.rb +18 -2
- data/spec/lita/handler_spec.rb +43 -46
- data/spec/lita/handlers/authorization_spec.rb +24 -31
- data/spec/lita/handlers/help_spec.rb +10 -8
- data/spec/lita/handlers/web_spec.rb +19 -0
- data/spec/lita/logger_spec.rb +26 -0
- data/spec/lita/message_spec.rb +16 -11
- data/spec/lita/robot_spec.rb +25 -2
- data/spec/lita/rspec_spec.rb +32 -20
- data/spec/lita/util_spec.rb +9 -0
- data/spec/lita_spec.rb +0 -51
- data/spec/spec_helper.rb +2 -1
- metadata +85 -3
- data/CHANGELOG.md +0 -21
data/lib/lita/handlers/help.rb
CHANGED
@@ -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*(.+)?/,
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
30
|
+
response.reply output.join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
33
34
|
|
34
|
-
|
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
|
data/lib/lita/logger.rb
ADDED
@@ -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
|
data/lib/lita/message.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
6
|
+
# The body of the message.
|
7
|
+
# @return [String] The message body.
|
8
|
+
attr_reader :body
|
7
9
|
|
8
|
-
|
9
|
-
|
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 =
|
36
|
+
command, *args = body.shellsplit
|
22
37
|
rescue ArgumentError
|
23
38
|
command, *args =
|
24
|
-
|
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
|
data/lib/lita/robot.rb
CHANGED
@@ -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
|
-
|
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
|