UNPKG

18.2 kBtext/coffeescriptView Raw
1# Description:
2# Lets you search for JIRA tickets, open
3# them, transition them thru different states, comment on them, rank
4# them up or down, start or stop watching them or change who is
5# assigned to a ticket. Also, notifications for mentions, assignments and watched tickets.
6#
7# Dependencies:
8# - moment
9# - octokat
10# - node-fetch
11# - underscore
12# - fuse.js
13#
14# Author:
15# ndaversa
16#
17# Contributions:
18# sjakubowski
19
20_ = require "underscore"
21moment = require "moment"
22
23Config = require "./config"
24Github = require "./github"
25Help = require "./help"
26Jira = require "./jira"
27Adapters = require "./adapters"
28Utils = require "./utils"
29
30class JiraBot
31
32 constructor: (@robot) ->
33 return new JiraBot @robot unless @ instanceof JiraBot
34 Utils.robot = @robot
35 Utils.JiraBot = @
36
37 @webhook = new Jira.Webhook @robot
38 switch @robot.adapterName
39 when "slack"
40 @adapter = new Adapters.Slack @robot
41 else
42 @adapter = new Adapters.Generic @robot
43
44 @registerWebhookListeners()
45 @registerEventListeners()
46 @registerRobotResponses()
47
48 send: (context, message, filter=yes) ->
49 context = @adapter.normalizeContext context
50 message = @filterAttachmentsForPreviousMentions context, message if filter
51 @adapter.send context, message
52
53 filterAttachmentsForPreviousMentions: (context, message) ->
54 return message if _(message).isString()
55 return message unless message.attachments?.length > 0
56 room = context.message.room
57
58 removals = []
59 for attachment in message.attachments when attachment and attachment.type is "JiraTicketAttachment"
60 ticket = attachment.author_name?.trim().toUpperCase()
61 continue unless Config.ticket.regex.test ticket
62
63 key = "#{room}:#{ticket}"
64 if Utils.cache.get key
65 removals.push attachment
66 Utils.Stats.increment "jirabot.surpress.attachment"
67 @robot.logger.debug "Supressing ticket attachment for #{ticket} in #{@adapter.getRoomName context}"
68 else
69 Utils.cache.put key, true, Config.cache.mention.expiry
70
71 message.attachments = _(message.attachments).difference removals
72 return message
73
74 matchJiraTicket: (message) ->
75 if message.match?
76 matches = message.match Config.ticket.regexGlobal
77 unless matches and matches[0]
78 urlMatch = message.match Config.jira.urlRegex
79 if urlMatch and urlMatch[1]
80 matches = [ urlMatch[1] ]
81 else if message.message?.rawText?.match?
82 matches = message.message.rawText.match Config.ticket.regexGlobal
83
84 if matches and matches[0]
85 return matches
86 else
87 if message.message?.rawMessage?.attachments?
88 attachments = message.message.rawMessage.attachments
89 for attachment in attachments
90 if attachment.text?
91 matches = attachment.text.match Config.ticket.regexGlobal
92 if matches and matches[0]
93 return matches
94 return false
95
96 prepareResponseForJiraTickets: (msg) ->
97 Promise.all(msg.match.map (key) =>
98 _attachments = []
99 Jira.Create.fromKey(key).then (ticket) ->
100 _attachments.push ticket.toAttachment()
101 ticket
102 .then (ticket) ->
103 Github.PullRequests.fromKey ticket.key unless Config.github.disabled
104 .then (prs) ->
105 prs?.toAttachment()
106 .then (attachments) ->
107 _attachments.push a for a in attachments if attachments
108 _attachments
109 ).then (attachments) =>
110 @send msg, attachments: _(attachments).flatten()
111 .catch (error) =>
112 @send msg, "#{error}"
113 @robot.logger.error error.stack
114
115 registerWebhookListeners: ->
116 # Watchers
117 disableDisclaimer = """
118 If you wish to stop receiving notifications for the tickets you are watching, reply with:
119 > jira disable notifications
120 """
121 @robot.on "JiraWebhookTicketInProgress", (ticket, event) =>
122 assignee = Utils.lookupUserWithJira ticket.fields.assignee
123 assigneeText = "."
124 assigneeText = " by #{assignee}" if assignee isnt "Unassigned"
125
126 @adapter.dm Utils.lookupChatUsersWithJira(ticket.watchers),
127 text: """
128 A ticket you are watching is now being worked on#{assigneeText}
129 """
130 author: event.user
131 footer: disableDisclaimer
132 attachments: [ ticket.toAttachment no ]
133 Utils.Stats.increment "jirabot.webhook.ticket.inprogress"
134
135 @robot.on "JiraWebhookTicketInReview", (ticket, event) =>
136 assignee = Utils.lookupUserWithJira ticket.fields.assignee
137 assigneeText = ""
138 assigneeText = "Please message #{assignee} if you wish to provide feedback." if assignee isnt "Unassigned"
139
140 @adapter.dm Utils.lookupChatUsersWithJira(ticket.watchers),
141 text: """
142 A ticket you are watching is now ready for review.
143 #{assigneeText}
144 """
145 author: event.user
146 footer: disableDisclaimer
147 attachments: [ ticket.toAttachment no ]
148 Utils.Stats.increment "jirabot.webhook.ticket.inreview"
149
150 @robot.on "JiraWebhookTicketDone", (ticket, event) =>
151 @adapter.dm Utils.lookupChatUsersWithJira(ticket.watchers),
152 text: """
153 A ticket you are watching has been marked `Done`.
154 """
155 author: event.user
156 footer: disableDisclaimer
157 attachments: [ ticket.toAttachment no ]
158 Utils.Stats.increment "jirabot.webhook.ticket.done"
159
160 # Comment notifications for watchers
161 @robot.on "JiraWebhookTicketComment", (ticket, comment) =>
162 @adapter.dm Utils.lookupChatUsersWithJira(ticket.watchers),
163 text: """
164 A ticket you are watching has a new comment from #{comment.author.displayName}:
165 ```
166 #{comment.body}
167 ```
168 """
169 author: comment.author
170 footer: disableDisclaimer
171 attachments: [ ticket.toAttachment no ]
172 Utils.Stats.increment "jirabot.webhook.ticket.comment"
173
174 # Comment notifications for assignee
175 @robot.on "JiraWebhookTicketComment", (ticket, comment) =>
176 return unless ticket.fields.assignee
177 return if ticket.watchers.length > 0 and _(ticket.watchers).findWhere name: ticket.fields.assignee.name
178
179 @adapter.dm Utils.lookupChatUsersWithJira(ticket.fields.assignee),
180 text: """
181 A ticket you are assigned to has a new comment from #{comment.author.displayName}:
182 ```
183 #{comment.body}
184 ```
185 """
186 author: comment.author
187 footer: disableDisclaimer
188 attachments: [ ticket.toAttachment no ]
189 Utils.Stats.increment "jirabot.webhook.ticket.comment"
190
191 # Mentions
192 @robot.on "JiraWebhookTicketMention", (ticket, user, event, context) =>
193 @adapter.dm user,
194 text: """
195 You were mentioned in a ticket by #{event.user.displayName}:
196 ```
197 #{context}
198 ```
199 """
200 author: event.user
201 footer: disableDisclaimer
202 attachments: [ ticket.toAttachment no ]
203 Utils.Stats.increment "jirabot.webhook.ticket.mention"
204
205 # Assigned
206 @robot.on "JiraWebhookTicketAssigned", (ticket, user, event) =>
207 @adapter.dm user,
208 text: """
209 You were assigned to a ticket by #{event.user.displayName}:
210 """
211 author: event.user
212 footer: disableDisclaimer
213 attachments: [ ticket.toAttachment no ]
214 Utils.Stats.increment "jirabot.webhook.ticket.assigned"
215
216 registerEventListeners: ->
217
218 #Find Matches (for cross-script usage)
219 @robot.on "JiraFindTicketMatches", (msg, cb) =>
220 cb @matchJiraTicket msg
221
222 #Prepare Responses For Tickets (for cross-script usage)
223 @robot.on "JiraPrepareResponseForTickets", (msg) =>
224 @prepareResponseForJiraTickets msg
225
226 #Create
227 @robot.on "JiraTicketCreated", (details) =>
228 @send details.room,
229 text: "Ticket created"
230 attachments: [
231 details.ticket.toAttachment no
232 details.assignee
233 details.transition
234 ]
235 Utils.Stats.increment "jirabot.ticket.create.success"
236
237 @robot.on "JiraTicketCreationFailed", (error, room) =>
238 robot.logger.error error.stack
239 @send room, "Unable to create ticket #{error}"
240 Utils.Stats.increment "jirabot.ticket.create.failed"
241
242 #Created in another room
243 @robot.on "JiraTicketCreatedElsewhere", (details) =>
244 room = @adapter.getRoom details
245 for r in Utils.lookupRoomsForProject details.ticket.fields.project.key
246 @send r,
247 text: "Ticket created in <##{room.id}|#{room.name}> by <@#{details.user.id}>"
248 attachments: [
249 details.ticket.toAttachment no
250 details.assignee
251 details.transition
252 ]
253 Utils.Stats.increment "jirabot.ticket.create.elsewhere"
254
255 #Clone
256 @robot.on "JiraTicketCloned", (ticket, room, clone, msg) =>
257 room = @adapter.getRoom msg
258 @send room,
259 text: "Ticket created: Cloned from #{clone} in <##{room.id}|#{room.name}> by <@#{msg.message.user.id}>"
260 attachments: [ ticket.toAttachment no ]
261 Utils.Stats.increment "jirabot.ticket.clone.success"
262
263 @robot.on "JiraTicketCloneFailed", (error, ticket, room) =>
264 @robot.logger.error error.stack
265 room = @adapter.getRoom room
266 @send room, "Unable to clone `#{ticket}` to the <\##{room.id}|#{room.name}> project :sadpanda:\n```#{error}```"
267 Utils.Stats.increment "jirabot.ticket.clone.failed"
268
269 #Transition
270 @robot.on "JiraTicketTransitioned", (ticket, transition, room, includeAttachment=no) =>
271 @send room,
272 text: "Transitioned #{ticket.key} to `#{transition.to.name}`"
273 attachments: [ ticket.toAttachment no ] if includeAttachment
274 Utils.Stats.increment "jirabot.ticket.transition.success"
275
276 @robot.on "JiraTicketTransitionFailed", (error, room) =>
277 @robot.logger.error error.stack
278 @send room, "#{error}"
279 Utils.Stats.increment "jirabot.ticket.transition.failed"
280
281 #Assign
282 @robot.on "JiraTicketAssigned", (ticket, user, room, includeAttachment=no) =>
283 @send room,
284 text: "Assigned <@#{user.id}> to #{ticket.key}"
285 attachments: [ ticket.toAttachment no ] if includeAttachment
286 Utils.Stats.increment "jirabot.ticket.assign.success"
287
288 @robot.on "JiraTicketUnassigned", (ticket, room, includeAttachment=no) =>
289 @send room,
290 text: "#{ticket.key} is now unassigned"
291 attachments: [ ticket.toAttachment no ] if includeAttachment
292 Utils.Stats.increment "jirabot.ticket.unassign.success"
293
294 @robot.on "JiraTicketAssignmentFailed", (error, room) =>
295 @robot.logger.error error.stack
296 @send room, "#{error}"
297 Utils.Stats.increment "jirabot.ticket.assign.failed"
298
299 #Watch
300 @robot.on "JiraTicketWatched", (ticket, user, room, includeAttachment=no) =>
301 @send room,
302 text: "Added <@#{user.id}> as a watcher on #{ticket.key}"
303 attachments: [ ticket.toAttachment no ] if includeAttachment
304 Utils.Stats.increment "jirabot.ticket.watch.success"
305
306 @robot.on "JiraTicketUnwatched", (ticket, user, room, includeAttachment=no) =>
307 @send room,
308 text: "Removed <@#{user.id}> as a watcher on #{ticket.key}"
309 attachments: [ ticket.toAttachment no ] if includeAttachment
310 Utils.Stats.increment "jirabot.ticket.unwatch.success"
311
312 @robot.on "JiraTicketWatchFailed", (error, room) =>
313 @robot.logger.error error.stack
314 @send room, "#{error}"
315 Utils.Stats.increment "jirabot.ticket.watch.failed"
316
317 #Rank
318 @robot.on "JiraTicketRanked", (ticket, direction, room, includeAttachment=no) =>
319 @send room,
320 text: "Ranked #{ticket.key} to `#{direction}`"
321 attachments: [ ticket.toAttachment no ] if includeAttachment
322 Utils.Stats.increment "jirabot.ticket.rank.success"
323
324 @robot.on "JiraTicketRankFailed", (error, room) =>
325 @robot.logger.error error.stack
326 @send room, "#{error}"
327 Utils.Stats.increment "jirabot.ticket.rank.failed"
328
329 #Labels
330 @robot.on "JiraTicketLabelled", (ticket, room, includeAttachment=no) =>
331 @send room,
332 text: "Added labels to #{ticket.key}"
333 attachments: [ ticket.toAttachment no ] if includeAttachment
334 Utils.Stats.increment "jirabot.ticket.label.success"
335
336 @robot.on "JiraTicketLabelFailed", (error, room) =>
337 @robot.logger.error error.stack
338 @send room, "#{error}"
339 Utils.Stats.increment "jirabot.ticket.label.failed"
340
341 #Comments
342 @robot.on "JiraTicketCommented", (ticket, room, includeAttachment=no) =>
343 @send room,
344 text: "Added comment to #{ticket.key}"
345 attachments: [ ticket.toAttachment no ] if includeAttachment
346 Utils.Stats.increment "jirabot.ticket.comment.success"
347
348 @robot.on "JiraTicketCommentFailed", (error, room) =>
349 @robot.logger.error error.stack
350 @send room, "#{error}"
351 Utils.Stats.increment "jirabot.ticket.comment.failed"
352
353 registerRobotResponses: ->
354 #Help
355 @robot.respond Config.help.regex, (msg) =>
356 msg.finish()
357 [ __, topic] = msg.match
358 @send msg, Help.forTopic topic, @robot
359 Utils.Stats.increment "command.jirabot.help"
360
361 #Enable/Disable Watch Notifications
362 @robot.respond Config.watch.notificationsRegex, (msg) =>
363 msg.finish()
364 [ __, state ] = msg.match
365 switch state
366 when "allow", "start", "enable"
367 @adapter.enableNotificationsFor msg.message.user
368 @send msg, """
369 JIRA Watch notifications have been *enabled*
370
371 You will start receiving notifications for JIRA tickets you are watching
372
373 If you wish to _disable_ them just send me this message:
374 > jira disable notifications
375 """
376 when "disallow", "stop", "disable"
377 @adapter.disableNotificationsFor msg.message.user
378 @send msg, """
379 JIRA Watch notifications have been *disabled*
380
381 You will no longer receive notifications for JIRA tickets you are watching
382
383 If you wish to _enable_ them again just send me this message:
384 > jira enable notifications
385 """
386 Utils.Stats.increment "command.jirabot.toggleNotifications"
387
388 #Search
389 @robot.respond Config.search.regex, (msg) =>
390 msg.finish()
391 [__, query] = msg.match
392 room = msg.message.room
393 project = Config.maps.projects[room]
394 Jira.Search.withQueryForProject(query, project, msg)
395 .then (results) =>
396 attachments = (ticket.toAttachment() for ticket in results.tickets)
397 @send msg,
398 text: results.text
399 attachments: attachments
400 , no
401 .catch (error) =>
402 @send msg, "Unable to search for `#{query}` :sadpanda:"
403 @robot.logger.error error.stack
404 Utils.Stats.increment "command.jirabot.search"
405
406 #Transition
407 if Config.maps.transitions
408 @robot.hear Config.transitions.regex, (msg) =>
409 msg.finish()
410 [ __, key, toState ] = msg.match
411 Jira.Transition.forTicketKeyToState key, toState, msg, no
412 Utils.Stats.increment "command.jirabot.transition"
413
414 #Clone
415 @robot.hear Config.clone.regex, (msg) =>
416 msg.finish()
417 [ __, ticket, channel ] = msg.match
418 project = Config.maps.projects[channel]
419 Jira.Clone.fromTicketKeyToProject ticket, project, channel, msg
420 Utils.Stats.increment "command.jirabot.clone"
421
422 #Watch
423 @robot.hear Config.watch.regex, (msg) =>
424 msg.finish()
425 [ __, key, remove, person ] = msg.match
426
427 if remove
428 Jira.Watch.forTicketKeyRemovePerson key, person, msg, no
429 else
430 Jira.Watch.forTicketKeyForPerson key, person, msg, no
431 Utils.Stats.increment "command.jirabot.watch"
432
433 #Rank
434 @robot.hear Config.rank.regex, (msg) =>
435 msg.finish()
436 [ __, key, direction ] = msg.match
437 Jira.Rank.forTicketKeyByDirection key, direction, msg, no
438 Utils.Stats.increment "command.jirabot.rank"
439
440 #Comment or maybe Labels (damn you slack)
441 @robot.hear Config.comment.regex, (msg) =>
442 msg.finish()
443 message = msg.message.rawText or msg.message.text
444
445 if Config.labels.addRegex.test message
446 [ __, key ] = msg.match
447 [ __, labels ] = Utils.extract.labels message.replace(Config.labels.commandSplitRegex, "$1")
448 Jira.Labels.forTicketKeyWith key, labels, msg, no
449 Utils.Stats.increment "command.jirabot.label"
450 else
451 [ __, key, comment ] = msg.match
452 Jira.Comment.forTicketKeyWith key, comment, msg, no
453 Utils.Stats.increment "command.jirabot.comment"
454
455 #Subtask
456 @robot.respond Config.subtask.regex, (msg) =>
457 msg.finish()
458 [ __, key, summary ] = msg.match
459 Jira.Create.subtaskFromKeyWith key, summary, msg
460 Utils.Stats.increment "command.jirabot.subtask"
461
462 #Assign
463 @robot.hear Config.assign.regex, (msg) =>
464 msg.finish()
465 [ __, key, remove, person ] = msg.match
466
467 if remove
468 Jira.Assign.forTicketKeyToUnassigned key, msg, no
469 else
470 Jira.Assign.forTicketKeyToPerson key, person, msg, no
471
472 Utils.Stats.increment "command.jirabot.assign"
473
474 #Create
475 @robot.respond Config.commands.regex, (msg) =>
476 message = msg.message.rawText or msg.message.text
477 [ __, project, command, summary ] = message.match Config.commands.regex
478 room = project or @adapter.getRoomName msg
479 project = Config.maps.projects[room.toLowerCase()]
480 type = Config.maps.types[command.toLowerCase()]
481
482 unless project
483 channels = []
484 for team, key of Config.maps.projects
485 room = @adapter.getRoom team
486 channels.push " <\##{room.id}|#{room.name}>" if room
487 return msg.reply "#{type} must be submitted in one of the following project channels: #{channels}"
488
489 if Config.duplicates.detection and @adapter.detectForDuplicates?
490 @adapter.detectForDuplicates project, type, summary, msg
491 else
492 Jira.Create.with project, type, summary, msg
493
494 Utils.Stats.increment "command.jirabot.create"
495
496 #Mention ticket by url or key
497 @robot.listen @matchJiraTicket, (msg) =>
498 @prepareResponseForJiraTickets msg
499 Utils.Stats.increment "command.jirabot.mention.ticket"
500
501module.exports = JiraBot