lita-locker 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ module Lita
17
17
  )
18
18
 
19
19
  route(
20
- /^locker\sresource\screate\s#{RESOURCE_REGEX}$/,
20
+ /^locker\sresource\screate\s#{RESOURCES_REGEX}$/,
21
21
  :create,
22
22
  command: true,
23
23
  restrict_to: [:locker_admins],
@@ -27,7 +27,7 @@ module Lita
27
27
  )
28
28
 
29
29
  route(
30
- /^locker\sresource\sdelete\s#{RESOURCE_REGEX}$/,
30
+ /^locker\sresource\sdelete\s#{RESOURCES_REGEX}$/,
31
31
  :delete,
32
32
  command: true,
33
33
  restrict_to: [:locker_admins],
@@ -44,36 +44,66 @@ module Lita
44
44
  )
45
45
 
46
46
  def list(response)
47
- output = ''
48
- resources.each do |r|
49
- r_name = r.sub('resource_', '')
50
- res = resource(r_name)
51
- output += t('resource.desc', name: r_name, state: res['state'])
47
+ should_rate_limit = false
48
+
49
+ Resource.list.each_slice(10) do |slice|
50
+ if should_rate_limit
51
+ sleep 1
52
+ else
53
+ should_rate_limit = true
54
+ end
55
+
56
+ slice.each do |r|
57
+ res = Resource.new(r)
58
+ response.reply(t('resource.desc', name: r, state: res.state.value))
59
+ end
52
60
  end
53
- response.reply(output)
54
61
  end
55
62
 
56
63
  def create(response)
57
- name = response.matches[0][0]
58
- if create_resource(name)
59
- response.reply(t('resource.created', name: name))
60
- else
61
- response.reply(t('resource.exists', name: name))
64
+ names = response.match_data['resources'].split(/,\s*/)
65
+ results = []
66
+
67
+ names.each do |name|
68
+ if Resource.exists?(name)
69
+ results <<= t('resource.exists', name: name)
70
+ else
71
+ Resource.create(name)
72
+ results <<= t('resource.created', name: name)
73
+ end
62
74
  end
75
+
76
+ response.reply(results.join(', '))
63
77
  end
64
78
 
65
79
  def delete(response)
66
- name = response.matches[0][0]
67
- return response.reply(t('resource.does_not_exist', name: name)) unless resource_exists?(name)
68
- delete_resource(name)
69
- response.reply(t('resource.deleted', name: name))
80
+ names = response.match_data['resources'].split(/,\s*/)
81
+ results = []
82
+
83
+ names.each do |name|
84
+ if Resource.exists?(name)
85
+ Resource.delete(name)
86
+ results <<= t('resource.deleted', name: name)
87
+ else
88
+ results <<= t('resource.does_not_exist', name: name)
89
+ end
90
+ end
91
+
92
+ response.reply(results.join(', '))
70
93
  end
71
94
 
72
95
  def show(response)
73
- name = response.matches[0][0]
74
- return response.reply(t('resource.does_not_exist', name: name)) unless resource_exists?(name)
75
- r = resource(name)
76
- response.reply(t('resource.desc', name: name, state: r['state']))
96
+ name = response.match_data['resource']
97
+ return response.reply(t('resource.does_not_exist', name: name)) unless Resource.exists?(name)
98
+ r = Resource.new(name)
99
+ resp = t('resource.desc', name: name, state: r.state.value)
100
+ if r.labels.count > 0
101
+ resp += ', used by: '
102
+ r.labels.each do |label|
103
+ resp += Label.new(label).id
104
+ end
105
+ end
106
+ response.reply(resp)
77
107
  end
78
108
 
79
109
  Lita.register_handler(LockerResources)
data/lib/lita-locker.rb CHANGED
@@ -4,6 +4,9 @@ Lita.load_locales Dir[File.expand_path(
4
4
  File.join('..', '..', 'locales', '*.yml'), __FILE__
5
5
  )]
6
6
 
7
+ require 'redis-objects'
8
+ require 'time-lord'
9
+
7
10
  require 'locker/label'
8
11
  require 'locker/misc'
