require 'logger' require 'net/http' require 'json' require 'tilt/erb' require 'queryparams' require_relative 'metadata' require_relative 'mjolnir' require_relative 'helpers' require_relative 'bot' Thread.abort_on_exception = true module Bender class Main < Mjolnir include Helpers desc 'version', 'Echo the application version' def version puts VERSION end desc 'art', 'View the application art' def art puts "\n%s\n" % ART end desc 'start', 'Start Bender HipChat bot and Web server' option :config, \ type: :string, aliases: %w[ -c ], desc: 'Set path to config file', required: true include_common_options def start raw_config = JSON.parse File.read(options.config), symbolize_names: true @config = DEFAULT_CONFIG.merge! raw_config BenderBot.const_set :RESOLVED_TRANSITIONS, %w[ 51 ] Bender.const_set :RESOLVED_TRANSITIONS, BenderBot::RESOLVED_TRANSITIONS Helpers.const_set :RESOLVED_TRANSITIONS, BenderBot::RESOLVED_TRANSITIONS BenderBot.const_set :RESOLVED_STATE, /resolve/i Bender.const_set :RESOLVED_STATE, BenderBot::RESOLVED_STATE Helpers.const_set :RESOLVED_STATE, BenderBot::RESOLVED_STATE BenderBot.const_set :CLOSED_TRANSITIONS, %w[ 61 71 ] Bender.const_set :CLOSED_TRANSITIONS, BenderBot::CLOSED_TRANSITIONS Helpers.const_set :CLOSED_TRANSITIONS, BenderBot::CLOSED_TRANSITIONS BenderBot.const_set :CLOSED_STATE, /close/i Bender.const_set :CLOSED_STATE, BenderBot::CLOSED_STATE Helpers.const_set :CLOSED_STATE, BenderBot::CLOSED_STATE BenderBot.const_set :SEVERITIES, { 1 => '10480', 2 => '10481', 3 => '10482', 4 => '10483', 5 => '10484' } Bender.const_set :SEVERITIES, BenderBot::SEVERITIES Helpers.const_set :SEVERITIES, BenderBot::SEVERITIES BenderBot.const_set :SHOW_FIELDS, { 'summary' => 'Summary', 'description' => 'Description', 'customfield_11250' => 'Severity', 'customfield_11251' => 'Impact Started', 'customfield_11252' => 'Impact Ended', 'customfield_11253' => 'Reported By', 'customfield_11254' => 'Services Affected', 'customfield_11255' => 'Cause', 'status' => 'Status', 'created' => 'Created', 'updated' => 'Updated' } Bender.const_set :SHOW_FIELDS, BenderBot::SHOW_FIELDS Helpers.const_set :SHOW_FIELDS, BenderBot::SHOW_FIELDS BenderBot.const_set :SEVERITY_FIELD, SHOW_FIELDS.key('Severity') Bender.const_set :SEVERITY_FIELD, BenderBot::SEVERITY_FIELD Helpers.const_set :SEVERITY_FIELD, BenderBot::SEVERITY_FIELD BenderBot.set_commands bot = start_bot ts = [] ts << periodically_refresh_group(bot) ts << periodically_refresh_users(bot) ts << periodically_refresh_incidents(bot) ts.map(&:join) end private def config ; @config end def start_bot Bot::Connection.configure do |conn| conn.jid = config[:jid] conn.password = config[:password] conn.nick = config[:mention] conn.mention_name = config[:nick] conn.rooms = config[:rooms].is_a?(Array) ? \ config[:rooms] : config[:rooms].split(',') Bot::Storage::YamlStore.file = config[:database] conn.store = Bot::Storage::YamlStore conn.logger = log end Bot.run! config, log end def set_room_name_and_topic room_id, incidents, hipchat, bot room = hipchat[room_id] new_room = room.get_room if incidents.nil? log.error \ error: 'Cannot set room name and topic', reason: 'incidents are nil', room_id: room_id return end open_incidents = incidents.select do |i| status = normalize_value i['fields']['status'] severity = short_severity(i['fields'][SEVERITY_FIELD]['value']) is_open = !(status =~ /resolved|closed/i) is_severe = severity =~ /(SEV1|SEV2)/i is_open && is_severe end @room_name ||= bot.store['primary_room_name'] || new_room['name'] @room_topic ||= bot.store['primary_room_topic'] || new_room['topic'] @open = nil unless defined? @open log.debug \ primary_room_name: bot.store['primary_room_name'], primary_room_topic: bot.store['primary_room_topic'], new_room_name: new_room['name'], new_room_topic: new_room['topic'], room_name: @room_name, room_topic: @room_topic, open: @open if open_incidents.empty? if @open.nil? || @open > 0 new_room['name'] = '[NONE] %s' % config[:room_base_name] new_room['topic'] = 'Good news everyone! No high-severity incidents at the moment' apply_room_name_and_topic new_room end @open = 0 else if @open.nil? || @open.zero? @room_name = new_room['name'] @room_topic = new_room['topic'] end unless @open == open_incidents.size new_room['name'] = '[IN PROGRESS] %s' % config[:room_base_name] tha_news = open_incidents.size == 1 ? "There's a high-severity incident" : "There are #{open_incidents.size} high-severity incidents" new_room['topic'] = "Terrible news everyone! #{tha_news}" apply_room_name_and_topic new_room end @open = open_incidents.size end bot.store['primary_room_name'] = @room_name bot.store['primary_room_topic'] = @room_topic end def apply_room_name_and_topic room uri = URI room['links']['self'] Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| req = Net::HTTP::Put.new uri req['Content-Type'] = 'application/json' req['Authorization'] = 'Bearer %s' % config[:hipchat_v2_token] req.body = { name: room['name'], topic: room['topic'], privacy: room['privacy'], is_archived: room['is_archived'], is_guest_accessible: room['is_guest_accessible'], owner: { id: room['owner']['id'] } }.to_json http.request req end end def periodically_refresh_incidents bot Thread.new do if config[:order] hipchat_v1 = HipChat::Client.new \ config[:hipchat_token], api_version: 'v1' hipchat_v2 = HipChat::Client.new \ config[:hipchat_v2_token], api_version: 'v2' else hipchat_v2 = HipChat::Client.new \ config[:hipchat_v2_token], api_version: 'v2' hipchat_v1 = HipChat::Client.new \ config[:hipchat_token], api_version: 'v1' end room_id = config[:primary_room_id] loop do is = refresh_incidents bot set_room_name_and_topic room_id, is, hipchat_v2, bot sleep config[:issue_refresh] end end end def periodically_refresh_users bot req_path = '/rest/api/2/user/assignable/search' req_params = QueryParams.encode \ project: config[:jira_project], startAt: 0, maxResults: 1_000_000 uri = URI(config[:jira_site] + req_path + '?' + req_params) http = Net::HTTP.new uri.hostname, uri.port req = Net::HTTP::Get.new uri req.basic_auth config[:jira_user], config[:jira_pass] req['Content-Type'] = 'application/json' req['Accept'] = 'application/json' Thread.new do loop do begin resp = http.request req data = JSON.parse resp.body rescue StandardError => e log.error \ message: 'Could not periodically refresh users', error: e.class sleep 5 next end users = data.inject({}) do |h, user| h[user['name']] = { key: user['key'], nick: user['name'], name: user['displayName'], email: user['emailAddress'] } ; h end bot.store['users'] = users sleep config[:user_refresh] end end end def periodically_refresh_group bot req_path = '/rest/api/2/group' req_params = QueryParams.encode \ groupname: config[:jira_group], expand: 'users' uri = URI(config[:jira_site] + req_path + '?' + req_params) http = Net::HTTP.new uri.hostname, uri.port req = Net::HTTP::Get.new uri req.basic_auth config[:jira_user], config[:jira_pass] req['Content-Type'] = 'application/json' req['Accept'] = 'application/json' Thread.new do loop do begin resp = http.request req data = JSON.parse resp.body rescue StandardError => e log.error \ message: 'Could not periodically refresh group', error: e.class sleep 5 next end user_names = data['users']['items'].map { |u| u['displayName'] } bot.store['group'] = user_names sleep config[:group_refresh] end end end end end