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, config[:resolved_transitions] Bender.const_set :RESOLVED_TRANSITIONS, BenderBot::RESOLVED_TRANSITIONS Helpers.const_set :RESOLVED_TRANSITIONS, BenderBot::RESOLVED_TRANSITIONS BenderBot.const_set :RESOLVED_STATE, Regexp.new(config[:resolved_state]) Bender.const_set :RESOLVED_STATE, BenderBot::RESOLVED_STATE Helpers.const_set :RESOLVED_STATE, BenderBot::RESOLVED_STATE BenderBot.const_set :CLOSED_TRANSITIONS, config[:closed_transitions] Bender.const_set :CLOSED_TRANSITIONS, BenderBot::CLOSED_TRANSITIONS Helpers.const_set :CLOSED_TRANSITIONS, BenderBot::CLOSED_TRANSITIONS BenderBot.const_set :CLOSED_STATE, Regexp.new(config[:closed_state]) Bender.const_set :CLOSED_STATE, BenderBot::CLOSED_STATE Helpers.const_set :CLOSED_STATE, BenderBot::CLOSED_STATE # Integerify keys severities = config[:severities].inject({}) { |h,(k,v)| h[k.to_s.to_i] = v ; h } BenderBot.const_set :SEVERITIES, severities Bender.const_set :SEVERITIES, BenderBot::SEVERITIES Helpers.const_set :SEVERITIES, BenderBot::SEVERITIES # Stringify keys show_fields = config[:show_fields].inject({}) { |h,(k,v)| h[k.to_s] = v ; h } BenderBot.const_set :SHOW_FIELDS, show_fields Bender.const_set :SHOW_FIELDS, BenderBot::SHOW_FIELDS Helpers.const_set :SHOW_FIELDS, BenderBot::SHOW_FIELDS BenderBot.const_set :SEVERITY_FIELD, config[:severity_field] Bender.const_set :SEVERITY_FIELD, BenderBot::SEVERITY_FIELD Helpers.const_set :SEVERITY_FIELD, BenderBot::SEVERITY_FIELD BenderBot.set_commands config[:without_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 begin room = hipchat[room_id] new_room = room.get_room rescue HipChat::UnknownResponseCode log.error \ error: 'Cannot get room name and topic', reason: 'HipChat returned an unknown response code', room_id: room_id sleep 10 return end 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']) rescue '' 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 Thread.new do loop do begin req_path = '/rest/api/2/group/member' is_last, values, start = false, [], 0 until is_last req_params = QueryParams.encode \ groupname: config[:jira_group], startAt: start 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' resp = http.request req data = JSON.parse resp.body values += data['values'] is_last = data['isLast'] start += data['maxResults'] end user_names = values.map { |u| u['displayName'] } bot.store['group'] = user_names sleep config[:group_refresh] rescue StandardError => e log.error \ message: 'Could not periodically refresh group', error: e.class, data: resp.body sleep 5 end end end end end end