9
12
  require 'locker/regex'
@@ -15,3 +18,23 @@ require 'lita/handlers/locker_labels'
15
18
  require 'lita/handlers/locker_misc'
16
19
  require 'lita/handlers/locker_resources'
17
20
  require 'lita/handlers/locker'
21
+
22
+ Lita::Handlers::Locker.template_root File.expand_path(
23
+ File.join('..', '..', 'templates'),
24
+ __FILE__
25
+ )
26
+
27
+ Lita::Handlers::LockerResources.template_root File.expand_path(
28
+ File.join('..', '..', 'templates'),
29
+ __FILE__
30
+ )
31
+
32
+ Lita::Handlers::LockerLabels.template_root File.expand_path(
33
+ File.join('..', '..', 'templates'),
34
+ __FILE__
35
+ )
36
+
37
+ Lita::Handlers::LockerMisc.template_root File.expand_path(
38
+ File.join('..', '..', 'templates'),
39
+ __FILE__
40
+ )
data/lib/locker/label.rb CHANGED
@@ -2,66 +2,173 @@
2
2
  module Locker
3
3
  # Label helpers
4
4
  module Label
5
- def label(name)
6
- redis.hgetall("label_#{name}")
7
- end
5
+ # Proper Resource class
6
+ class Label
7
+ include Redis::Objects
8
8
 
9
- def labels
10
- redis.keys('label_*')
11
- end
9
+ value :state
10
+ value :owner_id
11
+ value :taken_at
12
12
 
13
- def label_exists?(name)
14
- redis.exists("label_#{name}")
15
- end
13
+ set :membership
14
+ list :wait_queue
15
+ list :journal
16
16
 
17
- def lock_label!(name, owner, time_until)
18
- return false unless label_exists?(name)
19
- key = "label_#{name}"
20
- members = label_membership(name)
21
- members.each do |m|
22
- return false unless lock_resource!(m, owner, time_until)
23
- end
24
- redis.hset(key, 'state', 'locked')
25
- redis.hset(key, 'owner_id', owner.id)
26
- redis.hset(key, 'until', time_until)
27
- true
28
- end
17
+ lock :coord, expiration: 5
18
+
19
+ attr_reader :id
29
20
 
30
- def unlock_label!(name)
31
- return false unless label_exists?(name)
32
- key = "label_#{name}"
33
- members = label_membership(name)
34
- members.each do |m|
35
- unlock_resource!(m)
21
+ def initialize(key)
22
+ fail 'Unknown label key' unless Label.exists?(key)
23
+ @id = Label.normalize(key)
36
24
  end
37
- redis.hset(key, 'state', 'unlocked')
38
- redis.hset(key, 'owner_id', '')
39
- true
40
- end
41
25
 
42
- def create_label(name)
43
- label_key = "label_#{name}"
44
- redis.hset(label_key, 'state', 'unlocked') unless
45
- resource_exists?(name) || label_exists?(name)
46
- end
26
+ def self.exists?(key)
27
+ redis.sismember('label-list', Label.normalize(key))
28
+ end
47
29
 
