lita-karma 3.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5f7807772d594056ec3c21dae05d056be2394e3a
4
- data.tar.gz: d28351489ce8234e359a15657bccb4cc87f4e0a7
3
+ metadata.gz: e1e03d87e444b488534df84e8c3d16a8f02f6c82
4
+ data.tar.gz: 45ad68d97c0bb3b3e172a1334c8d4ad5e29af53f
5
5
  SHA512:
6
- metadata.gz: ae64368983385ac07aa53548fdb5f91276b4d15c323cf47845208b46f4b1e7d1bf1fdc8ef041daee27bcf5eadb33821d0a7132eda98a2739bd8fdbec626248f3
7
- data.tar.gz: f5b8da41d0caa619038e2518a400312d54afd9a005e807c04d39f83a71f798f68b07e4a7df8abd14de9470ec8266990decea488038e037605a941f1a6d118e22
6
+ metadata.gz: 457f520bc9e527737878628b873e85dc86f2449b45d77c3ac3506a8e81276017311e4db61a312d8e01981652ab733d316be3cb936e8da8a14a115ee5e7f663b0
7
+ data.tar.gz: 8625c91141b4f995f4be50fe63c72e06ff89643f7f423330d79b43a00c367454059defc608841b795fd31e775973a6375965effbf9c67050dc6e628488a466df
data/README.md CHANGED
@@ -18,11 +18,10 @@ gem "lita-karma"
18
18
 
19
19
  ### Optional attributes
20
20
 
21
- * `cooldown` (Integer, nil) - Controls how long a user must wait after modifying a term before they can modify it again. The value should be an integer number of seconds. Set it to `nil` to disable rate limiting. Default: `300` (5 minutes).
22
- * `link_karma_threshold` (Integer, nil) - Controls how many points a term must have before it can be linked to other terms or before terms can be linked to it. Treated as an absolute value, so it applies to both positive and negative karma. Set it to `nil` to allow all terms to be linked regardless of karma. Default: `10`.
23
- * `term_pattern` (Regexp) - Determines what Lita will recognize as a valid term for tracking karma. Default: `/[\[\]\p{Word}\._|\{\}]{2,}/`.
24
- * `term_normalizer` (Proc) - A custom callable that determines how each term will be normalized before being stored in Redis. The proc should take one argument, the term as matched via regular expression, and return one value, the normalized version of the term.
25
-
21
+ * **cooldown** (`Integer`, `nil`) - Controls how long a user must wait after modifying a term before they can modify it again. The value should be an integer number of seconds. Set it to `nil` to disable rate limiting. Default: `300` (5 minutes).
22
+ * **link_karma_threshold** (`Integer`, `nil`) - Controls how many points a term must have before it can be linked to other terms or before terms can be linked to it. Treated as an absolute value, so it applies to both positive and negative karma. Set it to `nil` to allow all terms to be linked regardless of karma. Default: `10`.
23
+ * **term_pattern** (`Regexp`) - Determines what Lita will recognize as a valid term for tracking karma. Default: `/[\[\]\p{Word}\._|\{\}]{2,}/`.
24
+ * **term_normalizer** (`#call`) - A custom callable that determines how each term will be normalized before being stored in Redis. The proc should take one argument, the term as matched via regular expression, and return one value, the normalized version of the term. Default: turns the term into a string, downcases it, and strips whitespace off both ends.
26
25
 
27
26
  ### Example
28
27
 
@@ -40,6 +39,8 @@ end
40
39
 
41
40
  ## Usage
42
41
 
42
+ ### Giving Karma
43
+
43
44
  Lita will add a karma point whenever it hears a term upvoted:
44
45
 
45
46
  ```
@@ -58,6 +59,8 @@ To check the current karma for a term without modifying it:
58
59
  term~~
59
60
  ```
60
61
 
62
+ ### Listing Karma
63
+
61
64
  To list the top scoring terms:
62
65
 
63
66
  ```
@@ -76,12 +79,14 @@ To list the worst scoring terms:
76
79
  Lita: karma worst
77
80
  ```
78
81
 
79
- These commands will list 5 terms by default. To specify a number (no greater than 25), pass a second argument to the karma command:
82
+ The list commands will list 5 terms by default. To specify a number (no greater than 25), pass a second argument to the karma command:
80
83
 
81
84
  ```
82
85
  Lita: karma best 10
83
86
  ```
84
87
 
88
+ ### Linking Terms
89
+
85
90
  You can also link terms together. This adds one term's karma to another's whenever it is displayed. A link is uni-directional and non-destructive. You can unlink terms at any time.
86
91
 
