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 +4 -4
- data/README.md +22 -6
- data/lib/lita-karma.rb +11 -1
- data/lib/lita/handlers/karma/chat.rb +206 -0
- data/lib/lita/handlers/karma/config.rb +20 -0
- data/lib/lita/handlers/karma/term.rb +152 -0
- data/lita-karma.gemspec +1 -1
- data/locales/en.yml +40 -0
- data/spec/lita/handlers/{karma_spec.rb → karma/chat_spec.rb} +28 -41
- metadata +10 -6
- data/lib/lita/handlers/karma.rb +0 -315
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1e03d87e444b488534df84e8c3d16a8f02f6c82
|
4
|
+
data.tar.gz: 45ad68d97c0bb3b3e172a1334c8d4ad5e29af53f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
*
|
22
|
-
*
|
23
|
-
*
|
24
|
-
*
|
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
|
-
|
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
|
```
|
data/lib/lita-karma.rb
CHANGED
@@ -1 +1,11 @@
|
|
1
|
-
require "
|
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
|
data/lita-karma.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = "lita-karma"
|
3
|
-
spec.version = "3.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.}
|
data/locales/en.yml
ADDED
@@ -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
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
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.
|
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:
|
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
|
-
-
|
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.
|
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/
|
144
|
+
- spec/lita/handlers/karma/chat_spec.rb
|
142
145
|
- spec/spec_helper.rb
|
146
|
+
has_rdoc:
|
data/lib/lita/handlers/karma.rb
DELETED
@@ -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
|