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