48
- def delete_label(name)
49
- label_key = "label_#{name}"
50
- redis.del(label_key) if label_exists?(name)
51
- end
30
+ def self.create(key)
31
+ fail 'Label key already exists' if Label.exists?(key)
32
+ redis.sadd('label-list', Label.normalize(key))
33
+ l = Label.new(key)
34
+ l.state = 'unlocked'
35
+ l.owner_id = ''
36
+ l.log('Created')
37
+ l
38
+ end
39
+
40
+ def self.delete(key)
41
+ fail 'Unknown label key' unless Label.exists?(key)
42
+ %w(state, owner_id, membership, wait_queue, journal).each do |item|
43
+ redis.del("label:#{key}:#{item}")
44
+ end
45
+ redis.srem('label-list', Label.normalize(key))
46
+ end
47
+
48
+ def self.list
49
+ redis.smembers('label-list').sort
50
+ end
51
+
52
+ def self.normalize(key)
53
+ key.strip.downcase
54
+ end
55
+
56
+ def lock!(owner_id)
57
+ if locked?
58
+ wait_queue << owner_id if wait_queue.last != owner_id
59
+ return false
60
+ end
61
+
62
+ coord_lock.lock do
63
+ membership.each do |resource_name|
64
+ r = Locker::Resource::Resource.new(resource_name)
65
+ return false if r.locked?
66
+ end
67
+ # TODO: read-modify-write cycle, not the best
68
+ membership.each do |resource_name|
69
+ r = Locker::Resource::Resource.new(resource_name)
70
+ r.lock!(owner_id)
71
+ end
72
+ self.owner_id = owner_id
73
+ self.state = 'locked'
74
+ self.taken_at = Time.now.utc
75
+ end
76
+ u = Lita::User.fuzzy_find(owner_id)
77
+ log("Locked by #{u.name}")
78
+ true
79
+ end
80
+
81
+ def unlock!
82
+ return true if state == 'unlocked'
83
+ coord_lock.lock do
84
+ self.owner_id = ''
85
+ self.state = 'unlocked'
86
+ self.taken_at = ''
87
+ membership.each do |resource_name|
88
+ r = Locker::Resource::Resource.new(resource_name)
89
+ r.unlock!
90
+ end
91
+ end
92
+ log('Unlocked')
52
93
 
53
- def label_membership(name)
54
- redis.smembers("membership_#{name}")
94
+ # FIXME: Possible race condition where resources become unavailable between unlock and relock
95
+ if wait_queue.count > 0
96
+ next_user = wait_queue.shift
97
+ self.lock!(next_user)
98
+ end
99
+ true
100
+ end
101
+
102
+ def steal!(owner_id)
103
+ log("Stolen from #{owner.id} to #{owner_id}")
104
+ wait_queue.unshift(owner_id)
105
+ self.unlock!
106
+ end
107
+
108
+ def locked?
109
+ (state == 'locked')
110
+ end
111
+
112
+ def add_resource(resource)
113
+ log("Resource #{resource.id} added")
114
+ resource.labels << id
115
+ membership << resource.id
116
+ end
117
+
118
+ def remove_resource(resource)
119
+ log("Resource #{resource.id} removed")
120
+ resource.labels.delete(id)
121
+ membership.delete(resource.id)
122
+ end
123
+
124
+ def owner
125
+ return nil unless locked?
126
+ Lita::User.find_by_id(owner_id.value)
127
+ end
128
+
129
+ def held_for
130
+ return '' unless locked?
131
+ TimeLord::Time.new(Time.parse(taken_at.value) - 1).period.to_words
132
+ end
133
+
134
+ def to_json
135
+ val = { id: id,
136
+ state: state.value,
137
+ membership: membership }
138
+
139
+ if locked?
140
+ val[:owner_id] = owner_id.value
141
+ val[:taken_at] = taken_at.value
142
+ val[:wait_queue] = wait_queue
143
+ end
144
+
145
+ val.to_json
146
+ end
147
+
148
+ def log(statement)
149
+ journal << "#{Time.now.utc}: #{statement}"
150
+ end
55
151
  end
56
152
 
57
- def add_resource_to_label(label, resource)
58
- return unless label_exists?(label) && resource_exists?(resource)
59
- redis.sadd("membership_#{label}", resource)
153
+ def label_ownership(name)
154
+ l = Label.new(name)
155
+ return label_dependencies(name) unless l.locked?
156
+ mention = l.owner.mention_name ? "(@#{l.owner.mention_name})" : ''
157
+ failed(t('label.owned_lock', name: name, owner_name: l.owner.name, mention: mention, time: l.held_for))
60
158
  end
61
159
 
62
- def remove_resource_from_label(label, resource)
63
- return unless label_exists?(label) && resource_exists?(resource)
64
- redis.srem("membership_#{label}", resource)
160
+ def label_dependencies(name)
161
+ msg = failed(t('label.dependency')) + "\n"
162
+ deps = []
163
+ l = Label.new(name)
164
+ l.membership.each do |resource_name|
165
+ resource = Locker::Resource::Resource.new(resource_name)
166
+ if resource.state.value == 'locked'
167
+ deps.push "#{resource_name} - #{resource.owner.name}"
168
+ end
169
+ end
170
+ msg += deps.join("\n")
171
+ msg
65
172
  end
