lita 2.7.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop.yml +26 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +6 -470
  6. data/Rakefile +3 -3
  7. data/lib/lita.rb +27 -19
  8. data/lib/lita/adapter.rb +46 -5
  9. data/lib/lita/adapters/shell.rb +18 -13
  10. data/lib/lita/authorization.rb +1 -1
  11. data/lib/lita/cli.rb +37 -23
  12. data/lib/lita/common.rb +35 -0
  13. data/lib/lita/config.rb +33 -13
  14. data/lib/lita/daemon.rb +15 -12
  15. data/lib/lita/handler.rb +49 -9
  16. data/lib/lita/handlers/authorization.rb +47 -47
  17. data/lib/lita/handlers/help.rb +16 -17
  18. data/lib/lita/handlers/info.rb +38 -0
  19. data/lib/lita/handlers/room.rb +32 -0
  20. data/lib/lita/http_route.rb +30 -19
  21. data/lib/lita/message.rb +3 -6
  22. data/lib/lita/rack_app.rb +11 -89
  23. data/lib/lita/response.rb +5 -15
  24. data/lib/lita/robot.rb +26 -10
  25. data/lib/lita/rspec.rb +6 -8
  26. data/lib/lita/rspec/handler.rb +49 -121
  27. data/lib/lita/rspec/matchers/event_subscription_matcher.rb +67 -0
  28. data/lib/lita/rspec/matchers/http_route_matcher.rb +72 -0
  29. data/lib/lita/rspec/matchers/route_matcher.rb +69 -0
  30. data/lib/lita/source.rb +5 -18
  31. data/lib/lita/timer.rb +45 -0
  32. data/lib/lita/user.rb +51 -4
  33. data/lib/lita/util.rb +5 -5
  34. data/lib/lita/version.rb +1 -1
  35. data/lita.gemspec +6 -3
  36. data/spec/lita/adapter_spec.rb +10 -2
  37. data/spec/lita/adapters/shell_spec.rb +3 -3
  38. data/spec/lita/authorization_spec.rb +11 -11
  39. data/spec/lita/config_spec.rb +8 -0
  40. data/spec/lita/daemon_spec.rb +65 -0
  41. data/spec/lita/handler_spec.rb +50 -11
  42. data/spec/lita/handlers/authorization_spec.rb +1 -1
  43. data/spec/lita/handlers/info_spec.rb +31 -0
  44. data/spec/lita/handlers/room_spec.rb +20 -0
  45. data/spec/lita/logger_spec.rb +1 -1
  46. data/spec/lita/message_spec.rb +4 -4
  47. data/spec/lita/rack_app_spec.rb +92 -0
  48. data/spec/lita/response_spec.rb +17 -8
  49. data/spec/lita/robot_spec.rb +23 -14
  50. data/spec/lita/rspec_spec.rb +1 -1
  51. data/spec/lita/source_spec.rb +0 -16
  52. data/spec/lita/timer_spec.rb +30 -0
  53. data/spec/lita/user_spec.rb +66 -6
  54. data/spec/lita_spec.rb +37 -0
  55. data/spec/spec_helper.rb +11 -0
  56. data/templates/locales/en.yml +90 -0
  57. data/templates/plugin/Rakefile +1 -1
  58. data/templates/plugin/lib/lita/plugin_type/plugin.tt +4 -0
  59. data/templates/plugin/locales/en.yml.tt +4 -0
  60. metadata +77 -18
  61. data/lib/lita/handlers/web.rb +0 -25
  62. data/spec/lita/handlers/web_spec.rb +0 -19
