Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update DOTD Slack Integration #42397

Merged
merged 10 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions aws/ci_build
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,9 @@ def main
ChatClient.message 'server operations', commit_url, color: 'gray', message_format: 'text'
DevelopersTopic.set_dtp 'yes'
InfraProductionTopic.set_dtp_commit GitHub.sha('production')
ChatClient.set_reminder(
'developers',
"@#{DevelopersTopic.dotd}",
"to check Zendesk in 2 hours"
)

# Schedule a reminder for the dotd to check Zendesk in 2 hours
Slack.remind(Slack.user_id(DevelopersTopic.dotd), Time.now.to_i + 7200, 'Reminder: check <https://codeorg.zendesk.com/agent/filters/44863373|Zendesk>')

# Check hoc_mode and hoc_launch only on successful DTPs, so that we get about 1 reminder per
# day to bring staging, test and production to the same hoc_mode and hoc_launch after a
Expand Down
15 changes: 3 additions & 12 deletions bin/dotd
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ def check_for_cdo_keys

puts <<-EOS.unindent

This script requires CDO.github_access_token, CDO.slack_token, and CDO.honeybadger_api_token and CDO.devinternal_db_writer.
This script requires CDO.github_access_token, CDO.slack_bot_token, and CDO.honeybadger_api_token and CDO.devinternal_db_writer.

Create your API tokens from these pages:

https://github.com/settings/tokens ('public_repo' permission)
https://api.slack.com/custom-integrations/legacy-tokens
https://app.honeybadger.io/users/edit#authentication

The slack_bot_token can be found in the Shared-Engineering folder in LastPass, under DOTD Slack Bot Token.

Please add them to your locals.yml and rerun the script.
CDO.devinternal_db_writer should be pulled automatically from AWS Secrets Manager.

Expand Down Expand Up @@ -476,16 +477,6 @@ end
def main
check_for_cdo_keys

# auto-join Slack rooms where the DOTD might be @mentioned
# or need to affect the room topic.
Slack.join_room('developers')
Slack.join_room('deploy-status')
Slack.join_room('infra-staging')
Slack.join_room('infra-test')
Slack.join_room('infra-production')
Slack.join_room('infra-honeybadger')
Slack.join_room('levelbuilder')

dotd_name = ENV['CDODEV_DOTD_NAME'] || ENV['USER']
@logger.info("#{Time.new.strftime('%A, %B %d %Y')}: #{dotd_name} is DOTD")

Expand Down
1 change: 1 addition & 0 deletions config.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ slack_set_last_dtt_green_token:
slack_start_build_token:
slack_endpoint: !Secret
slack_token: !Secret
slack_bot_token: !Secret
slack_log_room: <%=env%>
hip_chat_logging: false
# Logging endpoint used by broken link checker.
Expand Down
8 changes: 0 additions & 8 deletions lib/cdo/chat_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ def self.message(room, message, options={})
)
end

# @param room [String] Name of the Slack channel to post /remind to.
# @param recipient [String] Slack user to remind, include @ in the argument
# @param reminder [String] Message for the /remind commmand
def self.set_reminder(room, recipient, reminder)
message = recipient + " " + reminder
Slack.command(room, "remind", message)
end

def self.snippet(message)
Slack.snippet(CDO.slack_log_room, message)
end
Expand Down
199 changes: 118 additions & 81 deletions lib/cdo/slack.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require 'uri'
require 'net/http'
require 'open-uri'
require 'retryable'
require 'json'
require 'cdo/honeybadger'

class Slack
COLOR_MAP = {
Expand All @@ -17,22 +19,42 @@ class Slack
'production' => 'infra-production'
}.freeze

SLACK_TOKEN = CDO.slack_token.freeze
# Common channel name to ID mappings
CHANNEL_IDS = {
'developers' => 'C0T0PNTM3',
'deploy-status' => 'C7GS8NE8L',
'infra-staging' => 'C03CK8E51',
'infra-test' => 'C03CM903Y',
'infra-production' => 'C03CK8FGX',
'infra-honeybadger' => 'C55JZ1BPZ',
'levelbuilder' => 'C0T10H2HY',
'server-operations' => 'C0CCSS3PX'
}.freeze

SLACK_TOKEN = CDO.methods.include?(:slack_token) ? CDO.slack_token.freeze : nil
SLACK_BOT_TOKEN = CDO.methods.include?(:slack_bot_token) ? CDO.slack_bot_token.freeze : nil

