ferris-bueller 0.0.2 → 0.0.3

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