lita-conferenz 1.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b4b5b603523ab6945ca312403138be66d58bb927
4
+ data.tar.gz: 94583855cb7b7a9a8545ece100e7db0b8202f8bf
5
+ SHA512:
6
+ metadata.gz: 6e3089bc86479ea11ee2da7a1b1ba80594883bd9b443481ae74d6cb482426f30b6d6741cf4d788b0c19672b4851bb98c1d069858e440666953a0b54e99ba2454
7
+ data.tar.gz: b54af757417eff2d669d94fe99b6b4e2092375d9c78a3bcb69a801c4ec7d2cf94eac855d5ed1cd6d76356dd63c0b2776968c071e039a814d6d3951f14eb5c355
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ vendor/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,11 @@
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/conferenz/chat"
10
+ require 'lita/handlers/conferenz/config'
11
+ require 'lita/handlers/conferenz/term'
@@ -0,0 +1,205 @@
1
+ module Lita::Handlers::Conferenz
2
+ class Chat < Lita::Handler
3
+ namespace "conferenz"
4
+
5
+ on :loaded, :define_routes
6
+
7
+ def define_routes(payload)
8
+ define_static_routes
9
+ define_dynamic_routes(config.term_pattern.source)
10
+ end
11
+
12
+ def increment(response)
13
+ modify(response, :increment)
14
+ end
15
+
16
+ def decrement(response)
17
+ modify(response, :decrement)
18
+ end
19
+
20
+ def check(response)
21
+ seen = Set.new
22
+
23
+ output = response.matches.map do |match|
24
+ term = get_term(match[0])
25
+ next if seen.include?(term)
26
+ seen << term
27
+ term.check
28
+ end.compact
29
+
30
+ response.reply output.join("; ")
31
+ end
32
+
33
+ def list_best(response)
34
+ list(response, :list_best)
35
+ end
36
+
37
+ def list_worst(response)
38
+ list(response, :list_worst)
39
+ end
40
+
41
+ def link(response)
42
+ response.matches.each do |match|
43
+ term1 = get_term(match[0])
44
+ term2 = get_term(match[1])
45
+
46
+ result = term1.link(term2)
47
+
48
+ case result
49
+ when Integer
50
+ response.reply t("threshold_not_satisfied", threshold: result)
51
+ when true
52
+ response.reply t("link_success", source: term2, target: term1)
53
+ else
54
+ response.reply t("already_linked", source: term2, target: term1)
55
+ end
56
+ end
57
+ end
58
+
59
+ def unlink(response)
60
+ response.matches.each do |match|
61
+ term1 = get_term(match[0])
62
+ term2 = get_term(match[1])
63
+
64
+ if term1.unlink(term2)
65
+ response.reply t("unlink_success", source: term2, target: term1)
66
+ else
67
+ response.reply t("already_unlinked", source: term2, target: term1)
68
+ end
69
+ end
70
+ end
71
+
72
+ def modified(response)
73
+ term = get_term(response.args[1])
74
+
75
+ users = term.modified
76
+
77
+ if users.empty?
78
+ response.reply t("never_modified", term: term)
79
+ else
80
+ response.reply users.map(&:name).join(", ")
81
+ end
82
+ end
83
+
84
+ def delete(response)
85
+ term = Term.new(robot, response.message.body.sub(/^conferenz delete /, ""), normalize: false)
86
+
87
+ if term.delete
88
+ response.reply t("delete_success", term: term)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def define_dynamic_routes(pattern)
95
+ self.class.route(
96
+ %r{(#{pattern})\+\+#{token_terminator.source}},
97
+ :increment,
98
+ help: { t("help.increment_key") => t("help.increment_value") }
99
+ )
100
+
101
+ self.class.route(
102
+ %r{(#{pattern})--#{token_terminator.source}},
103
+ :decrement,
104
+ help: { t("help.decrement_key") => t("help.decrement_value") }
105
+ )
106
+
107
+ self.class.route(
108
+ %r{(#{pattern})~~#{token_terminator.source}},
109
+ :check,
110
+ help: { t("help.check_key") => t("help.check_value") }
111
+ )
112
+
113
+ self.class.route(
114
+ %r{^(#{pattern})\s*\+=\s*(#{pattern})(?:\+\+|--|~~)?#{token_terminator.source}},
115
+ :link,
116
+ command: true,
117
+ help: { t("help.link_key") => t("help.link_value") }
118
+ )
119
+
120
+ self.class.route(
121
+ %r{^(#{pattern})\s*-=\s*(#{pattern})(?:\+\+|--|~~)?#{token_terminator.source}},
122
+ :unlink,
123
+ command: true,
124
+ help: { t("help.unlink_key") => t("help.unlink_value") }
125
+ )
126
+ end
127
+
128
+ def define_static_routes
129
+ self.class.route(
130
+ %r{^conferenz\s+worst},
131
+ :list_worst,
132
+ command: true,
133
+ help: { t("help.list_worst_key") => t("help.list_worst_value") }
134
+ )
135
+
136
+ self.class.route(
137
+ %r{^conferenz\s+best},
138
+ :list_best,
139
+ command: true,
140
+ help: { t("help.list_best_key") => t("help.list_best_value") }
141
+ )
142
+
143
+ self.class.route(
144
+ %r{^conferenz\s+modified\s+.+},
145
+ :modified,
146
+ command: true,
147
+ help: { t("help.modified_key") => t("help.modified_value") }
148
+ )
149
+
150
+ self.class.route(
151
+ %r{^conferenz\s+delete},
152
+ :delete,
153
+ command: true,
154
+ restrict_to: :conferenz_admins,
155
+ help: { t("help.delete_key") => t("help.delete_value") }
156
+ )
157
+
158
+ self.class.route(%r{^conferenz\s*$}, :list_best, command: true)
159
+ end
160
+
161
+ def determine_list_count(response)
162
+ n = (response.args[1] || 5).to_i - 1
163
+ n = 25 if n > 25
164
+ n
165
+ end
166
+
167
+ def get_term(term)
168
+ Term.new(robot, term)
169
+ end
170
+
171
+ def list(response, method_name)
172
+ terms_and_scores = Term.public_send(method_name, robot, determine_list_count(response))
173
+
174
+ output = terms_and_scores.each_with_index.map do |term_and_score, index|
175
+ "#{index + 1}. #{term_and_score[0]} (#{term_and_score[1].to_i})"
176
+ end.join("\n")
177
+
178
+ if output.empty?
179
+ response.reply t("no_terms")
180
+ else
181
+ response.reply output
182
+ end
183
+ end
184
+
185
+ def modify(response, method_name)
186
+ user = response.user
187
+
188
+ output = response.matches.map do |match|
189
+ get_term(match[0]).public_send(method_name, user)
190
+ end
191
+
192
+ response.reply output.join("; ")
193
+ end
194
+
195
+ # To ensure that constructs like foo++bar or foo--bar (the latter is
196
+ # common in some URL generation schemes) do not cause errant conferenz
197
+ # modifications, force conferenz tokens be followed by whitespace (in a zero-
198
+ # width, look-ahead operator) or the end of the string.
199
+ def token_terminator
200
+ %r{(?:(?=\s)|$)}
201
+ end
202
+ end
203
+ end
204
+
205
+ Lita.register_handler(Lita::Handlers::Conferenz::Chat)
@@ -0,0 +1,20 @@
1
+ module Lita::Handlers::Conferenz
2
+ class Config < Lita::Handler
3
+ namespace "conferenz"
4
+
5
+ default_term_normalizer = proc do |term|
6
+ term.to_s.downcase.strip
7
+ end
8
+
9
+ config :cooldown, types: [Integer, nil], default: 0
10
+ config :link_conferenz_threshold, types: [Integer, nil], default: 0
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::Conferenz::Config)
@@ -0,0 +1,153 @@
1
+ module Lita::Handlers::Conferenz
2
+ class Term
3
+ include Lita::Handler::Common
4
+
5
+ namespace "conferenz"
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
+ alias_method :==, :eql?
63
+
64
+ def hash
65
+ term.hash
66
+ end
67
+
68
+ def increment(user)
69
+ modify(user, 1)
70
+ end
71
+
72
+ def link(other)
73
+ if config.link_conferenz_threshold
74
+ threshold = config.link_conferenz_threshold.abs
75
+
76
+ if own_score.abs < threshold || other.own_score.abs < threshold
77
+ return threshold
78
+ end
79
+ end
80
+
81
+ redis.sadd("links:#{self}", other.to_s) && redis.sadd("linked_to:#{other}", to_s)
82
+ end
83
+
84
+ def links
85
+ @links ||= begin
86
+ redis.smembers("links:#{self}").each do |term|
87
+ linked_term = self.class.new(robot, term)
88
+ @link_cache[linked_term.term] = linked_term
89
+ end
90
+ end
91
+ end
92
+
93
+ def links_with_scores
94
+ @links_with_scores ||= begin
95
+ {}.tap do |h|
96
+ links.each do |link|
97
+ h[link] = @link_cache[link].own_score
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def modified
104
+ redis.smembers("modified:#{term}").map do |user_id|
105
+ Lita::User.find_by_id(user_id)
106
+ end
107
+ end
108
+
109
+ def own_score
110
+ @own_score ||= redis.zscore("terms", term).to_i
111
+ end
112
+
113
+ def to_s
114
+ term
115
+ end
116
+
117
+ def total_score
118
+ @total_score ||= begin
119
+ links.inject(own_score) do |memo, linked_term|
120
+ memo + @link_cache[linked_term].own_score
121
+ end
122
+ end
123
+ end
124
+
125
+ def unlink(other)
126
+ redis.srem("links:#{self}", other.to_s) && redis.srem("linked_to:#{other}", to_s)
127
+ end
128
+
129
+ private
130
+
131
+ def modify(user, delta)
132
+ ttl = redis.ttl("cooldown:#{user.id}:#{term}")
133
+
134
+ if ttl > 0
135
+ t("cooling_down", term: self, ttl: ttl, count: ttl)
136
+ else
137
+ modify!(user, delta)
138
+ end
139
+ end
140
+
141
+ def modify!(user, delta)
142
+ user_id = user.id
143
+ redis.zincrby("terms", delta, term)
144
+ redis.sadd("modified:#{self}", user_id)
145
+ redis.setex("cooldown:#{user_id}:#{self}", config.cooldown, 1) if config.cooldown
146
+ check
147
+ end
148
+
149
+ def normalize_term(term)
150
+ config.term_normalizer.call(term)
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "lita-conferenz"
3
+ spec.version = "1.0.1"
4
+ spec.authors = ["Piotr Limanowski"]
5
+ spec.email = ["plimanowski+conferenz@gmail.com"]
6
+ spec.description = %q{A Lita handler for tracking conferences attendance.}
7
+ spec.summary = %q{A Lita handler for tracking conferences attendance.}
8
+ spec.homepage = "https://github.com/peel/conferenz"
9
+ spec.license = "MIT"
10
+ spec.metadata = { "lita_plugin_type" => "handler" }
11
+
12
+ spec.files = `git ls-files`.split($/)
13
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
14
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
+ spec.require_paths = ["lib"]
16
+
17
+ spec.add_runtime_dependency "lita", ">= 4.0"
18
+
19
+ spec.add_development_dependency "bundler", "~> 1.3"
20
+ spec.add_development_dependency "rake"
21
+ spec.add_development_dependency "rspec", ">= 3.0.0"
22
+ spec.add_development_dependency "simplecov"
23
+ spec.add_development_dependency "coveralls"
24
+ end
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}."
@@ -0,0 +1,339 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "spec_helper"
3
+
4
+ describe Lita::Handlers::Conferenz::Chat, lita_handler: true do
5
+ let(:payload) { double("payload") }
6
+
7
+ prepend_before { registry.register_handler(Lita::Handlers::Conferenz::Config) }
8
+
9
+ before do
10
+ registry.config.handlers.conferenz.cooldown = nil
11
+ registry.config.handlers.conferenz.link_karma_threshold = nil
12
+ described_class.routes.clear
13
+ subject.define_routes(payload)
14
+ end
15
+
16
+ it { is_expected.to route("foo++").to(:increment) }
17
+ it { is_expected.to route("foo--").to(:decrement) }
18
+ it { is_expected.to route("foo++ bar").to(:increment) }
19
+ it { is_expected.to route("foo-- bar").to(:decrement) }
20
+ it { is_expected.to route("foo~~").to(:check) }
21
+ it { is_expected.to route_command("conferenz best").to(:list_best) }
22
+ it { is_expected.to route_command("conferenz worst").to(:list_worst) }
23
+ it { is_expected.to route_command("conferenz modified foo").to(:modified) }
24
+ it do
25
+ is_expected.to route_command("conferenz delete").with_authorization_for(:karma_admins).to(:delete)
26
+ end
27
+ it { is_expected.to route_command("conferenz").to(:list_best) }
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) }
31
+ it { is_expected.to route_command("foo += bar~~").to(:link) }
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) }
35
+ it { is_expected.to route_command("foo -= bar~~").to(:unlink) }
36
+ it { is_expected.not_to route("+++++").to(:increment) }
37
+ it { is_expected.not_to route("-----").to(:decrement) }
38
+ it { is_expected.not_to route("foo++bar").to(:increment) }
39
+ it { is_expected.not_to route("foo--bar").to(:decrement) }
40
+
41
+ describe "#increment" do
42
+ it "increases the term's score by one and says the new score" do
43
+ send_message("foo++")
44
+ expect(replies.last).to eq("foo: 1")
45
+ end
46
+
47
+ it "matches multiple terms in one message" do
48
+ send_message("foo++ bar++")
49
+ expect(replies.last).to eq("foo: 1; bar: 1")
50
+ end
51
+
52
+ it "doesn't start from zero if the term already has a positive score" do
53
+ send_message("foo++")
54
+ send_message("foo++")
55
+ expect(replies.last).to eq("foo: 2")
56
+ end
57
+
58
+ it "replies with a warning if term increment is on cooldown" do
59
+ registry.config.handlers.conferenz.cooldown = 10
60
+ send_message("foo++")
61
+ send_message("foo++")
62
+ expect(replies.last).to match(/cannot modify foo/)
63
+ end
64
+
65
+ it "is case insensitive" do
66
+ send_message("foo++")
67
+ send_message("FOO++")
68
+ expect(replies.last).to eq("foo: 2")
69
+ end
70
+
71
+ it "handles Unicode word characters" do
72
+ send_message("föö++")
73
+ expect(replies.last).to eq("föö: 1")
74
+ end
75
+ end
76
+
77
+ describe "#decrement" do
78
+ it "decreases the term's score by one and says the new score" do
79
+ send_message("foo--")
80
+ expect(replies.last).to eq("foo: -1")
81
+ end
82
+
83
+ it "matches multiple terms in one message" do
84
+ send_message("foo-- bar--")
85
+ expect(replies.last).to eq("foo: -1; bar: -1")
86
+ end
87
+
88
+ it "doesn't start from zero if the term already has a positive score" do
89
+ send_message("foo++")
90
+ send_message("foo--")
91
+ expect(replies.last).to eq("foo: 0")
92
+ end
93
+
94
+ it "replies with a warning if term increment is on cooldown" do
95
+ registry.config.handlers.conferenz.cooldown = 10
96
+ send_message("foo--")
97
+ send_message("foo--")
98
+ expect(replies.last).to match(/cannot modify foo/)
99
+ end
100
+ end
101
+
102
+ describe "#check" do
103
+ it "says the term's current score" do
104
+ send_message("foo~~")
105
+ expect(replies.last).to eq("foo: 0")
106
+ end
107
+
108
+ it "matches multiple terms in one message" do
109
+ send_message("foo~~ bar~~")
110
+ expect(replies.last).to eq("foo: 0; bar: 0")
111
+ end
112
+
113
+ it "doesn't match the same term multiple times in one message" do
114
+ send_message("foo~~ foo~~")
115
+ expect(replies.size).to eq(1)
116
+ end
117
+ end
118
+
119
+ describe "#list" do
120
+ it "replies with a warning if there are no terms" do
121
+ send_command("conferenz")
122
+ expect(replies.last).to match(/no terms being tracked/)
123
+ end
124
+
125
+ context "with modified terms" do
126
+ before do
127
+ send_message(
128
+ "one++ one++ one++ two++ two++ three++ four++ four-- five--"
129
+ )
130
+ end
131
+
132
+ it "lists the top 5 terms by default" do
133
+ send_command("conferenz")
134
+ expect(replies.last).to eq <<-MSG.chomp
135
+ 1. one (3)
136
+ 2. two (2)
137
+ 3. three (1)
138
+ 4. four (0)
139
+ 5. five (-1)
140
+ MSG
141
+ end
142
+
143
+ it 'lists the bottom 5 terms when passed "worst"' do
144
+ send_command("conferenz worst")
145
+ expect(replies.last).to eq <<-MSG.chomp
146
+ 1. five (-1)
147
+ 2. four (0)
148
+ 3. three (1)
149
+ 4. two (2)
150
+ 5. one (3)
151
+ MSG
152
+ end
153
+
154
+ it "limits the list to the count passed as the second argument" do
155
+ send_command("conferenz best 2")
156
+ expect(replies.last).to eq <<-MSG.chomp
157
+ 1. one (3)
158
+ 2. two (2)
159
+ MSG
160
+ end
161
+ end
162
+ end
163
+
164
+ describe "#link" do
165
+ it "says that it's linked term 2 to term 1" do
166
+ send_command("foo += bar")
167
+ expect(replies.last).to eq("bar has been linked to foo.")
168
+ end
169
+
170
+ it "says that term 2 was already linked to term 1 if it was" do
171
+ send_command("foo += bar")
172
+ send_command("foo += bar")
173
+ expect(replies.last).to eq("bar is already linked to foo.")
174
+ end
175
+
176
+ it "causes term 1's score to be modified by term 2's" do
177
+ send_message("foo++ bar++ baz++")
178
+ send_command("foo += bar")
179
+ send_command("foo += baz")
180
+ send_message("foo~~")
181
+ expect(replies.last).to match(
182
+ /foo: 3 \(1\), linked to: ba[rz]: 1, ba[rz]: 1/
183
+ )
184
+ end
185
+
186
+ context "when link_karma_threshold is set" do
187
+ before do
188
+ registry.config.handlers.conferenz.link_karma_threshold = 1
189
+ end
190
+
191
+ it "doesn't allow a term to be linked if both are below the threshold" do
192
+ send_command("foo += bar")
193
+ expect(replies.last).to include("must have less than")
194
+ end
195
+
196
+ it "doesn't allow a term to be linked if it's below the threshold" do
197
+ send_command("foo++")
198
+ send_command("foo += bar")
199
+ expect(replies.last).to include("must have less than")
200
+ end
201
+
202
+ it "doesn't allow a term to be linked to another term below the threshold" do
203
+ send_command("bar++")
204
+ send_command("foo += bar")
205
+ expect(replies.last).to include("must have less than")
206
+ end
207
+
208
+ it "allows links if both terms meet the threshold" do
209
+ send_command("foo++ bar++")
210
+ send_command("foo += bar")
211
+ expect(replies.last).to include("has been linked")
212
+ send_command("bar += foo")
213
+ expect(replies.last).to include("has been linked")
214
+ end
215
+
216
+ it "uses the absolute value for terms with negative karma" do
217
+ send_command("foo-- bar--")
218
+ send_command("foo += bar")
219
+ expect(replies.last).to include("has been linked")
220
+ send_command("bar += foo")
221
+ expect(replies.last).to include("has been linked")
222
+ end
223
+ end
224
+ end
225
+
226
+ describe "#unlink" do
227
+ it "says that it's unlinked term 2 from term 1" do
228
+ send_command("foo += bar")
229
+ send_command("foo -= bar")
230
+ expect(replies.last).to eq("bar has been unlinked from foo.")
231
+ end
232
+
233
+ it "says that term 2 was not linked to term 1 if it wasn't" do
234
+ send_command("foo -= bar")
235
+ expect(replies.last).to eq("bar is not linked to foo.")
236
+ end
237
+
238
+ it "causes term 1's score to stop being modified by term 2's" do
239
+ send_message("foo++ bar++")
240
+ send_command("foo += bar")
241
+ send_command("foo -= bar")
242
+ send_message("foo~~")
243
+ expect(replies.last).to eq("foo: 1")
244
+ end
245
+ end
246
+
247
+ describe "#modified" do
248
+ it "replies with a message if the term hasn't been modified" do
249
+ send_command("conferenz modified foo")
250
+ expect(replies.last).to match(/never been modified/)
251
+ end
252
+
253
+ it "lists users who have modified the given term in count order" do
254
+ other_user = Lita::User.create("2", name: "Other User")
255
+ send_message("foo++", as: user)
256
+ send_message("foo++", as: user)
257
+ send_message("foo++", as: other_user)
258
+ send_command("conferenz modified foo")
259
+ expect(replies.last).to eq("#{user.name}, #{other_user.name}")
260
+ end
261
+ end
262
+
263
+ describe "#delete" do
264
+ before do
265
+ robot.auth.add_user_to_group!(user, :karma_admins)
266
+ end
267
+
268
+ it "deletes the term" do
269
+ send_message("foo++")
270
+ send_command("conferenz delete foo")
271
+ expect(replies.last).to eq("foo has been deleted.")
272
+ send_message("foo~~")
273
+ expect(replies.last).to eq("foo: 0")
274
+ end
275
+
276
+ it "matches terms exactly, including leading whitespace" do
277
+ term = " 'foo bar* 'baz''/ :"
278
+ subject.redis.zincrby("terms", 1, term)
279
+ send_command("conferenz delete #{term}")
280
+ expect(replies.last).to include("has been deleted")
281
+ end
282
+
283
+ it "clears the modification list" do
284
+ send_message("foo++")
285
+ send_command("conferenz delete foo")
286
+ send_command("conferenz modified foo")
287
+ expect(replies.last).to eq("foo has never been modified.")
288
+ end
289
+
290
+ it "clears the deleted term's links" do
291
+ send_command("foo += bar")
292
+ send_command("foo += baz")
293
+ send_command("conferenz delete foo")
294
+ send_message("foo++")
295
+ expect(replies.last).to eq("foo: 1")
296
+ end
297
+
298
+ it "clears links from other terms connected to the deleted term" do
299
+ send_command("bar += foo")
300
+ send_command("baz += foo")
301
+ send_command("conferenz delete foo")
302
+ send_message("bar++")
303
+ expect(replies.last).to eq("bar: 1")
304
+ send_message("baz++")
305
+ expect(replies.last).to eq("baz: 1")
306
+ end
307
+ end
308
+
309
+ describe "custom term patterns and normalization" do
310
+ before do
311
+ registry.config.handlers.conferenz.term_pattern = /[<:]([^>:]+)[>:]/
312
+ registry.config.handlers.conferenz.term_normalizer = lambda do |term|
313
+ term.to_s.downcase.strip.sub(/[<:]([^>:]+)[>:]/, '\1')
314
+ end
315
+ described_class.routes.clear
316
+ subject.define_routes(payload)
317
+ end
318
+
319
+ it "increments multi-word terms bounded by delimeters" do
320
+ send_message(":Some Thing:++")
321
+ expect(replies.last).to eq("some thing: 1")
322
+ end
323
+
324
+ it "increments terms with symbols that are bounded by delimeters" do
325
+ send_message("<C++>++")
326
+ expect(replies.last).to eq("c++: 1")
327
+ end
328
+
329
+ it "decrements multi-word terms bounded by delimeters" do
330
+ send_message(":Some Thing:--")
331
+ expect(replies.last).to eq("some thing: -1")
332
+ end
333
+
334
+ it "checks multi-word terms bounded by delimeters" do
335
+ send_message(":Some Thing:~~")
336
+ expect(replies.last).to eq("some thing: 0")
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,12 @@
1
+ require "simplecov"
2
+ require "coveralls"
3
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
4
+ SimpleCov::Formatter::HTMLFormatter,
5
+ Coveralls::SimpleCov::Formatter
6
+ ]
7
+ SimpleCov.start { add_filter "/spec/" }
8
+
9
+ require "lita-conferenz"
10
+ require "lita/rspec"
11
+
12
+ Lita.version_3_compatibility_mode = false
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lita-conferenz
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Limanowski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lita
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: coveralls
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: A Lita handler for tracking conferences attendance.
98
+ email:
99
+ - plimanowski+conferenz@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - Gemfile
106
+ - Rakefile
107
+ - lib/lita-conferenz.rb
108
+ - lib/lita/handlers/conferenz/chat.rb
109
+ - lib/lita/handlers/conferenz/config.rb
110
+ - lib/lita/handlers/conferenz/term.rb
111
+ - lita-conferenz.gemspec
112
+ - locales/en.yml
113
+ - spec/lita/handlers/conferenz/chat_spec.rb
114
+ - spec/spec_helper.rb
115
+ homepage: https://github.com/peel/conferenz
116
+ licenses:
117
+ - MIT
118
+ metadata:
119
+ lita_plugin_type: handler
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 2.4.8
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: A Lita handler for tracking conferences attendance.
140
+ test_files:
141
+ - spec/lita/handlers/conferenz/chat_spec.rb
142
+ - spec/spec_helper.rb