lita 2.7.2 → 3.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.
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