Skip to content

Instantly share code, notes, and snippets.

@simonbs
Last active November 23, 2021 02:37
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save simonbs/a66c86041f454995957b1cb8db8b7209 to your computer and use it in GitHub Desktop.
Save simonbs/a66c86041f454995957b1cb8db8b7209 to your computer and use it in GitHub Desktop.
Peforms OAuth flow against Slack and sets a Slack Status. Meant to be used with Shortcuts.
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: pink; icon-glyph: user;
// In order to use the script, you must create a Slack app and install it on your workspace. Go here to create your app: api.slack.com/apps
// There's two important configurations you must make to the app:
// 1. Add the users.profile:write scope to the app. This scope is necessary to set a Slack status.
// 2. Add the following redirect URL, so Slack will redirect to Scriptable after authorizing.
// https://open.scriptable.app
// Run the script to grant your newly created app authorization to access your Slack account. After you've done this, you can use the script in Shortcuts.
// Prefix used for items in the keychain. The script could be updated to take this as input and differentiate between client IDs, client secrets and access tokens stored in the keychain.
let KEYCHAIN_PREFIX = "status_helper"
// Input paramters that are used to choose the action to perform when running the script.
let code = args.queryParameters.code
let action = args.shortcutParameter
if (code != null) {
// Swapping an OAuth code for an access token.
let clientID = await getSlackClientID(KEYCHAIN_PREFIX)
let clientSecret = await getSlackClientSecret(KEYCHAIN_PREFIX)
let accessToken = await swapCodeForAccessToken(clientID, clientSecret)
storeAccessToken(KEYCHAIN_PREFIX, accessToken)
await presentMenu()
} else if (action != null) {
let type = action.type
if (type == "set_status") {
let accessToken = getAccessToken(KEYCHAIN_PREFIX)
let result = await setStatus(accessToken, action.emoji, action.text)
Script.setShortcutOutput(result)
Script.complete()
} else if (type == "clear_status") {
let accessToken = getAccessToken(KEYCHAIN_PREFIX)
let result = await setStatus(accessToken, "", "")
Script.setShortcutOutput(result)
Script.complete()
} else {
let result = {"error": "Unsupported action: " + action.type}
Script.setShortcutOutput(result)
Script.complete()
}
} else {
await presentMenu()
}
/**
* Menu
*/
async function presentMenu() {
let accessToken = getAccessToken(KEYCHAIN_PREFIX)
let alert = new Alert()
alert.title = Script.name()
if (accessToken != null) {
alert.title = "You're authorized and can use the script in Shortcuts."
} else {
alert.message = "You must authorize to use the script."
}
alert.addAction("Authorize")
alert.addDestructiveAction("Clear Client ID and Client Secret")
alert.addDestructiveAction("Clear Access Token")
alert.addCancelAction("Cancel")
let idx = await alert.presentAlert()
if (idx == 0) {
let clientInfo = await getSlackClientIDAndClientSecret(KEYCHAIN_PREFIX)
if (clientInfo != null) {
authorize(clientInfo["client_id"])
}
} else if (idx == 1) {
clearClientIDAndClientSecret(KEYCHAIN_PREFIX)
} else if (idx == 2) {
clearAccessToken(KEYCHAIN_PREFIX)
}
}
function clearClientIDAndClientSecret(prefix) {
let clientIDKey = prefix + ".client_id"
let clientSecretKey = prefix + ".client_secret"
if (Keychain.contains(clientIDKey)) {
Keychain.remove(clientIDKey)
}
if (Keychain.contains(clientSecretKey)) {
Keychain.remove(clientSecretKey)
}
}
function clearAccessToken(prefix) {
let key = prefix + ".access_token"
if (Keychain.contains(key)) {
Keychain.remove(key)
}
}
/**
* Shortcuts
*/
async function setStatus(accessToken, emoji, text) {
let url = "https://slack.com/api/users.profile.set"
let req = new Request(url)
req.method = "POST"
req.headers = {
"Authorization": "Bearer " + accessToken,
"Content-Type": "application/json; charset=utf8"
}
req.body = JSON.stringify({
"profile": {
"status_emoji": emoji,
"status_text": text,
"status_expiration": 0
}
})
return await req.loadJSON()
}
/**
* Helpers
*/
function authorize(clientID) {
let url = "https://slack.com/oauth/authorize"
+ "?client_id=" + clientID
+ "&scope=users.profile:write"
+ "&redirect_uri=" + getRedirectURI()
Safari.open(url)
}
async function swapCodeForAccessToken(clientID, clientSecret) {
let url = "https://slack.com/api/oauth.access"
+ "?client_id=" + clientID
+ "&client_secret=" + clientSecret
+ "&code=" + code
+ "&redirect_uri=" + getRedirectURI()
let req = new Request(url)
let json = await req.loadJSON()
return json["access_token"]
}
function getRedirectURI() {
let scriptName = encodeURIComponent(Script.name())
return "https://open.scriptable.app/run?scriptName=" + scriptName
}
async function getSlackClientIDAndClientSecret(prefix) {
let clientID = await getSlackClientID(KEYCHAIN_PREFIX)
if (clientID != null) {
let clientSecret = await getSlackClientSecret(KEYCHAIN_PREFIX)
if (clientSecret != null) {
log(clientID)
log(clientSecret)
return {"client_id": clientID, "client_secret": clientSecret}
} else {
return null
}
} else {
return null
}
}
async function getSlackClientID(prefix) {
let key = prefix + ".client_id"
let title = "Enter Slack Client ID"
let message = "You can obtain your client ID from api.slack.com/apps"
let placeholder = "Client ID"
return await getFromKeychainOrPrompt(key, title, message, placeholder)
}
async function getSlackClientSecret(prefix) {
let key = prefix + ".client_secret"
let title = "Enter Slack Client Secret"
let message = "You can obtain your client secret from api.slack.com/apps"
let placeholder = "Client Secret"
return await getFromKeychainOrPrompt(key, title, message, placeholder)
}
async function getFromKeychainOrPrompt(key, title, message, placeholder) {
if (Keychain.contains(key)) {
return Keychain.get(key)
} else {
let alert = new Alert()
alert.title = title
alert.message = message
alert.addTextField(placeholder)
alert.addCancelAction("Cancel")
alert.addAction("Save")
await alert.present()
let value = alert.textFieldValue(0)
if (value != null && value.length > 0) {
Keychain.set(key, value)
return value
} else {
return null
}
}
}
function getAccessToken(prefix) {
let key = prefix + ".access_token"
if (Keychain.contains(key)) {
return Keychain.get(key)
} else {
return null
}
}
function storeAccessToken(prefix, accessToken) {
let key = prefix + ".access_token"
Keychain.set(key, accessToken)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment