lita 0.0.1 → 1.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/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
|