# Returns the user (mention) name of the user.
# WARNING: Does not include the mention character '@'.
# @param email [String] The email of the Slack user.
# @raise [ArgumentError] If the email does not correspond to a Slack user.
# @return [nil | String] The user (mention) name for the Slack user.
def self.user_name(email)
users_list = open("https://slack.com/api/users.list?token=#{SLACK_TOKEN}").
read
members = JSON.parse(users_list)['members']
members = post_to_slack("https://slack.com/api/users.list")['members']
raise "Failed to query users.list" unless members
user = members.find {|member| email == member['profile']['email']}
raise "Slack email #{email} not found" unless user
user['name']
end

def self.user_id(name)
members = post_to_slack("https://slack.com/api/users.list")['members']
raise "Failed to query users.list" unless members
user = members.find {|member| name == member['name']}
raise "Slack user #{name} not found" unless user
user['id']
end

# @param channel_name [String] The channel to fetch the topic.
# @return [String | nil] The existing topic, nil if not found.
def self.get_topic(channel_name, use_channel_map = false)
Expand All @@ -43,25 +65,9 @@ def self.get_topic(channel_name, use_channel_map = false)
channel_id = get_channel_id(channel_name)
return nil unless channel_id

response = Retryable.retryable(on: [Errno::ETIMEDOUT, OpenURI::HTTPError], tries: 2) do
open(
'https://slack.com/api/conversations.info'\
"?token=#{SLACK_TOKEN}"\
"&channel=#{channel_id}"\
)
end

begin
parsed_response = JSON.parse(response.read)
rescue JSON::ParserError
return nil
end

unless parsed_response['ok']
return nil
end

replace_user_links(parsed_response['channel']['topic']['value'])
response = post_to_slack("https://slack.com/api/conversations.info?channel=#{channel_id}")
return nil unless response
replace_user_links(response['channel']['topic']['value'])
end

# @param channel_name [String] The channel to update the topic.
Expand All @@ -77,30 +83,26 @@ def self.update_topic(channel_name, new_topic, use_channel_map = false)
channel_id = get_channel_id(channel_name)
return false unless channel_id

response = open('https://slack.com/api/conversations.setTopic'\
"?token=#{SLACK_TOKEN}"\
"&channel=#{channel_id}"\
"&topic=#{new_topic}"
)
result = JSON.parse(response.read)
raise "Failed to update_topic, with error: #{result['error']}" if result['error']
result['ok']
url = "https://slack.com/api/conversations.setTopic"
payload = {"channel" => channel_id, "topic" => new_topic}
result = post_to_slack(url, payload)
return !!result
end

def self.replace_user_links(message)
message.gsub(/<@(.*?)>/) {'@' + get_display_name($1)}
end

# @param user_id [String] The user whose name you are looking for.
# @return [String] Slack 'display_name' if one is set, otherwise Slack 'name'.
# Returns provided user_id if not found.
def self.get_display_name(user_id)
response = open(
'https://slack.com/api/users.info'\
"?token=#{SLACK_TOKEN}"\
"&user=#{user_id}"\
).read
response = post_to_slack("https://slack.com/api/users.info?user=#{user_id}")

return user_id unless response
parsed_response = JSON.parse(response)
return user_id unless parsed_response['ok']
parsed_response['user']['profile']['display_name']
profile = response['user']['profile']
return profile['display_name'] unless profile['display_name'] == ""
response['user']['name']
end

# For more information about the Slack API, see
Expand All @@ -113,6 +115,7 @@ def self.get_display_name(user_id)
# color (optional): The color the post should be.
# @return [Boolean] Whether the text was posted to Slack successfully.
# WARNING: This function mutates params.
# NOTE: This function utilizes an incoming webhook, not the Slack token
def self.message(text, params={})
return false unless CDO.slack_endpoint
params[:channel] = "\##{Slack::CHANNEL_MAP[params[:channel]] || params[:channel]}"
Expand Down Expand Up @@ -148,64 +151,46 @@ def self.message(text, params={})
end
end

# For more information see
# https://github.com/ErikKalkoken/slackApiDoc/blob/master/chat.command.md. NOTE This API is 'undocumented' and not part of the official Slack APIs.
# @param channel_name [String] Name of the Slack channel to post the command to.
# @param command [String] Command to execute, excluding the /.
# @param message [String] Optional text passed to the command.
# @return [Boolean] Whether the command was posted to Slack successfully.
def self.command(channel_name, command, message="")
channel_id = get_channel_id(channel_name)
response = open(
"https://slack.com/api/chat.command?channel=#{channel_id}"\
"&command=/#{command}"\
"&text=#{message}"\
"&token=#{SLACK_TOKEN}"
)