87
92
  ```
@@ -103,6 +108,17 @@ foo~~
103
108
 
104
109
  When a term is linked, the total karma score is displayed first, followed by the score of the term without its linked terms in parentheses.
105
110
 
111
+ ### Modification lists
112
+
113
+ To get a list of the users who have upvoted or downvoted a term:
114
+
115
+ ```
116
+ Lita: karma modified foo
117
+ > Joe, Amy
118
+ ```
119
+
120
+ ### Deleting Terms
121
+
106
122
  To permanently delete a term and all its links:
107
123
 
108
124
  ```
@@ -1 +1,11 @@
1
- require "lita/handlers/karma"
1
+ require "set"
2
+
3
+ require "lita"
4
+
5
+ Lita.load_locales Dir[File.expand_path(
6
+ File.join("..", "..", "locales", "*.yml"), __FILE__
7
+ )]
8
+
9
+ require "lita/handlers/karma/chat"
10
+ require 'lita/handlers/karma/config'
11
+ require 'lita/handlers/karma/term'
@@ -0,0 +1,206 @@
1
+ module Lita::Handlers::Karma
2
+ # Tracks karma points for arbitrary terms.
3
+ class Chat < Lita::Handler
4
+ namespace "karma"
5
+
6
+ on :loaded, :define_routes
7
+
8
+ def define_routes(payload)
9
+ define_static_routes
10
+ define_dynamic_routes(config.term_pattern.source)
11
+ end
12
+
13
+ def increment(response)
14
+ modify(response, :increment)
15
+ end
16
+
17
+ def decrement(response)
18
+ modify(response, :decrement)
19
+ end
20
+
21
+ def check(response)
22
+ seen = Set.new
23
+
24
+ output = response.matches.map do |match|
25
+ term = get_term(match[0])
26
+ next if seen.include?(term)
27
+ seen << term
28
+ term.check
29
+ end.compact
30
+
31
+ response.reply output.join("; ")
32
+ end
33
+
34
+ def list_best(response)
35
+ list(response, :list_best)
36
+ end
37
+
38
+ def list_worst(response)
39
+ list(response, :list_worst)
40
+ end
41
+
42
+ def link(response)
43
+ response.matches.each do |match|
44
+ term1 = get_term(match[0])
45
+ term2 = get_term(match[1])
46
+
47
+ result = term1.link(term2)
48
+
49
+ case result
50
+ when Integer
51
+ response.reply t("threshold_not_satisfied", threshold: result)
52
+ when true
53
+ response.reply t("link_success", source: term2, target: term1)
54
+ else
55
+ response.reply t("already_linked", source: term2, target: term1)
56
+ end
57
+ end
58
+ end
59
+
60
+ def unlink(response)
61
+ response.matches.each do |match|
62
+ term1 = get_term(match[0])
63
+ term2 = get_term(match[1])
64
+
65
+ if term1.unlink(term2)
66
+ response.reply t("unlink_success", source: term2, target: term1)
67
+ else
68
+ response.reply t("already_unlinked", source: term2, target: term1)
69
+ end
70
+ end
71
+ end
72
+
73
+ def modified(response)
74
+ term = get_term(response.args[1])
75
+
76
+ users = term.modified
77
+
78
+ if users.empty?
79
+ response.reply t("never_modified", term: term)
80
+ else
81
+ response.reply users.map(&:name).join(", ")
82
+ end
83
+ end
84
+
85
+ def delete(response)
86
+ term = Term.new(robot, response.message.body.sub(/^karma delete /, ""), normalize: false)
87
+
88
+ if term.delete
89
+ response.reply t("delete_success", term: term)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def define_dynamic_routes(pattern)
96
+ self.class.route(
97
+ %r{(#{pattern})\+\+#{token_terminator.source}},
98
+ :increment,
99
+ help: { t("help.increment_key") => t("help.increment_value") }
100
+ )
101
+
102
+ self.class.route(
103
+ %r{(#{pattern})--#{token_terminator.source}},
104
+ :decrement,
105
+ help: { t("help.decrement_key") => t("help.decrement_value") }
106
+ )
107
+
108
+ self.class.route(
109
+ %r{(#{pattern})~~#{token_terminator.source}},
110
+ :check,
111
+ help: { t("help.check_key") => t("help.check_value") }
112
+ )
113
+
114
+ self.class.route(
115
+ %r{^(#{pattern})\s*\+=\s*(#{pattern})(?:\+\+|--|~~)?#{token_terminator.source}},
116
+ :link,
117
+ command: true,
118
+ help: { t("help.link_key") => t("help.link_value") }
119
+ )
120
+
121
+ self.class.route(
122
+ %r{^(#{pattern})\s*-=\s*(#{pattern})(?:\+\+|--|~~)?#{token_terminator.source}},
123
+ :unlink,
124
+ command: true,
125
+ help: { t("help.unlink_key") => t("help.unlink_value") }
126
+ )
127
+ end
128
+
129
+ def define_static_routes
130
+ self.class.route(
131
+ %r{^karma\s+worst},
132
+ :list_worst,
133
+ command: true,
134
+ help: { t("help.list_worst_key") => t("help.list_worst_value") }
135
+ )
136
+
137
+ self.class.route(
138
+ %r{^karma\s+best},
139
+ :list_best,
140
+ command: true,
141
+ help: { t("help.list_best_key") => t("help.list_best_value") }
142
+ )
143
+
144
+ self.class.route(
145
+ %r{^karma\s+modified\s+.+},
146
+ :modified,
147
+ command: true,
148
+ help: { t("help.modified_key") => t("help.modified_value") }
149
+ )
150
+
151
+ self.class.route(
152
+ %r{^karma\s+delete},
153
+ :delete,
154
+ command: true,
155
+ restrict_to: :karma_admins,
156
+ help: { t("help.delete_key") => t("help.delete_value") }
157
+ )
158
+
159
+ self.class.route(%r{^karma\s*$}, :list_best, command: true)
160
+ end
161
+
162
+ def determine_list_count(response)
163
+ n = (response.args[1] || 5).to_i - 1
164
+ n = 25 if n > 25
165
+ n
166
+ end
167
+
168
+ def get_term(term)
169
+ Term.new(robot, term)
170
+ end
171
+
172
+ def list(response, method_name)
173
+ terms_and_scores = Term.public_send(method_name, robot, determine_list_count(response))
174
+
175
+ output = terms_and_scores.each_with_index.map do |term_and_score, index|
176
+ "#{index + 1}. #{term_and_score[0]} (#{term_and_score[1].to_i})"
177
+ end.join("\n")
178
+
179
+ if output.empty?
180
+ response.reply t("no_terms")
181
+ else
182
+ response.reply output
183
+ end
184
+ end
185
+
186
+ def modify(response, method_name)
187
+ user = response.user
188
+
189
+ output = response.matches.map do |match|
190
+ get_term(match[0]).public_send(method_name, user)
191
+ end
192
+
193
+ response.reply output.join("; ")
194
+ end
195
+
196
+ # To ensure that constructs like foo++bar or foo--bar (the latter is
197
+ # common in some URL generation schemes) do not cause errant karma
198
+ # modifications, force karma tokens be followed by whitespace (in a zero-
199
+ # width, look-ahead operator) or the end of the string.
200
+ def token_terminator
201
+ %r{(?:(?=\s)|$)}
202
+ end
203
+ end
204
+ end
205
+
206
+ Lita.register_handler(Lita::Handlers::Karma::Chat)
@@ -0,0 +1,20 @@
1
+ module Lita::Handlers::Karma
2
+ class Config < Lita::Handler
3
+ namespace "karma"
4
+
5
+ default_term_normalizer = proc do |term|
6
+ term.to_s.downcase.strip
7
+ end
8
+
9
+ config :cooldown, types: [Integer, nil], default: 300
10
+ config :link_karma_threshold, types: [Integer, nil], default: 10
11
+ config :term_pattern, type: Regexp, default: /[\[\]\p{Word}\._|\{\}]{2,}/
12
+ config :term_normalizer, default: default_term_normalizer do
13
+ validate do |value|
14
+ t("callable_required") unless value.respond_to?(:call)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Lita.register_handler(Lita::Handlers::Karma::Config)
@@ -0,0 +1,152 @@
1
+ module Lita::Handlers::Karma
2
+ class Term
3
+ include Lita::Handler::Common
4
+
5
+ namespace "karma"
6
+
7
+ attr_reader :term
8
+
9
+ class << self
10
+ def list_best(robot, n = 5)
11
+ list(:zrevrange, robot, n)
12
+ end
13
+
14
+ def list_worst(robot, n = 5)
15
+ list(:zrange, robot, n)
16
+ end
17
+
18
+ private
19
+
20
+ def list(redis_command, robot, n)
21
+ n = 24 if n > 24
22
+
23
+ handler = new(robot, '', normalize: false)
24
+ handler.redis.public_send(redis_command, "terms", 0, n, with_scores: true)
25
+ end
26
+ end
27
+
28
+ def initialize(robot, term, normalize: true)
29
+ super(robot)
30
+ @term = normalize ? normalize_term(term) : term
31
+ @link_cache = {}
32
+ end
33
+
34
+ def check
35
+ string = "#{self}: #{total_score}"
36
+
37
+ unless links_with_scores.empty?
38
+ link_text = links_with_scores.map { |term, score| "#{term}: #{score}" }.join(", ")
39
+ string << " (#{own_score}), #{t("linked_to")}: #{link_text}"
40
+ end
41
+
42
+ string
43
+ end
44
+
45
+ def decrement(user)
46
+ modify(user, -1)
47
+ end
48
+
49
+ def delete
50
+ redis.zrem("terms", to_s)
51
+ redis.del("modified:#{self}")
52
+ redis.del("links:#{self}")
53
+ redis.smembers("linked_to:#{self}").each do |key|
54
+ redis.srem("links:#{key}", to_s)
55
+ end
56
+ redis.del("linked_to:#{self}")
57
+ end
58
+
59
+ def eql?(other)
60
+ term.eql?(other.term)
61
+ end
62
+
63
+ def hash
64
+ term.hash
65
+ end
66
+
67
+ def increment(user)
68
+ modify(user, 1)
69
+ end
70
+
71
+ def link(other)
72
+ if config.link_karma_threshold
73
+ threshold = config.link_karma_threshold.abs
74
+
75
+ if own_score.abs < threshold || other.own_score.abs < threshold
76
+ return threshold
77
+ end
78
+ end
79
+
80
+ redis.sadd("links:#{self}", other.to_s) && redis.sadd("linked_to:#{other}", to_s)
81
+ end
82
+
83
+ def links
84
+ @links ||= begin
85
+ redis.smembers("links:#{self}").each do |term|
86
+ linked_term = self.class.new(robot, term)
87
+ @link_cache[linked_term.term] = linked_term
88
+ end
89
+ end
90
+ end
91
+
92
+ def links_with_scores
93
+ @links_with_scores ||= begin
94
+ {}.tap do |h|
95
+ links.each do |link|
96
+ h[link] = @link_cache[link].own_score
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def modified
103
+ redis.smembers("modified:#{term}").map do |user_id|
104
+ Lita::User.find_by_id(user_id)
105
+ end
106
+ end
107
+
108
+ def own_score
109
+ @own_score ||= redis.zscore("terms", term).to_i
110
+ end
111
+
112
+ def to_s
113
+ term
114
+ end
115
+
116
+ def total_score
117
+ @total_score ||= begin
118
+ links.inject(own_score) do |memo, linked_term|
119
+ memo + @link_cache[linked_term].own_score
120
+ end
121
+ end
122
+ end
123
+
124
+ def unlink(other)
125
+ redis.srem("links:#{self}", other.to_s) && redis.srem("linked_to:#{other}", to_s)
126
+ end
127
+
128
+ private
129
+
130
+ def modify(user, delta)
131
+ ttl = redis.ttl("cooldown:#{user.id}:#{term}")
132
+
133
+ if ttl > 0
134
+ t("cooling_down", term: self, ttl: ttl, count: ttl)
135
+ else
136
+ modify!(user, delta)
137
+ end
138
+ end
139
+
140
+ def modify!(user, delta)
141
+ user_id = user.id
142
+ redis.zincrby("terms", delta, term)
143
+ redis.sadd("modified:#{self}", user_id)
144
+ redis.setex("cooldown:#{user_id}:#{self}", config.cooldown, 1) if config.cooldown
145
+ check
146
+ end
147
+
148
+ def normalize_term(term)
149
+ config.term_normalizer.call(term)
150
+ end
151
+ end
152
+ end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "lita-karma"
3
- spec.version = "3.0.0"
3
+ spec.version = "3.0.1"
4
4
  spec.authors = ["Jimmy Cuadra"]
5
5
  spec.email = ["jimmy@jimmycuadra.com"]
6
6
  spec.description = %q{A Lita handler for tracking karma points for arbitrary terms.}
@@ -0,0 +1,40 @@
1
+ en:
2
+ lita:
3
+ handlers:
4
+ karma:
5
+ already_linked: "%{source} is already linked to %{target}."
6
+ already_unlinked: "%{source} is not linked to %{target}."
7
+ callable_required: must be a callable object
8
+ cooling_down:
9
+ one: "You cannot modify %{term} for another %{ttl} second."
10
+ other: "You cannot modify %{term} for another %{ttl} seconds."
11
+ delete_success: "%{term} has been deleted."
12
+ help:
13
+ increment_key: TERM++
14
+ increment_value: Increments TERM by one.
15
+ decrement_key: TERM--
16
+ decrement_value: Decrements TERM by one.
17
+ check_key: TERM~~
18
+ check_value: Shows the current karma of TERM.
19
+ link_key: TERM1 += TERM2
20
+ link_value: Links TERM2 to TERM1.
21
+ unlink_key: TERM1 -= TERM2
22
+ unlink_value: Unlinks TERM2 from TERM1.
23
+ list_worst_key: "karma worst [N]"
24
+ list_worst_value: Lists the bottom N terms by karma. N defaults to 5.
25
+ list_best_key: "karma best [N]"
26
+ list_best_value: Lists the top N terms by karma. N defaults to 5.
27
+ modified_key: karma modified TERM
28
+ modified_value: Lists the names of users who have upvoted or downvoted TERM.
29
+ delete_key: karma delete TERM
30
+ delete_value: >-
31
+ Permanently removes TERM and all its link information. TERM is matched
32
+ exactly as typed and does not adhere to the usual pattern for terms.
33
+ link_success: "%{source} has been linked to %{target}."
34
+ linked_to: linked to
35
+ never_modified: "%{term} has never been modified."
36
+ no_terms: There are no terms being tracked yet.
37
+ threshold_not_satisfied: >-
38
+ Terms must have less than or equal to -%{threshold} or greater than or equal to
39
+ %{threshold} karma to be linked or linked to.
40
+ unlink_success: "%{source} has been unlinked from %{target}."
@@ -1,8 +1,10 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Lita::Handlers::Karma, lita_handler: true do
3
+ describe Lita::Handlers::Karma::Chat, lita_handler: true do
4
4
  let(:payload) { double("payload") }
5
5
 
6
+ prepend_before { registry.register_handler(Lita::Handlers::Karma::Config) }
7
+
6
8
  before do
7
9
  registry.config.handlers.karma.cooldown = nil
8
10
  registry.config.handlers.karma.link_karma_threshold = nil
@@ -12,35 +14,28 @@ describe Lita::Handlers::Karma, lita_handler: true do
12
14
 
13
15
  it { is_expected.to route("foo++").to(:increment) }
14
16
  it { is_expected.to route("foo--").to(:decrement) }
17
+ it { is_expected.to route("foo++ bar").to(:increment) }
18
+ it { is_expected.to route("foo-- bar").to(:decrement) }
15
19
  it { is_expected.to route("foo~~").to(:check) }
16
20
  it { is_expected.to route_command("karma best").to(:list_best) }
17
21
  it { is_expected.to route_command("karma worst").to(:list_worst) }
18
- it { is_expected.to route_command("karma modified").to(:modified) }
22
+ it { is_expected.to route_command("karma modified foo").to(:modified) }
19
23
  it do
20
24
  is_expected.to route_command("karma delete").with_authorization_for(:karma_admins).to(:delete)
21
25
  end
22
26
  it { is_expected.to route_command("karma").to(:list_best) }
23
27
  it { is_expected.to route_command("foo += bar").to(:link) }
28
+ it { is_expected.to route_command("foo += bar++").to(:link) }
29
+ it { is_expected.to route_command("foo += bar--").to(:link) }
30
+ it { is_expected.to route_command("foo += bar~~").to(:link) }
24
31
  it { is_expected.to route_command("foo -= bar").to(:unlink) }
32
+ it { is_expected.to route_command("foo -= bar++").to(:unlink) }
33
+ it { is_expected.to route_command("foo -= bar--").to(:unlink) }
34
+ it { is_expected.to route_command("foo -= bar~~").to(:unlink) }
25
35
  it { is_expected.not_to route("+++++").to(:increment) }
26
36
  it { is_expected.not_to route("-----").to(:decrement) }
27
-
28
- describe "#update_data" do
29
- before { subject.redis.flushdb }
30
-
31
- it "adds reverse link data for all linked terms" do
32
- subject.redis.sadd("links:foo", ["bar", "baz"])
33
- subject.upgrade_data(payload)
34
- expect(subject.redis.sismember("linked_to:bar", "foo")).to be(true)
35
- expect(subject.redis.sismember("linked_to:baz", "foo")).to be(true)
36
- end
37
-
38
- it "skips the update if it's already been done" do
39
- expect(subject.redis).to receive(:keys).once.and_return([])
40
- subject.upgrade_data(payload)
41
- subject.upgrade_data(payload)
42
- end
43
- end
37
+ it { is_expected.not_to route("foo++bar").to(:increment) }
38
+ it { is_expected.not_to route("foo--bar").to(:decrement) }
44
39
 
45
40
  describe "#increment" do
46
41
  it "increases the term's score by one and says the new score" do
@@ -50,7 +45,7 @@ describe Lita::Handlers::Karma, lita_handler: true do
50
45
 
51
46
  it "matches multiple terms in one message" do
52
47
  send_message("foo++ bar++")
53
- expect(replies).to eq(["foo: 1", "bar: 1"])
48
+ expect(replies.last).to eq("foo: 1; bar: 1")
54
49
  end
55
50
 
56
51
  it "doesn't start from zero if the term already has a positive score" do
@@ -86,7 +81,7 @@ describe Lita::Handlers::Karma, lita_handler: true do
86
81
 
87
82
  it "matches multiple terms in one message" do
88
83
  send_message("foo-- bar--")
89
- expect(replies).to eq(["foo: -1", "bar: -1"])
84
+ expect(replies.last).to eq("foo: -1; bar: -1")
90
85
  end
91
86
 
92
87
  it "doesn't start from zero if the term already has a positive score" do
@@ -111,7 +106,12 @@ describe Lita::Handlers::Karma, lita_handler: true do
111
106
 
112
107
  it "matches multiple terms in one message" do
113
108
  send_message("foo~~ bar~~")
114
- expect(replies).to eq(["foo: 0", "bar: 0"])
109
+ expect(replies.last).to eq("foo: 0; bar: 0")
110
+ end
111
+
112
+ it "doesn't match the same term multiple times in one message" do
113
+ send_message("foo~~ foo~~")
114
+ expect(replies.size).to eq(1)
115
115
  end
116
116
  end
117
117
 
@@ -244,26 +244,18 @@ MSG
244
244
  end
245
245
 
246
246
  describe "#modified" do
247
- it "replies with the required format if a term is not provided" do
248
- send_command("karma modified")
249
- expect(replies.last).to match(/^Format:/)
250
- end
251
-
252
- it "replies with the required format if the term is an empty string" do
253
- send_command("karma modified ' '")
254
- expect(replies.last).to match(/^Format:/)
255
- end
256
-
257
247
  it "replies with a message if the term hasn't been modified" do
258
248
  send_command("karma modified foo")
259
249
  expect(replies.last).to match(/never been modified/)
260
250
  end
261
251
 
262
- it "lists users who have modified the given term" do
263
- allow(Lita::User).to receive(:find_by_id).and_return(user)
264
- send_message("foo++")
252
+ it "lists users who have modified the given term in count order" do
253
+ other_user = Lita::User.create("2", name: "Other User")
254
+ send_message("foo++", as: user)
255
+ send_message("foo++", as: user)
256
+ send_message("foo++", as: other_user)
265
257
  send_command("karma modified foo")
266
- expect(replies.last).to eq(user.name)
258
+ expect(replies.last).to eq("#{user.name}, #{other_user.name}")
267
259
  end
268
260
  end
269
261
 
@@ -280,11 +272,6 @@ MSG
280
272
  expect(replies.last).to eq("foo: 0")
281
273
  end
282
274
 
283
- it "replies with a warning if the term doesn't exist" do
284
- send_command("karma delete foo")
285
- expect(replies.last).to eq("foo does not exist.")
286
- end
287
-
288
275
  it "matches terms exactly, including leading whitespace" do
289
276
  term = " 'foo bar* 'baz''/ :"
290
277
  subject.redis.zincrby("terms", 1, term)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lita-karma
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jimmy Cuadra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-23 00:00:00.000000000 Z
11
+ date: 2015-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lita
@@ -108,9 +108,12 @@ files:
108
108
  - README.md
109
109
  - Rakefile
110
110
  - lib/lita-karma.rb
111
- - lib/lita/handlers/karma.rb
111
+ - lib/lita/handlers/karma/chat.rb
112
+ - lib/lita/handlers/karma/config.rb
113
+ - lib/lita/handlers/karma/term.rb
112
114
  - lita-karma.gemspec
113
- - spec/lita/handlers/karma_spec.rb
115
+ - locales/en.yml
116
+ - spec/lita/handlers/karma/chat_spec.rb
114
117
  - spec/spec_helper.rb
115
118
  homepage: https://github.com/jimmycuadra/lita-karma
116
119
  licenses:
@@ -133,10 +136,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
133
136
  version: '0'
134
137
  requirements: []
135
138
  rubyforge_project:
136
- rubygems_version: 2.2.2
139
+ rubygems_version: 2.4.5
137
140
  signing_key:
138
141
  specification_version: 4
139
142
  summary: A Lita handler for tracking karma points for arbitrary terms.
140
143
  test_files:
141
- - spec/lita/handlers/karma_spec.rb
144
+ - spec/lita/handlers/karma/chat_spec.rb
142
145
  - spec/spec_helper.rb
146
+ has_rdoc:
@@ -1,315 +0,0 @@
1
- require "lita"
2
-
3
- module Lita
4
- module Handlers
5
- # Tracks karma points for arbitrary terms.
6
- class Karma < Handler
7
- config :cooldown, types: [Integer, nil], default: 300
8
- config :link_karma_threshold, types: [Integer, nil], default: 10
9
- config :term_pattern, type: Regexp, default: /[\[\]\p{Word}\._|\{\}]{2,}/
10
- config :term_normalizer do
11
- validate do |value|
12
- "must be a callable object" unless value.respond_to?(:call)
13
- end
14
- end
15
-
16
- on :loaded, :upgrade_data
17
- on :loaded, :define_routes
18
-
19
- def upgrade_data(payload)
20
- return if redis.exists("support:reverse_links")
21
- redis.keys("links:*").each do |key|
22
- term = key.sub(/^links:/, "")
23
- redis.smembers(key).each do |link|
24
- redis.sadd("linked_to:#{link}", term)
25
- end
26
- end
27
- redis.incr("support:reverse_links")
28
- end
29
-
30
- def define_routes(payload)
31
- define_static_routes
32
- define_dynamic_routes(config.term_pattern.source)
33
- end
34
-
35
- def increment(response)
36
- modify(response, 1)
37
- end
38
-
39
- def decrement(response)
40
- modify(response, -1)
41
- end
42
-
43
- def check(response)
44
- output = []
45
-
46
- response.matches.each do |match|
47
- term = normalize_term(match[0])
48
- total_score, own_score, links = scores_for(term)
49
-
50
- string = "#{term}: #{total_score}"
51
- unless links.empty?
52
- string << " (#{own_score}), linked to: #{links.join(", ")}"
53
- end
54
- output << string
55
- end
56
-
57
- response.reply *output
58
- end
59
-
60
- def list_best(response)
61
- list(response, :zrevrange)
62
- end
63
-
64
- def list_worst(response)
65
- list(response, :zrange)
66
- end
67
-
68
- def link(response)
69
- response.matches.each do |match|
70
- term1, term2 = normalize_term(match[0]), normalize_term(match[1])
71
-
72
- if config.link_karma_threshold
73
- threshold = config.link_karma_threshold.abs
74
-
75
- _total_score, term2_score, _links = scores_for(term2)
76
- _total_score, term1_score, _links = scores_for(term1)
77
-
78
- if term1_score.abs < threshold || term2_score.abs < threshold
79
- response.reply "Terms must have less than -#{threshold} or more than #{threshold} karma to be linked or linked to."
80
- return
81
- end
82
- end
83
-
84
- if redis.sadd("links:#{term1}", term2)
85
- redis.sadd("linked_to:#{term2}", term1)
86
- response.reply "#{term2} has been linked to #{term1}."
87
- else
88
- response.reply "#{term2} is already linked to #{term1}."
89
- end
90
- end
91
- end
92
-
93
- def unlink(response)
94
- response.matches.each do |match|
95
- term1, term2 = normalize_term(match[0]), normalize_term(match[1])
96
-
97
- if redis.srem("links:#{term1}", term2)
98
- redis.srem("linked_to:#{term2}", term1)
99
- response.reply "#{term2} has been unlinked from #{term1}."
100
- else
101
- response.reply "#{term2} is not linked to #{term1}."
102
- end
103
- end
104
- end
105
-
106
- def modified(response)
107
- term = normalize_term(response.args[1])
108
-
109
- if term.empty?
110
- response.reply "Format: #{robot.name}: karma modified TERM"
111
- return
112
- end
113
-
114
- user_ids = redis.smembers("modified:#{term}")
115
-
116
- if user_ids.empty?
117
- response.reply "#{term} has never been modified."
118
- else
119
- output = user_ids.map do |id|
120
- User.find_by_id(id).name
121
- end.join(", ")
122
- response.reply output
123
- end
124
- end
125
-
126
- def delete(response)
127
- term = response.message.body.sub(/^karma delete /, "")
128
-
129
- redis.del("modified:#{term}")
130
- redis.del("links:#{term}")
131
- redis.smembers("linked_to:#{term}").each do |key|
132
- redis.srem("links:#{key}", term)
133
- end
134
- redis.del("linked_to:#{term}")
135
-
136
- if redis.zrem("terms", term)
137
- response.reply("#{term} has been deleted.")
138
- else
139
- response.reply("#{term} does not exist.")
140
- end
141
- end
142
-
143
- private
144
-
145
- def cooling_down?(term, user_id, response)
146
- ttl = redis.ttl("cooldown:#{user_id}:#{term}")
147
-
148
- if ttl >= 0
149
- cooldown_message =
150
- "You cannot modify #{term} for another #{ttl} second"
151
- cooldown_message << (ttl == 1 ? "." : "s.")
152
- response.reply cooldown_message
153
- return true
154
- else
155
- return false
156
- end
157
- end
158
-
159
- def define_dynamic_routes(pattern)
160
- self.class.route(
161
- %r{(#{pattern})\+\+},
162
- :increment,
163
- help: { "TERM++" => "Increments TERM by one." }
164
- )
165
-
166
- self.class.route(
167
- %r{(#{pattern})\-\-},
168
- :decrement,
169
- help: { "TERM--" => "Decrements TERM by one." }
170
- )
171
-
172
- self.class.route(
173
- %r{(#{pattern})~~},
174
- :check,
175
- help: { "TERM~~" => "Shows the current karma of TERM." }
176
- )
177
-
178
- self.class.route(
179
- %r{^(#{pattern})\s*\+=\s*(#{pattern})},
180
- :link,
181
- command: true,
182
- help: {
183
- "TERM1 += TERM2" => <<-HELP.chomp
184
- Links TERM2 to TERM1. TERM1's karma will then be displayed as the sum of its \
185
- own and TERM2's karma.
186
- HELP
187
- }
188
- )
189
-
190
- self.class.route(
191
- %r{^(#{pattern})\s*-=\s*(#{pattern})},
192
- :unlink,
193
- command: true,
194
- help: {
195
- "TERM1 -= TERM2" => <<-HELP.chomp
196
- Unlinks TERM2 from TERM1. TERM1's karma will no longer be displayed as the sum \
197
- of its own and TERM2's karma.
198
- HELP
199
- }
200
- )
201
- end
202
-
203
- def define_static_routes
204
- self.class.route(
205
- %r{^karma\s+worst},
206
- :list_worst,
207
- command: true,
208
- help: {
209
- "karma worst [N]" => <<-HELP.chomp
210
- Lists the bottom N terms by karma. N defaults to 5.
211
- HELP
212
- }
213
- )
214
-
215
- self.class.route(
216
- %r{^karma\s+best},
217
- :list_best,
218
- command: true,
219
- help: {
220
- "karma best [N]" => <<-HELP.chomp
221
- Lists the top N terms by karma. N defaults to 5.
222
- HELP
223
- }
224
- )
225
-
226
- self.class.route(
227
- %r{^karma\s+modified},
228
- :modified,
229
- command: true,
230
- help: {
231
- "karma modified TERM" => <<-HELP.chomp
232
- Lists the names of users who have upvoted or downvoted TERM.
233
- HELP
234
- }
235
- )
236
-
237
- self.class.route(
238
- %r{^karma\s+delete},
239
- :delete,
240
- command: true,
241
- restrict_to: :karma_admins,
242
- help: {
243
- "karma delete TERM" => <<-HELP.chomp
244
- Permanently removes TERM and all its links. TERM is matched exactly as typed \
245
- and does not adhere to the usual pattern for terms.
246
- HELP
247
- }
248
- )
249
-
250
- self.class.route(%r{^karma\s*$}, :list_best, command: true)
251
- end
252
-
253
- def modify(response, delta)
254
- response.matches.each do |match|
255
- term = normalize_term(match[0])
256
- user_id = response.user.id
257
-
258
- return if cooling_down?(term, user_id, response)
259
-
260
- redis.zincrby("terms", delta, term)
261
- redis.sadd("modified:#{term}", user_id)
262
- set_cooldown(term, response.user.id)
263
- end
264
-
265
- check(response)
266
- end
267
-
268
- def normalize_term(term)
269
- if config.term_normalizer
270
- config.term_normalizer.call(term)
271
- else
272
- term.to_s.downcase.strip
273
- end
274
- end
275
-
276
- def list(response, redis_command)
277
- n = (response.args[1] || 5).to_i - 1
278
- n = 25 if n > 25
279
-
280
- terms_scores = redis.public_send(
281
- redis_command, "terms", 0, n, with_scores: true
282
- )
283
-
284
- output = terms_scores.each_with_index.map do |term_score, index|
285
- "#{index + 1}. #{term_score[0]} (#{term_score[1].to_i})"
286
- end.join("\n")
287
-
288
- if output.length == 0
289
- response.reply "There are no terms being tracked yet."
290
- else
291
- response.reply output
292
- end
293
- end
294
-
295
- def scores_for(term)
296
- own_score = total_score = redis.zscore("terms", term).to_i
297
- links = []
298
-
299
- redis.smembers("links:#{term}").each do |link|
300
- link_score = redis.zscore("terms", link).to_i
301
- links << "#{link}: #{link_score}"
302
- total_score += link_score
303
- end
304
-
305
- [total_score, own_score, links]
306
- end
307
-
308
- def set_cooldown(term, user_id)
309
- redis.setex("cooldown:#{user_id}:#{term}", config.cooldown.to_i, 1) if config.cooldown
310
- end
311
- end
312
-
313
- Lita.register_handler(Karma)
314
- end
315
- end