class BenderBot

Constants

JARO

Public Class Methods

set_commands(without) click to toggle source
# File lib/bender/bot.rb, line 39
def self.set_commands without
  commands = {
    help: {
      desc: 'Display this help text',
    },
    list: {
      cmd: '',
      desc: 'List open incidents'
    },
    show: {
      cmd: '',
      fmt: 'INCIDENT_NUMBER',
      desc: 'Display incident details'
    },
    resolve: {
      fmt: 'INCIDENT_NUMBER',
      desc: 'Resolve an incident',
    },
    close: {
      fmt: 'INCIDENT_NUMBER',
      desc: 'Close an incident',
    },
    open: {
      fmt: "#{SEVERITY_FIELD.upcase}=#{SEVERITIES.keys.sort_by(&:to_i).join(',')} SUMMARY_TEXT",
      desc: 'Open a new incident',
    },
    summary: {
      desc: 'Summarize incidents from past 24 hours (open or closed)'
    },
    comment: {
      fmt: 'INCIDENT_NUMBER COMMENT_TEXT',
      desc: 'Add a comment to an incident'
    }
  }
  without.each { |wo| commands.delete wo.to_sym }
  BenderBot.const_set :COMMANDS, commands
end

Public Instance Methods

handle(room, sender, message) click to toggle source
# File lib/bender/bot.rb, line 117
def handle room, sender, message
  refresh_room room

  @room      = room
  @sender    = sender
  @message   = message

  severities = Hash.new { |h,k| h[k] = [] }


  case message.strip

  when /^\/#{config[:jira_user]}$/
    reply_html QUOTES.sample(1).first, :red

  when /^\/whoami$/
    u = user_where name: sender
    if u
      m = '<b>%{nick}</b>: %{name} (<a href="mailto:%{email}">%{email}</a>)' % u
      reply_html m, :purple
    else
      reply_html "Couldn't find the associated user in JIRA", :red
    end

  when /^\/lookup\s+(.+)$/
    u = user_where(name: $1) || user_where(nick: $1)
    if u
      m = '<b>%{nick}</b>: %{name} (<a href="mailto:%{email}">%{email}</a>)' % u
      reply_html m, :purple
    else
      reply_html "Couldn't find the associated user in JIRA", :red
    end

  # /prefix help - This help text
  when /^(\?#{prefix}|\/#{prefix}\s+help)$/
    reply_with_help

  # /prefix - List open incidents
  when /^\/#{prefix}$/
    refresh_incidents

    is = store['incidents'].reverse.map do |i|
      status = normalize_value i['fields']['status']
      unless status =~ /resolved|closed/i
        '%s (%s - %s) [%s]: %s' % [
          incident_link(i),
          short_severity(sev_for(i)),
          normalize_value(i['fields']['status']),
          friendly_date(i['fields']['created']),
          i['fields']['summary']
        ]
      end
    end.compact.join('<br />')

    if is.empty?
      reply_html 'Good news everyone! No open incidents at the moment', :green
    else
      reply_html is
    end

  # /prefix summary - Summarize recent incidents
  when /^\/#{prefix}\s+summary$/
    refresh_incidents

    statuses = Hash.new { |h,k| h[k] = 0 }

    store['incidents'].reverse.each do |i|
      if recent_incident? i
        status = normalize_value(i['fields']['status'])

        repr = '%s (%s) [%s]: %s' % [
          incident_link(i),
          status,
          friendly_date(i['fields']['created']),
          i['fields']['summary']
        ]

        severities[sev_for(i)] << repr
        statuses[status] += 1
      end
    end

    summary = []
    summary << 'By Status:'
    statuses.each do |status, size|
      summary << '%s: %d incident(s)' % [ status, size ]
    end
    summary << ''
    summary << "By #{SEVERITY_FIELD.capitalize}:"
    severities.keys.sort.each do |severity|
      summary << '%s: %d incident(s)' % [
        short_severity(severity),
        severities[severity].size
      ]
    end

    if severities.empty?
      reply_html 'Good news everyone! No open incidents at the moment', :green

    else
      is = severities.keys.sort.map do |sev|
        "%s:<br />%s" % [ sev, severities[sev].join("<br />") ]
      end.join("<br /><br />")

      reply_html(summary.join("<br />") + "<br /><br />" + is)
    end


  # /prefix NUM - Show incident details
  when /^\/#{prefix}\s+(\d+)$/
    incident = select_incident $1

    if incident.nil?
      reply_html 'Sorry, no such incident!', :red
    else
      fields = SHOW_FIELDS.keys - %w[ summary ]

      i = fields.map do |f|
        val = incident['fields'][f]
        if val
          key = SHOW_FIELDS[f]
          val = normalize_value val
          '%s: %s' % [ key, val ]
        end
      end.compact

      reply_html "%s - %s<br />%s" % [
        incident_link(incident),
        incident['fields']['summary'],
        i.join("<br />")
      ]
    end

  # /prefix resolve NUM - Resolve an incident
  when /^\/#{prefix}\s+resolve\s+(\d+)$/
    unless allowed?
      reply_html "You're not allowed to do that!", :red
    else
      incident = select_incident $1
      if incident
        reply_html(*resolve_incident(incident))
      else
        reply_html 'Sorry, no such incident!', :red
      end
    end

  # /prefix close NUM - Close an incident
  when /^\/#{prefix}\s+close\s+(\d+)$/
    unless allowed?
      reply_html "You're not allowed to do that!", :red
    else
      incident = select_incident $1
      if incident
        reply_html(*close_incident(incident))
      else
        reply_html 'Sorry, no such incident!', :red
      end
    end

  # /prefix open SEVERITY SUMMARY - File a new incident
  when /^\/#{prefix}\s+open\s+(severity|sev|s|p)?(\d+)\s+(.*)/im
    unless allowed?
      reply_html "You're not allowed to do that!", :red
    else
      user = user_where name: sender
      data = {
        fields: {
          project: { key: config[:jira_project] },
          issuetype: { name: config[:jira_type] },
          reporter: { name: user[:nick] },
          summary: $3,
          SEVERITY_FIELD => {
            id: SEVERITIES[$2.to_i]
          }
        }
      }

      reply_html(*file_incident(data))
    end


  # /prefix comment [INCIDENT_NUMBER] [COMMENT_TEXT]
  when /^\/#{prefix}\s+comment\s+(\d+)\s+(.*)/im
    incident = select_incident $1
    comment  = $2
    user     = user_where name: sender

    if !user
      reply_html "Couldn't find the associated user in JIRA", :red
    elsif incident
      reply_html(*comment_on_incident(incident, comment, user))
    else
      reply_html 'Sorry, no such incident!', :red
    end


  when /^\/#{prefix}/i
    reply_html 'Invalid usage', :red
    reply_with_help
  end


  return true
end
refresh_room(jid) click to toggle source
# File lib/bender/bot.rb, line 105
def refresh_room jid
  begin
    @this_room = @@rooms.select { |r| r.xmpp_jid == jid }.first
    @@hipchat[@this_room.name].get_room # test
  rescue
    @@hipchat  = HipChat::Client.new(@@config[:hipchat_token])
    @@rooms    = @@hipchat.rooms
    @this_room = @@rooms.select { |r| r.xmpp_jid == jid }.first
  end
end
reply_html(message, color=:yellow) click to toggle source
# File lib/bender/bot.rb, line 78
def reply_html message, color=:yellow
  tries ||= 3
  @@hipchat[@this_room.name].send(nick, message, color: color)
rescue
  tries -= 1
  if tries > 0
    refresh_room @this_room.xmpp_jid
    log.warn reason: 'could not reply_html', remediation: 'retrying'
    retry
  end
  log.error reason: 'could not reply_html'
end
reply_with_help() click to toggle source
# File lib/bender/bot.rb, line 92
def reply_with_help
  help = COMMANDS.each.map do |cmd, opts|
    opts[:cmd] ||= cmd
    opts[:cmd]   = opts[:cmd].to_s.insert(0, ' ') if opts[:cmd]
    opts[:fmt] ||= ''
    opts[:fmt]   = opts[:fmt].to_s.insert(0, ' ') if opts[:fmt]
    "<code>/#{prefix}%{cmd}%{fmt}</code> - %{desc}" % opts
  end.join('<br />')

  reply_html help
end

Private Instance Methods

allowed?() click to toggle source
# File lib/bender/bot.rb, line 496
def allowed?
  user = user_where name: @sender
  user && store['group'].include?(user[:name])
end
close_incident(incident) click to toggle source
# File lib/bender/bot.rb, line 432
def close_incident incident
  status = normalize_value incident['fields']['status']
  if status =~ CLOSED_STATE
    return [
      "#{incident_link(incident)} is already closed!",
      :green
    ]
  end

  req_path = '/rest/api/2/issue/%s/transitions?expand=transitions.fields' % [
    incident['key']
  ]
  uri = URI(config[:jira_site] + req_path)
  http = Net::HTTP.new uri.hostname, uri.port

  req = Net::HTTP::Post.new uri
  req.basic_auth config[:jira_user], config[:jira_pass]
  req['Content-Type'] = 'application/json'
  req['Accept'] = 'application/json'

  CLOSED_TRANSITIONS.each do |tid|
    req.body = {
      transition: { id: tid }
    }.to_json
    http.request req
  end

  incident = select_incident incident['key'].split('-',2).last
  status = normalize_value incident['fields']['status']

  if status =~ CLOSED_STATE
    [ 'Closed ' + incident_link(incident), :green ]
  else
    [
      "Failed to close #{incident_link(incident)} automatically, you might try yourself",
      :red
    ]
  end
end
comment_on_incident(incident, comment, user) click to toggle source
# File lib/bender/bot.rb, line 473
def comment_on_incident incident, comment, user
  req_path = '/rest/api/2/issue/%s/comment' % incident['key']
  uri = URI(config[:jira_site] + req_path)
  http = Net::HTTP.new uri.hostname, uri.port

  req = Net::HTTP::Post.new uri
  req.basic_auth config[:jira_user], config[:jira_pass]
  req['Content-Type'] = 'application/json'
  req['Accept'] = 'application/json'
  req.body = { body: '_[~%s]_ says: %s' % [ user[:nick], comment ] }.to_json

  case http.request(req)
  when Net::HTTPCreated
    [ 'Added comment to ' + incident_link(incident), :green ]
  else
    [
      'Sorry, I had trouble adding your comment on' + incident_link(incident),
      :red
    ]
  end
end
compare(name1, name2) click to toggle source
# File lib/bender/bot.rb, line 354
def compare name1, name2
  n1 = name1.gsub(/\W/, '').downcase
  n2 = name2.gsub(/\W/, '').downcase
  JARO.getDistance n1, n2
end
config() click to toggle source
# File lib/bender/bot.rb, line 328
def config ; @@config end
file_incident(data) click to toggle source
# File lib/bender/bot.rb, line 362
def file_incident data
  req_path = '/rest/api/2/issue'
  uri = URI(config[:jira_site] + req_path)
  http = Net::HTTP.new uri.hostname, uri.port

  req = Net::HTTP::Post.new uri
  req.basic_auth config[:jira_user], config[:jira_pass]
  req['Content-Type'] = 'application/json'
  req['Accept'] = 'application/json'
  req.body = data.to_json

  resp  = http.request req
  issue = JSON.parse(resp.body)

  if issue.has_key? 'key'
    [ 'Filed ' + incident_link(issue), :green ]
  else
    log.error \
      error: 'Could not file ticket',
      reason: 'Invalid response',
      request: req.inspect,
      response: issue
    [ "Sorry, I couldn't file that!", :red ]
  end
end
log() click to toggle source
# File lib/bender/bot.rb, line 326
def log ; @@logger end
prefix() click to toggle source
# File lib/bender/bot.rb, line 502
def prefix ; config[:prefix] end
resolve_incident(incident) click to toggle source
# File lib/bender/bot.rb, line 390
def resolve_incident incident
  status = normalize_value incident['fields']['status']
  if status =~ RESOLVED_STATE
    return [
      "#{incident_link(incident)} is already resolved!",
      :green
    ]
  end

  req_path = '/rest/api/2/issue/%s/transitions?expand=transitions.fields' % [
    incident['key']
  ]
  uri = URI(config[:jira_site] + req_path)
  http = Net::HTTP.new uri.hostname, uri.port

  req = Net::HTTP::Post.new uri
  req.basic_auth config[:jira_user], config[:jira_pass]
  req['Content-Type'] = 'application/json'
  req['Accept'] = 'application/json'

  RESOLVED_TRANSITIONS.each do |tid|
    req.body = {
      transition: { id: tid }
    }.to_json
    http.request req
  end

  incident = select_incident incident['key'].split('-',2).last
  status = normalize_value incident['fields']['status']

  if status =~ RESOLVED_STATE
    [ 'Resolved ' + incident_link(incident), :green ]
  else
    [
      "Failed to resolve #{incident_link(incident)} automatically, you might try yourself",
      :red
    ]
  end
end
sev_for(i) click to toggle source
# File lib/bender/bot.rb, line 505
def sev_for i
  sev = if i['fields'][SEVERITY_FIELD]
    i['fields'][SEVERITY_FIELD]['value'] || i['fields'][SEVERITY_FIELD]['name']
  end
  sev.nil? ? '??' : sev
end
user_where(fields, threshold=0.85) click to toggle source
# File lib/bender/bot.rb, line 330
def user_where fields, threshold=0.85
  field, value = fields.to_a.shift
  suggested_user = store['users'].values.sort_by do |u|
    compare value, u[field]
  end.last

  distance = compare value, suggested_user[field]

  user = distance < threshold ? nil : suggested_user

  log.debug \
    action: 'user_where',
    fields: fields,
    threshold: threshold,
    field: field,
    value: value,
    suggested_user: suggested_user,
    distance: distance,
    user: user

  return user
end