ferris-bueller 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c5cd0bb225b443bbd5cd93bb39a5c36b216e61bc
4
- data.tar.gz: ac947c90b3c291b59e71caf4ba15765da8c47d7c
3
+ metadata.gz: e42b63a6a4a472e0a57bf203fd2b40df207691f0
4
+ data.tar.gz: e17b58dd6390318e6540015e1d5ead60a8b5fd27
5
5
  SHA512:
6
- metadata.gz: 8094033f7c2e10ac62d306e307ae89b47783956d51f1885b61f94e20bfe37cb464bd95950a1ae9d56c98e2e656366a5ea034629429af82aa6f01c2219b87819e
7
- data.tar.gz: 6573073a948f2cf9386afe6d2fc374289be838e4d1e6db263d5dfce039f7b138a44bbcf3a7619e6b3b999346aa6e49d5fc99f4d976b9caa4c86e37408c080c88
6
+ metadata.gz: 5e63ebf4b536ae282d08365cd580796e843a89685d53ef498f936bee99e592fd04a484cd6a9398296882ff17b3602a0eb373e0bc991dc9c379e4359d17f39a84
7
+ data.tar.gz: ae7e81ccd67840d08b36d0855fd7adcfb441e9249d7ede8916f8fb0f148e4722e6b208ce6c0dfe81e676e5d454735856b9a44eded1cafbf5c140c178e0305731
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.2
1
+ 0.0.3
@@ -6,6 +6,7 @@ require 'queryparams'
6
6
 
7
7
  require_relative 'constants'
8
8
  require_relative 'web'
9
+ require_relative 'jira_api'
9
10
  require_relative 'slack_api'
10
11
 
11
12
 
@@ -25,6 +26,15 @@ module FerrisBueller
25
26
  Web.set :logger, log
26
27
  Web.set :verification_token, options.verification_token
27
28
  Web.set :api, SlackAPI.new(token: options.api_token, logger: log)
29
+ Web.set :jira, JiraAPI.new(
30
+ api_url: options.jira_url,
31
+ user: options.jira_user,
32
+ pass: options.jira_pass,
33
+ logger: log
34
+ )
35
+ Web.set :jira_project, options.jira_project
36
+ Web.set :jira_type, options.jira_type
37
+ Web.set :refresh_rate, options.incident_refresh
28
38
 
29
39
  if log.level >= ::Logger::DEBUG
30
40
  Web.set :raise_errors, true
@@ -76,15 +86,15 @@ module FerrisBueller
76
86
  maxResults: 1_000_000
77
87
 
78
88
  users = data.inject({}) do |h, user|
