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 +4 -4
- data/VERSION +1 -1
- data/lib/ferris-bueller/helpers.rb +21 -11
- data/lib/ferris-bueller/jira_api.rb +45 -0
- data/lib/ferris-bueller/main.rb +1 -1
- data/lib/ferris-bueller/replies.rb +274 -10
- data/lib/ferris-bueller/slack_api.rb +2 -2
- data/lib/ferris-bueller/web_helpers.rb +7 -7
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e42b63a6a4a472e0a57bf203fd2b40df207691f0
|
4
|
+
data.tar.gz: e17b58dd6390318e6540015e1d5ead60a8b5fd27
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e63ebf4b536ae282d08365cd580796e843a89685d53ef498f936bee99e592fd04a484cd6a9398296882ff17b3602a0eb373e0bc991dc9c379e4359d17f39a84
|
7
|
+
data.tar.gz: ae7e81ccd67840d08b36d0855fd7adcfb441e9249d7ede8916f8fb0f148e4722e6b208ce6c0dfe81e676e5d454735856b9a44eded1cafbf5c140c178e0305731
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
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[
|
80
|
-
key: user[
|
81
|
-
nick: user[
|
82
|
-
name: user[
|
83
|
-
email: user[
|
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[
|
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[
|
109
|
-
store[
|
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[
|
133
|
-
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
|
data/lib/ferris-bueller/main.rb
CHANGED
@@ -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'] ||
|
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[
|
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[
|
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
|
-
|
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
|
-
|
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
|
-
|
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: '
|
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: '
|
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: '
|
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,
|
72
|
-
|
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
|
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[
|
18
|
+
data = api.send 'users.info', user: params[:user_id]
|
19
19
|
|
20
20
|
slack_user = {
|
21
|
-
key: data[
|
22
|
-
name: (data[
|
23
|
-
nick: data[
|
24
|
-
email: data[
|
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[
|
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.
|
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-
|
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
|