result = JSON.parse(response.read)
raise "Failed to post command with: #{result['error']}" if result['error']
result['ok']
# Bot tokens are unable to post to reminders.add or chat.command, so we will mimic the functionality of a slack reminder
# by scheduling a DM to the user at a specific time.
# @param recipient_id [String] Slack ID of user to message.
# @param time [String] Unix timestamp of the time the message should be sent.
# @param message [String] Text to be sent in the scheduled message.
def self.remind(recipient_id, time, message)
result = post_to_slack("https://slack.com/api/chat.scheduleMessage", {"channel" => recipient_id, "post_at" => time, "text" => message})
return !!result
end

# @param room [String] Channel name or id to post the snippet.
# @param text [String] Snippet text.
def self.snippet(room, text)
# omit leading '#' when passing channel names to this API
channel = CHANNEL_MAP[room] || room
open('https://slack.com/api/files.upload'\
"?token=#{SLACK_TOKEN}"\
"&content=#{URI.escape(text)}"\
"&channels=#{channel}"
)
result = post_to_slack("https://slack.com/api/files.upload?channels=#{channel}&content=#{URI.escape(text)}")
return !!result
end

# @param name [String] Name of the Slack channel to join.
def self.join_room(name)
response = open(
'https://slack.com/api/conversations.join'\
"?token=#{SLACK_TOKEN}"\
"&channel=#{get_channel_id(name)}"
)

result = JSON.parse(response.read)
raise "Failed to join_room, with error: #{result['error']}" if result['error']
result['ok']
channel = get_channel_id(name)
return false unless channel
result = post_to_slack("https://slack.com/api/conversations.join", {"channel" => channel})
return !!result
end

# Returns the channel ID for the channel with the requested channel_name.
# @param channel_name [String] The name of the Slack channel.
# @return [nil | String] The Slack channel ID for the channel, nil if not
# found.
private_class_method def self.get_channel_id(channel_name)
return CHANNEL_IDS[channel_name] if CHANNEL_IDS[channel_name]

raise "CDO.slack_token undefined" if SLACK_TOKEN.nil?
# Documentation at https://api.slack.com/methods/channels.list.
slack_api_url = "https://slack.com/api/conversations.list"\
"?token=#{SLACK_TOKEN}&limit=1000&types=public_channel&exclude_archived=true"
channels = open(slack_api_url).read
begin
parsed_channels = JSON.parse(channels)
rescue JSON::ParserError
return nil
end
return nil unless parsed_channels['channels']
url = "https://slack.com/api/conversations.list?limit=1000&types=public_channel&exclude_archived=true"
parsed_channels = post_to_slack(url)
return nil unless parsed_channels && parsed_channels['channels']

parsed_channels['channels'].each do |parsed_channel|
return parsed_channel['id'] if parsed_channel['name'] == channel_name
end
Expand All @@ -226,4 +211,56 @@ def self.join_room(name)
gsub(/<\/a>/, '>').
gsub(/<br\/?>/, "\n")
end

private_class_method def self.post_to_slack(url, payload = nil)
if SLACK_BOT_TOKEN && SLACK_BOT_TOKEN != ''
token = SLACK_BOT_TOKEN
else
# TODO: Remove after deprecating legacy SLACK_TOKEN
opts = {
error_class: "Slack integration [warn]",
error_message: "Using legacy token",
context: {url: url, payload: payload}
}
Honeybadger.notify_cronjob_error opts

token = SLACK_TOKEN
end

headers = {
"Content-type" => "application/json; charset=utf-8",
"Authorization" => "Bearer #{token}"
}

uri = URI(url)
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
req = Net::HTTP::Post.new(url, headers)
req.body = payload.to_json if payload

begin
res = https.request(req)
parsed_res = JSON.parse(res.body)
response = parsed_res

unless response['ok']
opts = {
error_class: "Slack integration [error]",
error_message: parsed_res['error'],
context: {url: url, payload: payload, response: parsed_res}
}
Honeybadger.notify_cronjob_error opts
response = false
end
rescue Exception => error
opts = {
error_class: "Slack integration [error]",
error_message: error,
context: {url: url, payload: payload}
}
Honeybadger.notify_cronjob_error opts
response = false
end
response
end
end
Loading