79
- h[user['name']] = {
80
- key: user['key'],
81
- nick: user['name'],
82
- name: user['displayName'],
83
- email: user['emailAddress']
89
+ h[user[:name]] = {
90
+ key: user[:key],
91
+ nick: user[:name],
92
+ name: user[:displayName],
93
+ email: user[:emailAddress]
84
94
  } ; h
85
95
  end
86
96
 
87
- store['jira_users'] = users
97
+ store[:jira_users] = users
88
98
 
89
99
  rescue StandardError => e
90
100
  log.error \
@@ -105,8 +115,8 @@ module FerrisBueller
105
115
  groupname: options.jira_group,
106
116
  expand: 'users'
107
117
 
108
- user_names = data['users']['items'].map { |u| u['displayName'] }
109
- store['jira_members'] = user_names
118
+ user_names = data[:users][:items].map { |u| u[:name] }
119
+ store[:jira_members] = user_names
110
120
 
111
121
  rescue StandardError => e
112
122
  log.error \
@@ -129,8 +139,8 @@ module FerrisBueller
129
139
  startAt: 0,
130
140
  maxResults: 1_000_000
131
141
 
132
- store['jira_incidents'] = data['issues'].map do |i|
133
- i['num'] = i['key'].split('-', 2).last ; i
142
+ store[:jira_incidents] = data[:issues].map do |i|
143
+ i[:num] = i[:key].split('-', 2).last ; i
134
144
  end
135
145
 
136
146
  rescue StandardError => e
@@ -169,7 +179,7 @@ module FerrisBueller
169
179
  params: params,
170
180
  api_url: api_url,
171
181
  response: resp
172
- JSON.parse resp.body
182
+ JSON.parse resp.body, symbolize_names: true
173
183
  end
174
184
 
175
185
  end
@@ -0,0 +1,45 @@
1
+ require 'json'
2
+ require 'net/http'
3
+
4
+ require 'slog'
5
+
6
+ Thread.abort_on_exception = true
7
+
8
+
9
+ module FerrisBueller
10
+ class JiraAPI
11
+ def initialize options={}
12
+ @user = options.fetch :user
13
+ @pass = options.fetch :pass
14
+ @api_url = options.fetch :api_url
15
+ @base_path = options.fetch :base_path, '/rest/api/2'
16
+ @logger = options.fetch :logger, Slog.new
17
+ log.trace event: 'Jira API client initialized'
18
+ end
19
+
20
+ def send path, data={}
21
+ uri = URI File.join(@api_url, @base_path, path)
22
+ http = Net::HTTP.new uri.hostname, uri.port
23
+ http.use_ssl if uri.scheme == 'https'
24
+ req = Net::HTTP::Post.new uri
25
+ req.basic_auth @user, @pass
26
+ req['Content-Type'] = 'application/json'
27
+ req['Accept'] = 'application/json'
28
+ req.body = JSON.generate data
29
+ log.trace \
30
+ event: 'sending Jira API request',
31
+ path: path,
32
+ data: data
33
+ res = JSON.parse http.request(req).body, symbolize_names: true
34
+ log.debug \
35
+ event: 'Jira API request returned',
36
+ path: path,
37
+ data: data,
38
+ response: res
39
+ res
40
+ end
41
+
42
+ private
43
+ def log ; @logger end
44
+ end
45
+ end
@@ -84,7 +84,7 @@ module FerrisBueller
84
84
  type: :numeric,
85
85
  aliases: %w[ -i ],
86
86
  desc: 'Set JIRA incident refresh rate',
87
- default: (ENV['FERRIS_BUELLER_INCIDENT_REFRESH'] || 5)
87
+ default: (ENV['FERRIS_BUELLER_INCIDENT_REFRESH'] || 2)
88
88
  option :member_refresh, \
89
89
  type: :numeric,
90
90
  aliases: %w[ -g ],
@@ -1,10 +1,15 @@
1
+ require_relative 'constants'
2
+ require_relative 'main'
3
+
4
+
1
5
  module FerrisBueller
2
6
  module Replies
7
+ include Constants
3
8
 
4
9
  def reply_whoami params
5
10
  u = user_lookup(params)
6
11
  if u
7
- { text: "You're <@#{params['user_id']}>",
12
+ { text: "You're <@#{params[:user_id]}>",
8
13
  attachments: [
9
14
  {
10
15
  title: 'Slack User',
@@ -22,7 +27,7 @@ module FerrisBueller
22
27
  }
23
28
  else
24
29
  {
25
- text: "You're <@#{params['user_id']}>, but I can't say much more than that"
30
+ text: "You're <@#{params[:user_id]}>, but I can't say much more than that"
26
31
  }
27
32
  end
28
33
  end
@@ -39,38 +44,297 @@ module FerrisBueller
39
44
 
40
45
 
41
46
  def reply_list params
42
- { text: 'list' }
47
+ incidents = open_incidents
48
+ return { text: 'Could not list incidents' } if incidents.nil?
49
+ return { text: 'No open incidents at the moment' } if incidents.empty?
50
+
51
+ attachments = incidents.map do |i|
52
+ attach_incident(incident)
53
+ end
54
+ {
55
+ text: 'Found %d open incidents' % attachments.size,
56
+ attachments: attachments
57
+ }
43
58
  end
44
59
 
45
60
 
46
61
  def reply_summary params
47
- { text: 'summary' }
62
+ incidents = recent_incidents
63
+ return { text: 'Could not list incidents' } if incidents.nil?
64
+ return { text: 'No recent incidents' } if incidents.empty?
65
+
66
+ attachments = incidents.map do |i|
67
+ attach_incident i
68
+ end
69
+ {
70
+ text: 'Found %d recent incidents' % attachments.size,
71
+ attachments: attachments
72
+ }
48
73
  end
49
74
 
50
75
 
51
76
  def reply_show inc_num, params
52
- { text: 'show %d' % inc_num }
77
+ incident = select_incident inc_num
78
+ return { text: 'Could not list incidents' } unless incident
79
+
80
+ {
81
+ attachments: [
82
+ attach_incident(incident)
83
+ ]
84
+ }
53
85
  end
54
86
 
55
87
 
56
88
  def reply_resolve inc_num, params
57
- { text: 'resolve %d' % inc_num }
89
+ return { text: "You're not allowed to do that" } unless allowed? params
90
+
91
+ incident = select_incident inc_num
92
+ return { text: 'Could not list incidents' } unless incident
93
+
94
+ resolution = resolve_incident incident
95
+ return { text: 'Could not resolve incident' } if resolution.nil?
96
+ return { text: 'Already resolved' } if resolution == false
97
+
98
+ {
99
+ text: 'Resolved incident',
100
+ attachments: [
101
+ attach_incident(incident)
102
+ ]
103
+ }
58
104
  end
59
105
 
60
106
 
61
107
  def reply_close inc_num, params
62
- { text: 'close %d' % inc_num }
108
+ return { text: "You're not allowed to do that" } unless allowed? params
109
+
110
+ incident = select_incident inc_num
111
+ return { text: 'Could not list incidents' } unless incident
112
+
113
+ resolution = close_incident incident
114
+ return { text: 'Could not close incident' } unless resolution
115
+
116
+ {
117
+ text: 'Closed incident',
118
+ attachments: [
119
+ attach_incident(incident)
120
+ ]
121
+ }
63
122
  end
64
123
 
65
124
 
66
125
  def reply_open sev_num, summary, params
67
- { text: 'open %d %s' % [ sev_num, summary ] }
126
+ return { text: "You're not allowed to do that" } unless allowed? params
127
+
128
+ new_incident = construct_incident sev_num, summary, params
129
+ incident = open_incident new_incident, summary
130
+ return { text: 'Could not open incident' } unless incident
131
+
132
+ incident = new_incident.merge incident
133
+ {
134
+ text: 'Opened incident',
135
+ attachments: [
136
+ attach_incident(incident)
137
+ ]
138
+ }
68
139
  end
69
140
 
70
141
 
71
- def reply_comment inc_num, comment, params
72
- { text: 'comment %d %s' % [ inc_num, comment ] }
142
+ def reply_comment inc_num, message, params
143
+ incident = select_incident inc_num
144
+ return { text: 'Could not list incidents' } unless incident
145
+
146
+ comment = construct_comment message, params
147
+ annotation = comment_on_incident incident, comment
148
+ return { text: 'Could not comment on incident' } unless annotation
149
+
150
+ {
151
+ text: 'Commented on incident',
152
+ attachments: [
153
+ attach_incident(incident)
154
+ ]
155
+ }
73
156
  end
74
157
 
158
+
159
+
160
+ private
161
+
162
+ def attach_incident i
163
+ {
164
+ title: i[:key],
165
+ text: i[:fields][:summary],
166
+ mrkdwn_in: %w[ text pretext ]
167
+ }
168
+ end
169
+
170
+
171
+ def resolve_incident i
172
+ status = normalize_value i[:fields][:status]
173
+ return false if status =~ RESOLVED_STATE
174
+
175
+ log.trace \
176
+ event: 'resolving incident',
177
+ incident: i
178
+
179
+ RESOLVED_TRANSITIONS.map do |tid|
180
+ Thread.new do
181
+ jira.send "/issue/#{i[:key]}/transitions?expand=transitions.fields", \
182
+ transition: { id: tid }
183
+ end
184
+ end.join
185
+
186
+ sleep 1.5 * settings.refresh_rate
187
+
188
+ incident = select_incident i[:key].split('-',2).last
189
+ status = normalize_value incident[:fields][:status]
190
+
191
+ log.debug \
192
+ event: 'transitioned incident for resolve',
193
+ incident: i,
194
+ status: status
195
+
196
+ return incident if status =~ RESOLVED_STATE
197
+ end
198
+
199
+
200
+ def close_incident i
201
+ status = normalize_value i[:fields][:status]
202
+ return false if status =~ CLOSED_STATE
203
+
204
+ log.trace \
205
+ event: 'closing incident',
206
+ incident: i
207
+
208
+ CLOSED_TRANSITIONS.map do |tid|
209
+ Thread.new do
210
+ jira.send "/issue/#{i[:key]}/transitions?expand=transitions.fields", \
211
+ transition: { id: tid }
212
+ end
213
+ end.join
214
+
215
+ sleep 1.5 * settings.refresh_rate
216
+
217
+ incident = select_incident i[:key].split('-',2).last
218
+ status = normalize_value incident[:fields][:status]
219
+
220
+ log.debug \
221
+ event: 'transitioned incident for close',
222
+ incident: i,
223
+ status: status
224
+
225
+ return incident if status =~ CLOSED_STATE
226
+ end
227
+
228
+
229
+ def construct_incident sev_num, summary, params
230
+ u = user_lookup params
231
+ return unless u
232
+ {
233
+ fields: {
234
+ project: { key: settings.jira_project },
235
+ issuetype: { name: settings.jira_type },
236
+ reporter: { name: u[:jira][:nick] },
237
+ summary: summary,
238
+ SHOW_FIELDS.key('Severity') => {
239
+ id: SEVERITIES[sev_num.to_i]
240
+ }
241
+ }
242
+ }
243
+ end
244
+
245
+
246
+ def open_incident i, summary
247
+ return unless i
248
+ incident = jira.send 'issue', i
249
+ return unless incident.include? :key
250
+ log.info \
251
+ event: 'opened incident',
252
+ incident: incident
253
+ incident
254
+ end
255
+
256
+
257
+ def construct_comment message, params
258
+ u = user_lookup params
259
+ return unless u
260
+ {
261
+ body: '_[~%s]_ says: %s' % [ u[:jira][:nick], message ]
262
+ }
263
+ end
264
+
265
+
266
+ def comment_on_incident i, c
267
+ return unless i
268
+ return unless c
269
+ resp = jira.send "/issue/#{i[:key]}/comment", c
270
+ return unless resp.include? :id
271
+ log.info \
272
+ event: 'comment on incident',
273
+ incident: i,
274
+ comment: c
275
+ resp
276
+ end
277
+
278
+
279
+ def recent_incidents
280
+ return if store[:jira_incidents].nil?
281
+ store[:jira_incidents].select do |i|
282
+ Time.now - Time.parse(i[:fields][:created]) < one_day
283
+ end
284
+ end
285
+
286
+
287
+ def open_incidents
288
+ return if store[:jira_incidents].nil?
289
+ store[:jira_incidents].select do |i|
290
+ status = normalize_value i[:fields][:status]
291
+ !(status =~ /resolved|closed/i)
292
+ end
293
+ end
294
+
295
+
296
+ def select_incident num
297
+ return if store[:jira_incidents].nil?
298
+ store[:jira_incidents].select do |i|
299
+ i[:key] =~ /-#{num}$/
300
+ end.shift
301
+ end
302
+
303
+
304
+ def normalize_value val
305
+ case val
306
+ when Hash
307
+ val[:name] || val[:value] || val
308
+ when Array
309
+ val.map { |v| v[:value] }.join(', ')
310
+ when /^\d{4}\-\d{2}\-\d{2}/
311
+ '%s (%s)' % [ val, normalize_date(val) ]
312
+ else
313
+ val
314
+ end
315
+ end
316
+
317
+
318
+ def normalize_date val
319
+ Time.parse(val).utc.iso8601(0).sub(/Z$/, 'UTC')
320
+ end
321
+
322
+
323
+ def friendly_date val
324
+ Time.parse(val).strftime('%Y-%m-%d %H:%M %Z')
325
+ end
326
+
327
+
328
+ def one_day
329
+ 24 * 60 * 60 # seconds/day
330
+ end
331
+
332
+ def allowed? params
333
+ u = user_lookup params
334
+ u && store[:jira_members] \
335
+ && store[:jira_members].include?(u[:jira][:nick])
336
+ end
337
+
338
+
75
339
  end
76
340
  end
@@ -12,7 +12,7 @@ module FerrisBueller
12
12
  @token = options.fetch :token
13
13
  @logger = options.fetch :logger, Slog.new
14
14
  @api_url = options.fetch :api_url, 'https://slack.com/api'
15
- log.trace event: 'API client initialized'
15
+ log.trace event: 'Slack API client initialized'
16
16
  end
17
17
 
18
18
  def send method, options={}
@@ -21,7 +21,7 @@ module FerrisBueller
21
21
  log.trace event: 'sending api request', method: method, options: options
22
22
  res = Net::HTTP.post_form uri, options
23
23
  log.debug event: 'sent api request', method: method, options: options, response: res
24
- JSON.parse res.body
24
+ JSON.parse res.body, symbolize_names: true
25
25
  end
26
26
 
27
27
  private
@@ -5,7 +5,7 @@ module FerrisBueller
5
5
  module WebHelpers
6
6
  JARO_WINKLER = FuzzyStringMatch::JaroWinklerPure.new
7
7
 
8
- def options ; settings.options end
8
+ def jira ; settings.jira end
9
9
 
10
10
  def store ; settings.store end
11
11
 
@@ -15,16 +15,16 @@ module FerrisBueller
15
15
 
16
16
 
17
17
  def user_lookup params, threshold=0.75
18
- data = api.send 'users.info', user: params['user_id']
18
+ data = api.send 'users.info', user: params[:user_id]
19
19
 
20
20
  slack_user = {
21
- key: data['user']['id'],
22
- name: (data['user']['real_name'] || data['user']['name']),
23
- nick: data['user']['name'],
24
- email: data['user']['email']
21
+ key: data[:user][:id],
22
+ name: (data[:user][:real_name] || data[:user][:name]),
23
+ nick: data[:user][:name],
24
+ email: data[:user][:email]
25
25
  }
26
26
 
27
- jira_matches = store['jira_users'].values.map do |jira_user|
27
+ jira_matches = store[:jira_users].values.map do |jira_user|
28
28
  distances = [ :name, :nick ].map do |k|
29
29
  compare slack_user[k], jira_user[k]
30
30
  end.compact
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ferris-bueller
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Clemmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-21 00:00:00.000000000 Z
11
+ date: 2016-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slog
@@ -150,6 +150,7 @@ files:
150
150
  - lib/ferris-bueller.rb
151
151
  - lib/ferris-bueller/constants.rb
152
152
  - lib/ferris-bueller/helpers.rb
153
+ - lib/ferris-bueller/jira_api.rb
153
154
  - lib/ferris-bueller/main.rb
154
155
  - lib/ferris-bueller/metadata.rb
155
156
  - lib/ferris-bueller/mjolnir.rb