66
173
  end
67
174
  end
data/lib/locker/misc.rb CHANGED
@@ -4,12 +4,27 @@ module Locker
4
4
  module Misc
5
5
  def user_locks(user)
6
6
  owned = []
7
- labels.each do |name|
8
- name.slice! 'label_'
9
- label = label(name)
10
- owned.push(name) if label['owner_id'] == user.id
7
+ Locker::Label::Label.list.each do |name|
8
+ label = Locker::Label::Label.new(name)
9
+ owned.push(name) if label.owner == user
11
10
  end
12
11
  owned
13
12
  end
13
+
14
+ def success(message)
15
+ render_template('success', string: message)
16
+ end
17
+
18
+ def failed(message)
19
+ render_template('failed', string: message)
20
+ end
21
+
22
+ def locked(message)
23
+ render_template('lock', string: message)
24
+ end
25
+
26
+ def unlocked(message)
27
+ render_template('unlock', string: message)
28
+ end
14
29
  end
15
30
  end
data/lib/locker/regex.rb CHANGED
@@ -2,11 +2,13 @@
2
2
  module Locker
3
3
  # Regex definitions
4
4
  module Regex
5
- LABEL_REGEX = /([\.\w\s-]+)/
6
- RESOURCE_REGEX = /([\.\w-]+)/
5
+ LABEL_REGEX = /(?<label>[\.\w\s-]+)(\s)?/
6
+ LABELS_REGEX = /(?<labels>[\.\w\s-]+(?:,\s*[\.\w\s-]+)*)(\s)?/
7
+ RESOURCE_REGEX = /(?<resource>[\.\w-]+)/
8
+ RESOURCES_REGEX = /(?<resources>[\.\w-]+(?:,\s*[\.\w-]+)*)/
7
9
  COMMENT_REGEX = /(\s\#.+)?/
8
10
  LOCK_REGEX = /\(lock\)\s/i
9
- USER_REGEX = /(?:@)?(?<username>[\w\s]+)/
11
+ USER_REGEX = /(?:@)?(?<username>[\w\s-]+)/
10
12
  UNLOCK_REGEX = /(?:\(unlock\)|\(release\))\s/i
11
13
  end
12
14
  end
@@ -2,47 +2,82 @@
2
2
  module Locker
3
3
  # Resource helpers
4
4
  module Resource
5
- def resource(name)
6
- redis.hgetall("resource_#{name}")
7
- end
5
+ # Proper Resource class
6
+ class Resource
7
+ include Redis::Objects
8
8
 
9
- def resources
10
- redis.keys('resource_*')
11
- end
9
+ value :state
10
+ value :owner_id
11
+ set :labels
12
12
 
13
- def resource_exists?(name)
14
- redis.exists("resource_#{name}")
15
- end
13
+ lock :coord, expiration: 5
16
14
 
17
- def lock_resource!(name, owner, time_until)
18
- return false unless resource_exists?(name)
19
- resource_key = "resource_#{name}"
20
- value = redis.hget(resource_key, 'state')
21
- return false unless value == 'unlocked'
22
- # FIXME: Race condition!
23
- redis.hset(resource_key, 'state', 'locked')
24
- redis.hset(resource_key, 'owner_id', owner.id)
25
- redis.hset(resource_key, 'until', time_until)
26
- true
27
- end
15
+ attr_reader :id
28
16
 
29
- def unlock_resource!(name)
30
- return false unless resource_exists?(name)
31
- key = "resource_#{name}"
32
- redis.hset(key, 'state', 'unlocked')
33
- redis.hset(key, 'owner_id', '')
34
- true
35
- end
17
+ def initialize(key)
18
+ fail 'Unknown resource key' unless Resource.exists?(key)
19
+ @id = key
20
+ end
36
21
 
37
- def create_resource(name)
38
- resource_key = "resource_#{name}"
39
- redis.hset(resource_key, 'state', 'unlocked') unless
40
- resource_exists?(name) || label_exists?(name)
41
- end
22
+ def self.exists?(key)
23
+ redis.sismember('resource-list', key)
24
+ end
25
+
26
+ def self.create(key)
27
+ fail 'Resource key already exists' if Resource.exists?(key)
28
+ redis.sadd('resource-list', key)
29
+ r = Resource.new(key)
30
+ r.state = 'unlocked'
31
+ r.owner_id = ''
32
+ r
33
+ end
34
+
35
+ def self.delete(key)
36
+ fail 'Unknown resource key' unless Resource.exists?(key)
37
+ %w(state, owner_id).each do |item|
38
+ redis.del("resource:#{key}:#{item}")
39
+ end
40
+ redis.srem('resource-list', key)
41
+ end
42
+
43
+ def self.list
44
+ redis.smembers('resource-list').sort
45
+ end
46
+
47
+ def lock!(owner_id)
48
+ return false if state == 'locked'
49
+ coord_lock.lock do
50
+ self.owner_id = owner_id
51
+ self.state = 'locked'
52
+ end
53
+ true
54
+ end
55
+
56
+ def unlock!
57
+ return true if state == 'unlocked'
58
+ coord_lock.lock do
59
+ self.owner_id = ''
60
+ self.state = 'unlocked'
61
+ end
62
+ true
63
+ end
64
+
65
+ def locked?
66
+ (state == 'locked')
67
+ end
68
+
69
+ def owner
70
+ return nil unless locked?
71
+ Lita::User.find_by_id(owner_id.value)
72
+ end
42
73
 
43
- def delete_resource(name)
44
- resource_key = "resource_#{name}"
45
- redis.del(resource_key) if resource_exists?(name)
74
+ def to_json
75
+ {
76
+ id: id,
77
+ state: state.value,
78
+ owner_id: owner_id.value
79
+ }.to_json
80
+ end
46
81
  end
47
82
  end
48
83
  end
data/lita-locker.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = 'lita-locker'
3
- spec.version = '0.7.0'
3
+ spec.version = '1.0.0'
4
4
  spec.authors = ['Eric Sigler']
5
5
  spec.email = ['me@esigler.com']
6
6
  spec.description = '"lock" and "unlock" arbitrary subjects'
@@ -10,11 +10,13 @@ Gem::Specification.new do |spec|
10
10
  spec.metadata = { 'lita_plugin_type' => 'handler' }
11
11
 
12
12
  spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
13
- spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
14
- spec.test_files = spec.files.grep(/^(test|spec|features)\//)
13
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
14
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
15
  spec.require_paths = ['lib']
16
16
 
17
- spec.add_runtime_dependency 'lita', '>= 4.0.1'
17
+ spec.add_runtime_dependency 'lita', '>= 4.2'
18
+ spec.add_runtime_dependency 'redis-objects'
19
+ spec.add_runtime_dependency 'time-lord'
18
20
 
19
21
  spec.add_development_dependency 'bundler', '~> 1.3'
20
22
  spec.add_development_dependency 'coveralls'
@@ -22,4 +24,7 @@ Gem::Specification.new do |spec|
22
24
  spec.add_development_dependency 'rspec', '>= 3.0.0'
23
25
  spec.add_development_dependency 'rubocop'
24
26
  spec.add_development_dependency 'simplecov'
27
+
28
+ spec.post_install_message = 'After upgrading to lita-locker 1.x, you should read: ' \
29
+ 'https://github.com/esigler/lita-locker/blob/master/UPGRADING.md'
25
30
  end
data/locales/en.yml CHANGED
@@ -7,6 +7,12 @@ en:
7
7
  already_unlocked: "%{label} was already unlocked"
8
8
  self: Why are you stealing the lock from yourself?
9
9
  help:
10
+ log:
11
+ syntax: locker log <label>
12
+ desc: Show up to the last 10 activity log entries for <label>
13
+ dequeue:
14
+ syntax: locker dequeue <label>
15
+ desc: Remove yourself from the queue for a label
10
16
  lock:
11
17
  syntax: lock <subject>
12
18
  desc: Make something unavailable to others. Can have # comments afterwards.
@@ -27,11 +33,11 @@ en:
27
33
  syntax: locker resource list
28
34
  desc: List all resources
29
35
  create:
30
- syntax: locker resource create <name>
31
- desc: Create a resource with <name>
36
+ syntax: "locker resource create <name>[, <name> ...]"
37
+ desc: Create resource(s) with each <name>
32
38
  delete:
33
- syntax: locker resource delete <name>
34
- desc: Delete the resource with <name>
39
+ syntax: "locker resource delete <name>[, <name> ...]"
40
+ desc: Delete the resource(s) with each <name>
35
41
  show:
36
42
  syntax: locker resource show <name>
37
43
  desc: Show the state of <name>
@@ -40,20 +46,20 @@ en:
40
46
  syntax: locker label list
41
47
  desc: List all labels
42
48
  create:
43
- syntax: locker label create <name>
44
- desc: Create a label with <name>
49
+ syntax: "locker label create <name>[, <name> ...]"
50
+ desc: Create label(s) with each <name>
45
51
  delete:
46
- syntax: locker label delete <name>
47
- desc: Delete the label with <name>
52
+ syntax: "locker label delete <name>[, <name> ...]"
53
+ desc: Delete the label(s) with each <name>
48
54
  show:
49
55
  syntax: locker label show <name>
50
56
  desc: Show all resources for <name>
51
57
  add:
52
- syntax: locker label add <resource> to <name>
53
- desc: Adds <resource> to the list of things to lock/unlock for <name>
58
+ syntax: "locker label add <resource>[, <resource> ...] to <name>"
59
+ desc: Adds each <resource> to the list of things to lock/unlock for <name>
54
60
  remove:
55
- syntax: locker label remove <resource> from <name>
56
- desc: Removes <resource> from <name>
61
+ syntax: "locker label remove <resource>[, <resource> ...] from <name>"
62
+ desc: Removes each <resource> from <name>
57
63
  resource:
58
64
  created: "Resource %{name} created"
59
65
  desc: "Resource: %{name}, state: %{state}"
@@ -69,14 +75,17 @@ en:
69
75
  subject:
70
76
  does_not_exist: "Sorry, that does not exist"
71
77
  label:
78
+ log_entry: "%{entry}"
79
+ self_lock: "You already have the lock on %{name}"
72
80
  unlock: "%{name} unlocked"
73
- owned: "%{name} is locked by %{owner_name}"
74
- owned_mention: "%{name} is locked by %{owner_name} (@%{owner_mention})"
81
+ owned_lock: "%{name} is locked by %{owner_name} %{mention} (taken %{time}), you have been added to the queue, type 'locker dequeue %{name}' to be removed"
82
+ owned_unlock: "%{name} is locked by %{owner_name} %{mention} (taken %{time})"
75
83
  is_unlocked: "%{name} is unlocked"
76
84
  unable_to_lock: "%{name} unable to be locked"
77
85
  lock: "%{name} locked"
78
- desc: "Label: %{name}, state: %{state}"
79
- desc_owner: "Label: %{name}, state: %{state}, owner: %{owner_name}"
86
+ desc: "%{name} is unlocked"
87
+ desc_owner: "%{name} is locked by %{owner_name} (taken %{time})"
88
+ desc_owner_queue: "%{name} is locked by %{owner_name} (taken %{time}). Next up: %{queue}"
80
89
  created: "Label %{name} created"
81
90
  exists: "%{name} already exists"
82
91
  deleted: "Label %{name} deleted"
@@ -88,3 +97,9 @@ en:
88
97
  does_not_have_resource: "Label %{label} does not have Resource %{resource}"
89
98
  no_resources: "%{name} has no resources, so it cannot be locked"
90
99
  dependency: 'Label unable to be locked, blocked on:'
100
+ now_locked_by: "%{name} now locked by %{owner} %{mention}"
101
+ removed_from_queue: "You have been removed from the queue for %{name}"
102
+ unknown_in_queue: "You weren't in the queue for %{name}"
103
+ user:
104
+ unknown: Unknown user
105
+ no_active_locks: That user has no active locks