lita 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +179 -10
- data/lib/lita.rb +37 -5
- data/lib/lita/adapter.rb +8 -1
- data/lib/lita/adapters/shell.rb +12 -5
- data/lib/lita/authorization.rb +29 -0
- data/lib/lita/cli.rb +7 -1
- data/lib/lita/config.rb +11 -4
- data/lib/lita/handler.rb +17 -13
- data/lib/lita/handlers/authorization.rb +60 -0
- data/lib/lita/handlers/help.rb +40 -0
- data/lib/lita/message.rb +1 -0
- data/lib/lita/robot.rb +12 -1
- data/lib/lita/rspec.rb +18 -2
- data/lib/lita/user.rb +54 -0
- data/lib/lita/version.rb +1 -1
- data/lita.gemspec +2 -0
- data/skeleton/Gemfile +5 -0
- data/skeleton/lita_config.rb +10 -0
- data/spec/lita/adapter_spec.rb +11 -3
- data/spec/lita/adapters/shell_spec.rb +10 -1
- data/spec/lita/authorization_spec.rb +60 -0
- data/spec/lita/config_spec.rb +2 -3
- data/spec/lita/handler_spec.rb +21 -0
- data/spec/lita/handlers/authorization_spec.rb +70 -0
- data/spec/lita/handlers/help_spec.rb +19 -0
- data/spec/lita/message_spec.rb +7 -0
- data/spec/lita/robot_spec.rb +33 -5
- data/spec/lita/rspec_spec.rb +27 -1
- data/spec/lita/user_spec.rb +58 -0
- data/spec/lita_spec.rb +39 -9
- metadata +16 -4
- data/lib/lita/errors.rb +0 -6
data/lib/lita/handler.rb
CHANGED
@@ -2,19 +2,20 @@ module Lita
|
|
2
2
|
class Handler
|
3
3
|
extend Forwardable
|
4
4
|
|
5
|
-
attr_reader :redis
|
5
|
+
attr_reader :redis, :robot
|
6
6
|
private :redis
|
7
7
|
|
8
|
-
def_delegators :@message, :args, :command?, :scan
|
8
|
+
def_delegators :@message, :args, :command?, :scan, :user
|
9
9
|
|
10
|
-
class Route < Struct.new(:pattern, :method_name, :command)
|
10
|
+
class Route < Struct.new(:pattern, :method_name, :command, :required_groups)
|
11
11
|
alias_method :command?, :command
|
12
12
|
end
|
13
13
|
|
14
14
|
class << self
|
15
|
-
def route(pattern, to: nil, command: false)
|
15
|
+
def route(pattern, to: nil, command: false, restrict_to: nil)
|
16
16
|
@routes ||= []
|
17
|
-
|
17
|
+
required_groups = restrict_to.nil? ? nil : Array(restrict_to)
|
18
|
+
@routes << Route.new(pattern, to, command, required_groups)
|
18
19
|
end
|
19
20
|
|
20
21
|
def dispatch(robot, message)
|
@@ -27,21 +28,24 @@ module Lita
|
|
27
28
|
matches_for_route(route, instance)
|
28
29
|
)
|
29
30
|
end
|
30
|
-
end
|
31
|
+
end if defined?(@routes)
|
31
32
|
end
|
32
33
|
|
33
34
|
private
|
34
35
|
|
35
36
|
def route_applies?(route, instance)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
37
|
+
# Message must match the pattern
|
38
|
+
return unless route.pattern === instance.message_body
|
39
|
+
|
40
|
+
# Message must be a command if the route requires a command
|
41
|
+
return if route.command? && !instance.command?
|
42
|
+
|
43
|
+
# User must be in auth group if route is restricted
|
44
|
+
return if route.required_groups && route.required_groups.none? do |group|
|
45
|
+
Authorization.user_in_group?(instance.user, group)
|
42
46
|
end
|
43
47
|
|
44
|
-
|
48
|
+
true
|
45
49
|
end
|
46
50
|
|
47
51
|
def matches_for_route(route, instance)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Lita
|
2
|
+
module Handlers
|
3
|
+
class Authorization < Handler
|
4
|
+
route(/^auth\s+add/, to: :add, command: true)
|
5
|
+
route(/^auth\s+remove/, to: :remove, command: true)
|
6
|
+
|
7
|
+
def self.help
|
8
|
+
robot_name = Lita.config.robot.name
|
9
|
+
|
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
|
+
if Lita::Authorization.add_user_to_group(@user, @group)
|
20
|
+
reply "#{@user.name} was added to #{@group}."
|
21
|
+
else
|
22
|
+
reply "#{@user.name} was already in #{@group}."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove(matches)
|
27
|
+
return unless valid_message?
|
28
|
+
|
29
|
+
if Lita::Authorization.remove_user_from_group(@user, @group)
|
30
|
+
reply "#{@user.name} was removed from #{@group}."
|
31
|
+
else
|
32
|
+
reply "#{@user.name} was not in #{@group}."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def valid_message?
|
39
|
+
command, identifier, @group = args
|
40
|
+
|
41
|
+
unless identifier && @group
|
42
|
+
reply "Format: #{robot.name} auth add USER GROUP"
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
@user = User.find_by_id(identifier)
|
47
|
+
@user = User.find_by_name(identifier) unless @user
|
48
|
+
|
49
|
+
unless @user
|
50
|
+
reply %{No user was found with the identifier "#{identifier}".}
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Lita.register_handler(Authorization)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Lita
|
2
|
+
module Handlers
|
3
|
+
class Help < Handler
|
4
|
+
route(/^help\s*(.+)?/, to: :help, command: true)
|
5
|
+
|
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 = {}
|
17
|
+
|
18
|
+
Lita.handlers.each do |handler|
|
19
|
+
commands.merge!(handler.help) if handler.respond_to?(:help)
|
20
|
+
end
|
21
|
+
|
22
|
+
filter = matches[0][0]
|
23
|
+
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
|
28
|
+
end
|
29
|
+
|
30
|
+
message = commands.map do |command, description|
|
31
|
+
"#{command} - #{description}"
|
32
|
+
end.join("\n")
|
33
|
+
|
34
|
+
reply message
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Lita.register_handler(Help)
|
39
|
+
end
|
40
|
+
end
|
data/lib/lita/message.rb
CHANGED
data/lib/lita/robot.rb
CHANGED
@@ -13,6 +13,8 @@ module Lita
|
|
13
13
|
|
14
14
|
def run
|
15
15
|
@adapter.run
|
16
|
+
rescue Interrupt
|
17
|
+
shut_down
|
16
18
|
end
|
17
19
|
|
18
20
|
def send_messages(target, *strings)
|
@@ -20,6 +22,14 @@ module Lita
|
|
20
22
|
end
|
21
23
|
alias_method :send_message, :send_messages
|
22
24
|
|
25
|
+
def set_topic(target, topic)
|
26
|
+
@adapter.set_topic(target, topic)
|
27
|
+
end
|
28
|
+
|
29
|
+
def shut_down
|
30
|
+
@adapter.shut_down
|
31
|
+
end
|
32
|
+
|
23
33
|
private
|
24
34
|
|
25
35
|
def load_adapter
|
@@ -27,7 +37,8 @@ module Lita
|
|
27
37
|
adapter_class = Lita.adapters[adapter_name.to_sym]
|
28
38
|
|
29
39
|
unless adapter_class
|
30
|
-
|
40
|
+
Lita.logger.fatal("Unknown adapter: :#{adapter_name}.")
|
41
|
+
abort
|
31
42
|
end
|
32
43
|
|
33
44
|
@adapter = adapter_class.new(self)
|
data/lib/lita/rspec.rb
CHANGED
@@ -3,18 +3,34 @@ module Lita
|
|
3
3
|
def self.included(base)
|
4
4
|
base.class_eval do
|
5
5
|
let(:robot) { Robot.new }
|
6
|
+
let(:source) { Source.new(user) }
|
7
|
+
let(:user) { User.new("1", name: "Test User") }
|
6
8
|
|
7
9
|
before do
|
8
10
|
allow(Lita).to receive(:handlers).and_return([described_class])
|
9
11
|
stub_const("Lita::REDIS_NAMESPACE", "lita.test")
|
10
12
|
keys = Lita.redis.keys("*")
|
11
13
|
Lita.redis.del(keys) unless keys.empty?
|
14
|
+
allow(robot).to receive(:send_messages)
|
12
15
|
end
|
13
16
|
end
|
14
17
|
end
|
15
18
|
|
19
|
+
def expect_reply(*arguments, invert: false)
|
20
|
+
method = invert ? :not_to : :to
|
21
|
+
expect(robot).public_send(
|
22
|
+
method,
|
23
|
+
receive(:send_messages).with(source, *arguments)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
alias_method :expect_replies, :expect_reply
|
27
|
+
|
28
|
+
def expect_no_reply(*arguments)
|
29
|
+
expect_reply(*arguments, invert: true)
|
30
|
+
end
|
31
|
+
alias_method :expect_no_replies, :expect_no_reply
|
32
|
+
|
16
33
|
def send_test_message(body)
|
17
|
-
source = Source.new("Test User")
|
18
34
|
message = Message.new(robot, body, source)
|
19
35
|
robot.receive(message)
|
20
36
|
end
|
@@ -47,5 +63,5 @@ module Lita
|
|
47
63
|
end
|
48
64
|
|
49
65
|
RSpec.configure do |config|
|
50
|
-
config.include Lita::RSpec,
|
66
|
+
config.include Lita::RSpec, lita: true
|
51
67
|
end
|
data/lib/lita/user.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Lita
|
2
|
+
class User
|
3
|
+
class << self
|
4
|
+
def redis
|
5
|
+
@redis ||= Redis::Namespace.new("users", redis: Lita.redis)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create(id, metadata = {})
|
9
|
+
user = find_by_id(id)
|
10
|
+
unless user
|
11
|
+
user = new(id, metadata)
|
12
|
+
user.save
|
13
|
+
end
|
14
|
+
user
|
15
|
+
end
|
16
|
+
alias_method :find, :create
|
17
|
+
|
18
|
+
def find_by_id(id)
|
19
|
+
metadata = redis.hgetall("id:#{id}")
|
20
|
+
return new(id, metadata) if metadata.key?("name")
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_by_name(name)
|
24
|
+
id = redis.get("name:#{name}")
|
25
|
+
find_by_id(id) if id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :id, :name, :metadata
|
30
|
+
|
31
|
+
def initialize(id, metadata = {})
|
32
|
+
@id = id.to_s
|
33
|
+
@metadata = metadata
|
34
|
+
@name = @metadata[:name] || @metadata["name"] || @id
|
35
|
+
end
|
36
|
+
|
37
|
+
def save
|
38
|
+
redis.pipelined do
|
39
|
+
redis.hmset("id:#{id}", *metadata.to_a.flatten)
|
40
|
+
redis.set("name:#{name}", id)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def ==(other)
|
45
|
+
id == other.id && name == other.name
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def redis
|
51
|
+
self.class.redis
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/lita/version.rb
CHANGED
data/lita.gemspec
CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.required_ruby_version = ">= 2.0.0"
|
22
|
+
|
21
23
|
spec.add_runtime_dependency "bundler", "~> 1.3"
|
22
24
|
spec.add_runtime_dependency "redis-namespace", "~> 1.3.0"
|
23
25
|
spec.add_runtime_dependency "thor", "~> 0.18.1"
|
data/skeleton/Gemfile
CHANGED
data/skeleton/lita_config.rb
CHANGED
@@ -2,6 +2,16 @@ Lita.configure do |config|
|
|
2
2
|
# The name your robot will use.
|
3
3
|
config.robot.name = "Lita"
|
4
4
|
|
5
|
+
# The severity of messages to log. Options are:
|
6
|
+
# :debug, :info, :warn, :error, :fatal
|
7
|
+
# Messages at the selected level and above will be logged.
|
8
|
+
config.robot.log_level = :info
|
9
|
+
|
10
|
+
# An array of user IDs that are considered administrators. These users
|
11
|
+
# the ability to add and remove other users from authorization groups.
|
12
|
+
# What is considered a user ID will change depending on which adapter you use.
|
13
|
+
# config.robot.admins = ["1", "2"]
|
14
|
+
|
5
15
|
# The adapter you want to connect with. Make sure you've added the
|
6
16
|
# appropriate gem to the Gemfile.
|
7
17
|
config.robot.adapter = :shell
|
data/spec/lita/adapter_spec.rb
CHANGED
@@ -3,12 +3,21 @@ require "spec_helper"
|
|
3
3
|
describe Lita::Adapter do
|
4
4
|
let(:robot) { double("Robot") }
|
5
5
|
|
6
|
+
let(:required_methods) { [:run, :send_messages, :set_topic, :shut_down] }
|
7
|
+
|
6
8
|
subject { described_class.new(robot) }
|
7
9
|
|
8
10
|
it "stores a Robot" do
|
9
11
|
expect(subject.robot).to eql(robot)
|
10
12
|
end
|
11
13
|
|
14
|
+
it "logs a warning if a required method has not been implemented" do
|
15
|
+
expect(Lita.logger).to receive(:warn).exactly(required_methods.size).times
|
16
|
+
required_methods.each do |method|
|
17
|
+
subject.public_send(method)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
12
21
|
describe ".require_config" do
|
13
22
|
let(:adapter_class) do
|
14
23
|
Class.new(described_class) do
|
@@ -21,9 +30,8 @@ describe Lita::Adapter do
|
|
21
30
|
subject { adapter_class.new(robot) }
|
22
31
|
|
23
32
|
it "ensures that config keys are present on initialization" do
|
24
|
-
expect
|
25
|
-
|
26
|
-
end.to raise_error(Lita::ConfigError, /foo, bar, baz, blah, bleh/)
|
33
|
+
expect(Lita.logger).to receive(:fatal).with(/foo, bar, baz, blah, bleh/)
|
34
|
+
expect { subject }.to raise_error(SystemExit)
|
27
35
|
end
|
28
36
|
end
|
29
37
|
end
|
@@ -12,16 +12,25 @@ describe Lita::Adapters::Shell do
|
|
12
12
|
|
13
13
|
it "passes input to the Robot and breaks on an exit message" do
|
14
14
|
expect(subject).to receive(:print).with("#{robot.name} > ").twice
|
15
|
-
allow(
|
15
|
+
allow($stdin).to receive(:gets).and_return("foo", "exit")
|
16
16
|
expect(robot).to receive(:receive).with(an_instance_of(Lita::Message))
|
17
|
+
allow(Thread).to receive(:new) { |&block| block.call }
|
17
18
|
subject.run
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
22
|
describe "#send_message" do
|
22
23
|
it "prints its input" do
|
24
|
+
expect(subject).to receive(:puts)
|
23
25
|
expect(subject).to receive(:puts).with("bar")
|
24
26
|
subject.send_messages(double("target"), "bar")
|
25
27
|
end
|
26
28
|
end
|
29
|
+
|
30
|
+
describe "#shut_down" do
|
31
|
+
it "outputs a blank line" do
|
32
|
+
expect(subject).to receive(:puts)
|
33
|
+
subject.shut_down
|
34
|
+
end
|
35
|
+
end
|
27
36
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Lita::Authorization, lita: true do
|
4
|
+
let(:user) do
|
5
|
+
user = double("User")
|
6
|
+
allow(user).to receive(:id).and_return("1")
|
7
|
+
user
|
8
|
+
end
|
9
|
+
|
10
|
+
describe ".add_user_to_group" do
|
11
|
+
it "adds users to an auth group" do
|
12
|
+
allow(described_class).to receive(:user_is_admin?).and_return(true)
|
13
|
+
described_class.add_user_to_group(user, "employees")
|
14
|
+
expect(described_class.user_in_group?(user, "employees")).to be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
it "can only be called by admins" do
|
18
|
+
allow(described_class).to receive(:user_is_admin?).and_return(false)
|
19
|
+
described_class.add_user_to_group(user, "employees")
|
20
|
+
expect(described_class.user_in_group?(user, "employees")).to be_false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe ".remove_user_from_group" do
|
25
|
+
it "removes users from an auth group" do
|
26
|
+
allow(described_class).to receive(:user_is_admin?).and_return(true)
|
27
|
+
described_class.add_user_to_group(user, "employees")
|
28
|
+
described_class.remove_user_from_group(user, "employees")
|
29
|
+
expect(described_class.user_in_group?(user, "employees")).to be_false
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can only be called by admins" do
|
33
|
+
allow(described_class).to receive(:user_is_admin?).and_return(true)
|
34
|
+
described_class.add_user_to_group(user, "employees")
|
35
|
+
allow(described_class).to receive(:user_is_admin?).and_return(false)
|
36
|
+
described_class.remove_user_from_group(user, "employees")
|
37
|
+
expect(described_class.user_in_group?(user, "employees")).to be_true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe ".user_in_group?" do
|
42
|
+
# Positive case is covered by .add_user_to_group's example.
|
43
|
+
|
44
|
+
it "returns false if the user is in the group" do
|
45
|
+
expect(described_class.user_in_group?(user, "employees")).to be_false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe ".user_is_admin?" do
|
50
|
+
it "returns true if the user's ID is in the config" do
|
51
|
+
Lita.config.robot.admins = "1"
|
52
|
+
expect(described_class.user_is_admin?(user)).to be_true
|
53
|
+
end
|
54
|
+
|
55
|
+
it "returns false if the user's ID is not in the config" do
|
56
|
+
Lita.config.robot.admins = nil
|
57
|
+
expect(described_class.user_is_admin?(user)).to be_false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|