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.
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
- @routes << Route.new(pattern, to, command)
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
- if route.pattern === instance.message_body
37
- if route.command?
38
- return instance.command?
39
- else
40
- return true
41
- end
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
- false
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
@@ -6,6 +6,7 @@ module Lita
6
6
  alias_method :message, :body
7
7
 
8
8
  def_delegators :@body, :scan
9
+ def_delegators :@source, :user
9
10
 
10
11
  def initialize(robot, body, source)
11
12
  @robot = robot
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
- raise UnknownAdapterError.new("Unknown adapter: :#{adapter_name}")
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, lita_handler: true
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
@@ -1,3 +1,3 @@
1
1
  module Lita
2
- VERSION = "0.0.1"
2
+ VERSION = "1.0.0"
3
3
  end
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
@@ -7,3 +7,8 @@ gem "lita"
7
7
 
8
8
  # Uncomment to use the IRC adapter
9
9
  # gem "lita-irc"
10
+
11
+ # Add handlers to give Lita new functionality.
12
+ # For example:
13
+ # gem "lita-google-images"
14
+ # gem "lita-karma"
@@ -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
@@ -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 do
25
- subject
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(subject).to receive(:gets).and_return("foo", "exit")
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