Skip to content

Instantly share code, notes, and snippets.

@zk-1
Created June 29, 2021 00:13
Show Gist options
  • Save zk-1/df97cd356c069cb15456ff33858ede35 to your computer and use it in GitHub Desktop.
Save zk-1/df97cd356c069cb15456ff33858ede35 to your computer and use it in GitHub Desktop.
Migrate profile fields for one or all members of a Slack workspace
#!/usr/bin/env bash
# Name: Slack Profile Field Migrate
# Usage: slack-profile-field-migrate.sh
# Description: Migrate profile fields for one or all members of a Slack workspace
# Author: Zoë Kelly ([email protected])
# License: MIT
# Created: 2021-06
# Updated: 2021-06-28
## Note:
# User Token Scopes required:
# users:read
# users.profile:read
# users.profile:write
# admin
# -- Add your Slack API token here --
app_api_token=""
log_file="$HOME/Desktop/slack-profile-field-migrate-log.csv"
interval_time=2
bold_and_red() { tput setaf 1; tput bold; }
bold_and_green() { tput setaf 2; tput bold; }
bold_and_yellow() { tput setaf 3; tput bold; }
bold_and_blue() { tput setaf 4; tput bold; }
bold_and_grey() { tput setaf 8; tput bold; }
reset_font() { tput sgr0; }
is_macos() { uname -s | grep -qs "Darwin"; }
uriencode() { jq -nr --arg v "$1" '$v|@uri'; }
shh() { "$@" > /dev/null 2>&1; }
bin_exists() { shh type "$@"; }
epoch_to_local() { perl -e '$date = localtime('$1');print "$date\n";'; }
sec_to_time_string() {
printf '%d hours, %d mins, %d secs\n' $(($1/60/60%24)) $(($1%3600/60)) $(($1%60))
}
slack_get() {
curl -s --cookie "$cookie" -X GET \
-H "Accept: application/json" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Bearer $2" \
"https://slack.com/api/$1"
}
slack_post_data() {
curl -s --cookie "$cookie" -X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json; charset=UTF-8" \
-H "Authorization: Bearer $2" \
-d "$3" \
"https://slack.com/api/$1"
}
slack_get_paginate() {
if [[ "$1" == *"?"* ]]; then
_the_url="$1&limit=900"
else
_the_url="$1?limit=900"
fi
_resp_body="$(slack_get "$_the_url" "$2")"
_next_page="$(echo "$_resp_body" | jq -r '.response_metadata.next_cursor')"
while [[ $_next_page != 'null' ]] && [[ -n $_next_page ]]; do
_next_response="$(slack_get "$_the_url&cursor=$_next_page" "$2")"
_resp_body="$_resp_body$_next_response"
_next_page="$(echo "$_next_response" | jq -r '.response_metadata.next_cursor')"
done
echo "$_resp_body"
}
choose_user_profile_field() {
IFS=$'\n'
declare -a _user_fields_array=()
_built_in_fields="Profile: Title title
Profile: Phone phone
Profile: Real Name real_name
Profile: Display Name display_name
Profile: Status Text status_text
Profile: Status Emoji status_emoji
Profile: Pronouns pronouns
"
_custom_fields="$(slack_get "team.profile.get" "$app_api_token" | jq -r '.profile.fields[] | select(.options.is_protected==true | not) | "Custom: " + .label + " " + .id')"
_user_fields_array+=($_built_in_fields $_custom_fields)
_user_fields="$(echo "${_user_fields_array[*]}")"
if [[ ! -n "$_user_fields" ]]; then
return "none"
fi
for _t in $_user_fields ; do
_user_fields_menu_prep="$_user_fields_menu_prep$(echo && printf "%s" "$_t" | awk -F$'\t' '{printf($1)}')"
done
_user_fields_menu="$(echo "$_user_fields_menu_prep")"
PS3="> "
select _uf in $_user_fields_menu; do
echo "$(echo ${_user_fields_array[$(($REPLY-1))]})"
break
done
IFS=$default_IFS
echo "$_the_field"
_user_fields_array=()
_user_fields_menu_prep=""
}
if ! bin_exists jq; then
echo "'jq' binary not found. Please install using 'brew install jq'
if you don't have Homebrew, visit https://brew.sh to install"
exit 1
fi
if ! is_macos; then
echo "This script currently only supports macOS, sorry!"
exit 1
fi
if [[ -z "$app_api_token" ]]; then
read -rp "Enter Slack API token: " app_api_token
fi
caller_user_id="$(slack_get "auth.test" "$app_api_token" | jq -r '.user_id')"
caller_user_is_admin="$(slack_get "users.info?user=$caller_user_id" "$app_api_token" | jq -r '.user.is_admin')"
caller_user_is_primary_owner="$(slack_get "users.info?user=$caller_user_id" "$app_api_token" | jq -r '.user.is_primary_owner')"
if [[ "$caller_user_is_admin" == "" || "$caller_user_is_admin" == "null" ]]; then
bold_and_red; echo "Error identifying calling user, check scopes"; reset_font
exit 1
fi
echo
echo "-------------- Slack Profile Field Migrate --------------"
echo
bold_and_blue; echo "Choose SOURCE field"; reset_font
src_field_info="$(choose_user_profile_field)"
src_field_type="$(echo "$src_field_info" | cut -d ':' -f 1)"
src_field_label="$(echo "$src_field_info" | cut -d ' ' -f 1)"
src_field_id="$(echo "$src_field_info" | cut -d ' ' -f 2)"
echo
bold_and_blue; echo "Choose DESTINATION field"; reset_font
dst_field_info="$(choose_user_profile_field)"
dst_field_type="$(echo "$dst_field_info" | cut -d ':' -f 1)"
dst_field_label="$(echo "$dst_field_info" | cut -d ' ' -f 1)"
dst_field_id="$(echo "$dst_field_info" | cut -d ' ' -f 2)"
echo
declare -a menu_opts=()
menu_opts+=("Copy for one user" "Copy for all users in workspace")
PS3="> "
select menu_opt in "${menu_opts[@]}"; do
i=0
if [[ ${menu_opt} == "${menu_opts[$((i++))]}" ]]; then
pf_scope="user"
elif [[ ${menu_opt} == "${menu_opts[$((i++))]}" ]]; then
pf_scope="all"
else
echo "Bye!"
exit 0
fi
break
done
menu_opts=()
if [[ "$pf_scope" == "user" ]]; then
read -rp "Enter the user's Slack ID: " user_id
user_info="$(slack_get "users.info?user=$user_id" "$app_api_token")"
user_profile_extended="$(slack_get "users.profile.get?user=$user_id" "$app_api_token" | jq '.profile')"
user_info="$(echo "$user_info" | jq --argjson profile "$user_profile_extended" '.user.profile = $profile')"
if echo "$user_info" | jq -r '.ok' | grep -qs 'true'; then
user_name="$(echo "$user_info" | jq -r '.user.profile.real_name')"
user_dispname="$(echo "$user_info" | jq -r '.user.profile.display_name')"
user_title="$(echo "$user_info" | jq -r '.user.profile.title')"
echo "User name: $user_name"
echo "Display name: @$user_dispname"
echo "User ID: $user_id"
echo "Title: $user_title"
else
echo "User not found"
exit 1
fi
target_user_is_admin="$(echo "$user_info" | jq -r '.user.is_admin')"
if [[ "$user_id" != "$caller_user_id" && "$caller_user_is_admin" != "true" ]]; then
bold_and_red; echo "You are not an admin, check scopes"; reset_font
exit 1
elif [[ "$user_id" != "$caller_user_id" && "$target_user_is_admin" == "true" && "$caller_user_is_primary_owner" == "false" ]]; then
bold_and_red; echo "The user you have chosen is an admin of this Workspace, and it's not possible to update fields for another admin unless you are a primary owner."; reset_font
exit 1
else
if [[ "$src_field_type" == "Profile" ]]; then
src_field_now_val="$(echo "$user_info" | jq -r --arg src_field_id "$src_field_id" 'try(.user.profile[$src_field_id])')"
elif [[ "$src_field_type" == "Custom" ]]; then
src_field_now="$(echo "$user_info" | jq --arg src_field_id "$src_field_id" 'try(.user.profile.fields[$src_field_id])')"
src_field_now_val="$(echo "$src_field_now" | jq -r 'try(.value)')"
src_field_now_alt="$(echo "$src_field_now" | jq -r 'try(.alt)')"
fi
if [[ "$dst_field_type" == "Profile" ]]; then
dst_field_now_val="$(echo "$user_info" | jq -r --arg dst_field_id "$dst_field_id" 'try(.user.profile[$dst_field_id])')"
elif [[ "$dst_field_type" == "Custom" ]]; then
dst_field_now="$(echo "$user_info" | jq --arg dst_field_id "$dst_field_id" 'try(.user.profile.fields[$dst_field_id])')"
dst_field_now_val="$(echo "$dst_field_now" | jq -r 'try(.value)')"
dst_field_now_alt="$(echo "$dst_field_now" | jq -r 'try(.alt)')"
fi
[[ "$src_field_now_val" == "null" ]] && src_field_now_val="" || src_field_now_val="${src_field_now_val/\\/\\\\}";
[[ "$src_field_now_alt" == "null" ]] && src_field_now_alt="" || src_field_now_alt="${src_field_now_alt/\\/\\\\}";
[[ "$dst_field_now_val" == "null" ]] && dst_field_now_val="" || dst_field_now_val="${dst_field_now_val/\\/\\\\}";
[[ "$dst_field_now_alt" == "null" ]] && dst_field_now_alt="" || dst_field_now_alt="${dst_field_now_alt/\\/\\\\}";
echo "Source field value: $src_field_now_val"
echo "Source field alt: $src_field_now_alt"
echo "Destination field current value: $dst_field_now_val"
echo "Destination field current alt: $dst_field_now_alt"
echo
read -rp "Proceed with copying '$src_field_label' to '$dst_field_label' for $user_name? (y/n): " proceed_pf
if [[ "$proceed_pf" == "y" ]]; then
if [[ "$dst_field_type" == "Profile" ]]; then
pf_set_res="$(slack_post_data "users.profile.set" "$app_api_token" "{\"user\":\"$user_id\",\"profile\":{\"$dst_field_id\":\"$src_field_now_val\"}}")"
elif [[ "$dst_field_type" == "Custom" ]]; then
pf_set_res="$(slack_post_data "users.profile.set" "$app_api_token" "{\"user\":\"$user_id\",\"profile\":{\"fields\":{\"$dst_field_id\":{\"value\":\"$src_field_now_val\",\"alt\":\"$src_field_now_alt\"}}}}")"
fi
pf_set_res_ok="$(echo "$pf_set_res" | jq -r '.ok')"
echo
if [[ "$pf_set_res_ok" == "true" ]]; then
echo "Done!"
exit 0
else
echo "There was an error"
echo "$pf_set_res"
exit 1
fi
else
exit 1
fi
fi
elif [[ "$pf_scope" == "all" ]]; then
if [[ "$caller_user_is_admin" != "true" ]]; then
bold_and_red; echo "You are not an admin, check scopes"; reset_font
exit 1
fi
echo
if [[ "$caller_user_is_primary_owner" == "true" ]]; then
echo "Now fetching all active, non-bot users. This may take some time.."
all_users_nlsv="$(slack_get_paginate "users.list" "$app_api_token" | jq -r '.members[] | select (.id=="USLACKBOT" | not) | select(.deleted==false and .is_bot==false) | .id')"
else
echo "Now fetching all active, non-bot, non-admin users. This may take some time.."
all_users_nlsv="$(slack_get_paginate "users.list" "$app_api_token" | jq -r '.members[] | select (.id=="USLACKBOT" | not) | select(.deleted==false and .is_bot==false and .is_admin==false) | .id')"
fi
all_users_count=$(echo "$all_users_nlsv" | wc -l | tr -d ' ' )
aprox_total_time="$(sec_to_time_string $((all_users_count * interval_time)))"
echo
echo "$all_users_count users fetched"
echo
read -rp "Also copy over blank values? (y/n): " include_blank_vals
echo
read -rp "Dry-run, or for real? (d/r): " for_realz
echo
echo "▶︎ To keep within rate limits, this feature runs at a $interval_time second interval (quicker for skipped ones)"
echo "▶︎ This feature will not copy fields for Slack admins or owners, since Slack does not allow this."
echo "▶︎ Estimated time required (not accounting for skipped users and rate limit backoffs): $aprox_total_time"
echo "▶︎ Log file will be $log_file"
echo "▶︎ Best viewed on a terminal emulator 180 characters wide or wider:"
echo "************************************************************************************************************************************************************************************"
sleep 2
if [[ "$for_realz" == "r" ]]; then
bold_and_red; echo "This will copy '$src_field_label' to '$dst_field_label' FOR ALL USERS"
read -rp "To confirm, type 'Yes, I am absolutely sure': " pf_cp_all_users_confirm
reset_font
fi
if [[ "$for_realz" == "d" || $pf_cp_all_users_confirm == "Yes, I am absolutely sure" ]]; then
echo
start_epoch="$(date +%s)"
echo "\"timestamp\",\"user_id\",\"user_name\",\"status\",\"src_field\",\"dst_field\",\"value\",\"was_rate_lim\",\"error\"" > $log_file
ani_frame=0; incr=0; successes=0; fails=0; skips=0; rate_limited=0;
was_rate_limited=0
for id in $all_users_nlsv; do
now_epoch="$(date +%s)"
elapsed_time="$(sec_to_time_string $((now_epoch - start_epoch)))"
aprox_time_left="$(sec_to_time_string $(((all_users_count - incr) * interval_time)))"
pf_user_info="$(slack_get "users.info?user=$id" "$app_api_token")"
pf_user_profile_extended="$(slack_get "users.profile.get?user=$id" "$app_api_token" | jq '.profile')"
pf_user_info="$(echo "$pf_user_info" | jq --argjson profile "$pf_user_profile_extended" '.user.profile = $profile')"
pf_user_ok="$(echo "$pf_user_info" | jq -r '.ok')"
pf_user_name="$(echo "$pf_user_info" | jq -r '.user.profile.real_name')"
status_text="$incr / $all_users_count ELAPSED: $elapsed_time EST TIME LEFT: $aprox_time_left ✔:$successes 𐄂:$fails ❏:$skips ⌚︎:$rate_limited USER: $pf_user_name "
case "$ani_frame" in
0) echo " ▝▗ $status_text";;
1) echo " ▖▘ $status_text";;
esac
tput cuu1 tput el
tput sgr0
if [[ $ani_frame == 1 ]]; then ani_frame=0; else ((ani_frame++)); fi
if [[ "$src_field_type" == "Profile" ]]; then
src_field_now_val="$(echo "$pf_user_info" | jq -r --arg src_field_id "$src_field_id" 'try(.user.profile[$src_field_id])')"
elif [[ "$src_field_type" == "Custom" ]]; then
src_field_now="$(echo "$pf_user_info" | jq --arg src_field_id "$src_field_id" 'try(.user.profile.fields[$src_field_id])')"
src_field_now_val="$(echo "$src_field_now" | jq -r 'try(.value)')"
src_field_now_alt="$(echo "$src_field_now" | jq -r 'try(.alt)')"
else
src_field_now_val=""
src_field_now_alt=""
fi
[[ "$src_field_now_val" == "null" ]] && src_field_now_val="" || src_field_now_val="${src_field_now_val/\\/\\\\}";
[[ "$src_field_now_alt" == "null" ]] && src_field_now_alt="" || src_field_now_alt="${src_field_now_alt/\\/\\\\}";
proceed_repeat=1
while [[ "$proceed_repeat" == 1 ]]; do
if [[ "$pf_user_ok" != "true" ]]; then
pf_set_res_ok="fail_user_read"
elif [[ -z "$src_field_now_val" && "$include_blank_vals" != "y" ]]; then
pf_set_res_ok="skip_blank";
else
if [[ "$for_realz" == "r" ]]; then
if [[ "$dst_field_type" == "Profile" ]]; then
pf_set_res="$(slack_post_data "users.profile.set" "$app_api_token" "{\"user\":\"$id\",\"profile\":{\"$dst_field_id\":\"$src_field_now_val\"}}")"
elif [[ "$dst_field_type" == "Custom" ]]; then
pf_set_res="$(slack_post_data "users.profile.set" "$app_api_token" "{\"user\":\"$id\",\"profile\":{\"fields\":{\"$dst_field_id\":{\"value\":\"$src_field_now_val\",\"alt\":\"$src_field_now_alt\"}}}}")"
fi
pf_set_res_ok="$(echo "$pf_set_res" | jq -r '.ok')"
else
pf_set_res_ok="true"
fi
fi
if [[ "$pf_set_res_ok" == "skip_blank" ]]; then
log_status="skip_blank"
pf_final_err=""
proceed_repeat=0
sleep 0.6
((skips++))
elif [[ "$pf_set_res_ok" == "true" ]]; then
log_status="success"
pf_final_err=""
proceed_repeat=0
sleep $interval_time
((successes++))
elif [[ "$pf_set_res_ok" == "fail_user_read" ]]; then
log_status="fail_user_read"
pf_final_err="$(echo "$pf_user_info" | jq -r '.error')"
proceed_repeat=0
sleep 0.6
((fails++))
else
pf_final_err="$(echo "$pf_set_res" | jq -r '.error')"
if [[ "$pf_final_err" == "ratelimited" ]]; then
log_status="rate_limited"
proceed_repeat=1
was_rate_limited=1
((rate_limited++))
sleep 5
elif [[ "$pf_final_err" == "" ]]; then
log_status="fail_blank_resp"
proceed_repeat=0
sleep $interval_time
((fails++))
else
log_status="fail_user_write"
proceed_repeat=0
sleep $interval_time
((fails++))
fi
fi
done
echo "\"$(date +%s)\",\"$id\",\"$pf_user_name\",\"$log_status\",\"$src_field_label\",\"$dst_field_label\",\"$src_field_now_val\",\"$was_rate_limited\",\"$pf_final_err\"" >> $log_file
pf_final_err=""
log_status=""
was_rate_limited=0
((incr++))
done
now_epoch="$(date +%s)"
echo
bold_and_blue; echo "Process completed in $(sec_to_time_string $((now_epoch - start_epoch))), with $successes successes, $fails fails, $skips blanks skipped and $rate_limited rate limits hit."; reset_font
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment