lita-locker 0.7.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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