@@ -0,0 +1,67 @@
1
+ module Lita
2
+ module RSpec
3
+ # A namespace to hold all of Lita's RSpec matchers.
4
+ module Matchers
5
+ # Used to complete an event subscription test chain.
6
+ class EventSubscriptionMatcher
7
+ attr_accessor :context, :event_name, :expectation
8
+ attr_reader :expected_route
9
+
10
+ def initialize(context, event_name, expectation: true)
11
+ self.context = context
12
+ self.event_name = event_name
13
+ self.expectation = expectation
14
+ end
15
+
16
+ # Sets an expectation that a handler method will or will not be called in
17
+ # response to a triggered event, then triggers the event.
18
+ # @param target_method_name [String, Symbol] The name of the method that
19
+ # should or should not be triggered.
20
+ # @return [void]
21
+ def to(target_method_name)
22
+ self.expected_route = target_method_name
23
+
24
+ e = expectation
25
+ ev = event_name
26
+ i = i18n_key
27
+
28
+ context.instance_eval do
29
+ called = false
30
+ allow(subject).to receive(target_method_name) { called = true }
31
+ robot.trigger(ev)
32
+ expect(called).to be(e), I18n.t(i, event: ev, route: target_method_name)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def description_prefix
39
+ if expectation
40
+ "routes event"
41
+ else
42
+ "doesn't route event"
43
+ end
44
+ end
45
+
46
+ def expected_route=(route)
47
+ @expected_route = route
48
+ set_description
49
+ end
50
+
51
+ def i18n_key
52
+ if expectation
53
+ "lita.rspec.event_subscription_failure"
54
+ else
55
+ "lita.rspec.negative_event_subscription_failure"
56
+ end
57
+ end
58
+
59
+ def set_description
60
+ description = %{#{description_prefix} "#{event_name}"}
61
+ description << " to :#{expected_route}" if expected_route
62
+ ::RSpec.current_example.metadata[:description] = description
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,72 @@
1
+ module Lita
2
+ module RSpec
3
+ # A namespace to hold all of Lita's RSpec matchers.
4
+ module Matchers
5
+ # Used to complete an HTTP routing test chain.
6
+ class HTTPRouteMatcher
7
+ attr_accessor :context, :http_method, :expectation, :path
8
+ attr_reader :expected_route
9
+
10
+ def initialize(context, http_method, path, expectation: true)
11
+ self.context = context
12
+ self.http_method = http_method
13
+ self.path = path
14
+ self.expectation = expectation
15
+ set_description
16
+ end
17
+
18
+ # Sets an expectation that an HTTP route will or will not be triggered,
19
+ # then makes an HTTP request against the app with the HTTP request
20
+ # method and path originally provided.
21
+ # @param route [Symbol] The name of the method that should or should not
22
+ # be triggered.
23
+ # @return [void]
24
+ def to(route)
25
+ self.expected_route = route
26
+
27
+ e = expectation
28
+ m = http_method.upcase
29
+ p = path
30
+ i = i18n_key
31
+
32
+ context.instance_eval do
33
+ called = false
34
+ allow(subject).to receive(route) { called = true }
35
+ env = Rack::MockRequest.env_for(p, method: m)
36
+ robot.app.call(env)
37
+ expect(called).to be(e), I18n.t(i, method: m, path: p, route: route)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def description_prefix
44
+ if expectation
45
+ "routes"
46
+ else
47
+ "doesn't route"
48
+ end
49
+ end
50
+
51
+ def expected_route=(route)
52
+ @expected_route = route
53
+ set_description
54
+ end
55
+
56
+ def i18n_key
57
+ if expectation
58
+ "lita.rspec.http_route_failure"
59
+ else
60
+ "lita.rspec.negative_http_route_failure"
61
+ end
62
+ end
63
+
64
+ def set_description
65
+ description = "#{description_prefix} #{http_method.upcase} #{path}"
66
+ description << " to :#{expected_route}" if expected_route
67
+ ::RSpec.current_example.metadata[:description] = description
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,69 @@
1
+ module Lita
2
+ module RSpec
3
+ # A namespace to hold all of Lita's RSpec matchers.
4
+ module Matchers
5
+ # Used to complete a chat routing test chain.
6
+ class RouteMatcher
7
+ attr_accessor :context, :expectation, :message_body
8
+ attr_reader :expected_route
9
+
10
+ def initialize(context, message_body, expectation: true)
11
+ self.context = context
12
+ self.message_body = message_body
13
+ self.expectation = expectation
14
+ set_description
15
+ end
16
+
17
+ # Sets an expectation that a route will or will not be triggered, then
18
+ # sends the message originally provided.
19
+ # @param route [Symbol] The name of the method that should or should not
20
+ # be triggered.
21
+ # @return [void]
22
+ def to(route)
23
+ self.expected_route = route
24
+
25
+ e = expectation
26
+ b = message_body
27
+ i = i18n_key
28
+
29
+ context.instance_eval do
30
+ allow(Authorization).to receive(:user_in_group?).and_return(true)
31
+ called = false
32
+ allow(subject).to receive(route) { called = true }
33
+ send_message(b)
34
+ expect(called).to be(e), I18n.t(i, message: b, route: route)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def description_prefix
41
+ if expectation
42
+ "routes"
43
+ else
44
+ "doesn't route"
45
+ end
46
+ end
47
+
48
+ def expected_route=(route)
49
+ @expected_route = route
50
+ set_description
51
+ end
52
+
53
+ def i18n_key
54
+ if expectation
55
+ "lita.rspec.route_failure"
56
+ else
57
+ "lita.rspec.negative_route_failure"
58
+ end
59
+ end
60
+
61
+ def set_description
62
+ description = %{#{description_prefix} "#{message_body}"}
63
+ description << " to :#{expected_route}" if expected_route
64
+ ::RSpec.current_example.metadata[:description] = description
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -27,25 +27,12 @@ module Lita
27
27
  # this string will differ depending on the chat service.
28
28
  # @param private_message [Boolean] A flag indicating whether or not the
29
29
  # message was sent privately.
30
- def initialize(*args)
31
- options = args.last.is_a?(Hash) ? args.pop : {}
30
+ def initialize(user: nil, room: nil, private_message: false)
31
+ @user = user
32
+ @room = room
33
+ @private_message = private_message
32
34
 
33
- @user = options[:user]
34
- @room = options[:room]
35
- @private_message = options[:private_message]
36
-
37
- if args.size > 0
38
- Lita.logger.warn <<-WARNING.chomp
39
- Passing positional arguments to Source is deprecated. \
40
- Use Source.new(user: user, room: room, private_message: pm) instead.
41
- WARNING
42
- @user = args[0] if args[0]
43
- @room = args[1] if args[1]
44
- end
45
-
46
- if user.nil? && room.nil?
47
- raise ArgumentError.new("Either a user or a room is required.")
48
- end
35
+ raise ArgumentError, I18n.t("lita.source.user_or_room_required") if user.nil? && room.nil?
49
36
 
50
37
  @private_message = true if room.nil?
51
38
  end
@@ -0,0 +1,45 @@
1
+ module Lita
2
+ # A timer that executes a block after a certain number of seconds, either once or repeatedly.
3
+ # @since 3.0.0
4
+ class Timer
5
+ # @param interval [Integer] The number of seconds to wait before calling the block.
6
+ # @param recurring [Boolean] If true, the timer will fire repeatedly until stopped.
7
+ # @yieldparam timer [Lita::Timer] The current {Lita::Timer} instance.
8
+ def initialize(interval: 0, recurring: false, &block)
9
+ @interval = interval
10
+ @recurring = recurring
11
+ @running = false
12
+ @block = block
13
+ end
14
+
15
+ # Starts running the timer.
16
+ def start
17
+ @running = true
18
+ run
19
+ end
20
+
21
+ # Stops the timer, preventing any further invocations of the block until started again.
22
+ def stop
23
+ @running = false
24
+ end
25
+
26
+ private
27
+
28
+ # Is this a recurring timer?
29
+ def recurring?
30
+ @recurring
31
+ end
32
+
33
+ # Sleep for the given interval, call the block, then run again if it's a recurring timer.
34
+ def run
35
+ sleep @interval
36
+ @block.call(self) if running? && @block
37
+ run if running? && recurring?
38
+ end
39
+
40
+ # Is the timer currently running?
41
+ def running?
42
+ @running
43
+ end
44
+ end
45
+ end
@@ -22,14 +22,28 @@ module Lita
22
22
  end
23
23
  user
24
24
  end
25
- alias_method :find, :create
25
+
26
+ # @deprecated Use {.create} instead.
27
+ def find(id, metadata = {})
28
+ Lita.logger.warn I18n.t("lita.user.find_deprecated")
29
+ create(id, metadata)
30
+ end
26
31
 
27
32
  # Finds a user by ID.
28
33
  # @param id [Integer, String] The user's unique ID.
29
34
  # @return [Lita::User, nil] The user or +nil+ if no such user is known.
30
35
  def find_by_id(id)
31
36
  metadata = redis.hgetall("id:#{id}")
32
- return new(id, metadata) if metadata.key?("name")
37
+ new(id, metadata) if metadata.key?("name")
38
+ end
39
+
40
+ # Finds a user by mention name.
41
+ # @param mention_name [String] The user's mention name.
42
+ # @return [Lita::User, nil] The user or +nil+ if no such user is known.
43
+ # @since 3.0.0
44
+ def find_by_mention_name(mention_name)
45
+ id = redis.get("mention_name:#{mention_name}")
46
+ find_by_id(id) if id
33
47
  end
34
48
 
35
49
  # Finds a user by display name.
@@ -39,6 +53,28 @@ module Lita
39
53
  id = redis.get("name:#{name}")
40
54
  find_by_id(id) if id
41
55
  end
56
+
57
+ # Attempts to find a user with a name starting with the provided string.
58
+ # @param name [String] The first characters in the user's name.
59
+ # @return [Lita::User, nil] The user, or +nil+ if zero or greater than 1 matches were found.
60
+ # @since 3.0.0
61
+ def find_by_partial_name(name)
62
+ keys = redis.keys("name:#{name}*")
63
+
64
+ if keys.length == 1
65
+ id = redis.get(keys.first)
66
+ find_by_id(id)
67
+ end
68
+ end
69
+
70
+ # Finds a user by ID, mention name, name, or partial name.
71
+ # @param identifier [String] The user's ID, name, partial name, or mention name.
72
+ # @return [Lita::User, nil] The user or +nil+ if no users were found.
73
+ # @since 3.0.0
74
+ def fuzzy_find(identifier)
75
+ find_by_id(identifier) || find_by_mention_name(identifier) ||
76
+ find_by_name(identifier) || find_by_partial_name(identifier)
77
+ end
42
78
  end
43
79
 
44
80
  # The user's unique ID.
@@ -60,15 +96,19 @@ module Lita
60
96
  @id = id.to_s
61
97
  @metadata = metadata
62
98
  @name = @metadata[:name] || @metadata["name"] || @id
99
+ ensure_name_metadata_set
63
100
  end
64
101
 
65
102
  # Saves the user record to Redis, overwriting an previous data for the
66
103
  # current ID and user name.
67
104
  # @return [void]
68
105
  def save
106
+ mention_name = metadata[:mention_name] || metadata["mention_name"]
107
+
69
108
  redis.pipelined do
70
109
  redis.hmset("id:#{id}", *metadata.to_a.flatten)
71
110
  redis.set("name:#{name}", id)
111
+ redis.set("mention_name:#{mention_name}", id) if mention_name
72
112
  end
73
113
  end
74
114
 
@@ -77,12 +117,19 @@ module Lita
77
117
  # @param other (Lita::User) The user to compare against.
78
118
  # @return [Boolean] True if users are equal, false otherwise.
79
119
  def ==(other)
80
- other.respond_to?(:id) && id == other.id &&
81
- other.respond_to?(:name) && name == other.name
120
+ other.respond_to?(:id) && id == other.id && other.respond_to?(:name) && name == other.name
82
121
  end
83
122
 
84
123
  private
85
124
 
125
+ # Ensure the user's metadata contains their name, to ensure their Redis hash contains at least
126
+ # one value. It's not possible to store an empty hash key in Redis.
127
+ def ensure_name_metadata_set
128
+ username = metadata.delete("name")
129
+ username = metadata.delete(:name) unless username
130
+ metadata["name"] = username || id
131
+ end
132
+
86
133
  # The Redis connection for user persistence.
87
134
  def redis
88
135
  self.class.redis
@@ -1,5 +1,5 @@
1
1
  module Lita
2
- # Handy utilities used by other parts Lita classes.
2
+ # Handy utilities used by other Lita classes.
3
3
  module Util
4
4
  # A regular expression for acronyms.
5
5
  ACRONYM_REGEX = /(?=a)b/
@@ -11,12 +11,12 @@ module Lita
11
11
  # @return [String] The transformed word.
12
12
  def underscore(camel_cased_word)
13
13
  word = camel_cased_word.to_s.dup
14
- word.gsub!('::', '/')
14
+ word.gsub!("::", "/")
15
15
  word.gsub!(/(?:([A-Za-z\d])|^)(#{ACRONYM_REGEX})(?=\b|[^a-z])/) do
16
- "#{$1}#{$1 && '_'}#{$2.downcase}"
16
+ "#{Regexp.last_match[1]}#{Regexp.last_match[1] && '_'}#{Regexp.last_match[2].downcase}"
17
17
  end
18
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
19
- word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
18
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
19
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
20
20
  word.tr!("-", "_")
21
21
  word.downcase!
22
22
  word
@@ -1,4 +1,4 @@
1
1
  module Lita
2
2
  # The current version of Lita.
3
- VERSION = "2.7.2"
3
+ VERSION = "3.0.0"
4
4
  end
@@ -22,16 +22,19 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_runtime_dependency "bundler", ">= 1.3"
24
24
  spec.add_runtime_dependency "faraday", ">= 0.8.7"
25
+ spec.add_runtime_dependency "http_router", ">= 0.11.1"
26
+ spec.add_runtime_dependency "ice_nine", ">= 0.11.0"
27
+ spec.add_runtime_dependency "i18n", ">= 0.6.9"
25
28
  spec.add_runtime_dependency "multi_json", ">= 1.7.7"
29
+ spec.add_runtime_dependency "puma", ">= 2.7.1"
26
30
  spec.add_runtime_dependency "rack", ">= 1.5.2"
27
31
  spec.add_runtime_dependency "redis-namespace", ">= 1.3.0"
28
- spec.add_runtime_dependency "thin", ">= 1.5.1"
29
32
  spec.add_runtime_dependency "thor", ">= 0.18.1"
30
33
 
31
34
  spec.add_development_dependency "rake"
32
- spec.add_development_dependency "rspec", ">= 2.14"
35
+ spec.add_development_dependency "rspec", ">= 3.0.0.beta1"
33
36
  spec.add_development_dependency "simplecov"
34
37
  spec.add_development_dependency "coveralls"
35
38
  spec.add_development_dependency "pry"
36
- spec.add_development_dependency "cane"
39
+ spec.add_development_dependency "rubocop"
37
40
  end
@@ -1,9 +1,9 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Lita::Adapter do
4
- let(:robot) { double("Robot") }
4
+ let(:robot) { instance_double("Lita::Robot") }
5
5
 
6
- let(:required_methods) { [:run, :send_messages, :set_topic, :shut_down] }
6
+ let(:required_methods) { [:join, :part, :run, :send_messages, :set_topic, :shut_down] }
7
7
 
8
8
  subject { described_class.new(robot) }
9
9
 
@@ -34,4 +34,12 @@ describe Lita::Adapter do
34
34
  expect { subject }.to raise_error(SystemExit)
35
35
  end
36
36
  end
37
+
38
+ describe ".namespace" do
39
+ it "raises an exception if self.name is not implemented" do
40
+ expect do
41
+ Class.new(Lita::Adapter).namespace
42
+ end.to raise_error(RuntimeError)
43
+ end
44
+ end
37
45
  end