Skip to content

Instantly share code, notes, and snippets.

@noperator
Created November 18, 2022 22:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noperator/deeefa2e03b196e088217127cefa24ea to your computer and use it in GitHub Desktop.
Save noperator/deeefa2e03b196e088217127cefa24ea to your computer and use it in GitHub Desktop.
Slack un-unreader
#!/usr/bin/env bash
# Author: @noperator
# Purpose: Mark Slack messages as read when they match given criteria.
# Usage: help
set -euo pipefail
# Make a curl request with required Slack tokens.
curl-slack() {
curl -s \
-A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' \
-H "cookie: d=$COOKIE_TOKEN" \
-F "token=$FORM_TOKEN" \
$@
}
# List channels in the given workspace. Took about 2 min for 160 channels.
convo-list() {
CURSOR=''
PAGE=1
while [[ "$PAGE" -eq 1 || -n "$CURSOR" ]]; do
echo -n '[*] page: ' >&2
printf "%.04d, " "$PAGE" >&2
echo -n 'results: '
DATA=$(
curl-slack \
-F 'types=public_channel,private_channel' \
-F 'limit=200' \
-F 'exclude_archived=true' \
-F "cursor=$CURSOR" \
"https://$WORKSPACE.slack.com/api/conversations.list"
)
echo "$DATA" | jq '.channels | length' | xargs printf "%.04d, " >&2
echo "$DATA" | jq '.channels | map({id, name})' >"channels-$PAGE.json"
echo -n "total: " >&2
cat channels-*.json | jq -s 'map(map(.id)[]) | unique | length' | xargs printf "%.04d, " >&2
echo "cursor: $CURSOR" >&2
CURSOR=$(echo "$DATA" | jq -r '.response_metadata.next_cursor')
PAGE=$((PAGE + 1))
sleep 2
done
}
# Get information about the given channel (viz., the timestamp of last read
# message).
convo-info() {
curl-slack \
-F "channel=$1" \
"https://$WORKSPACE.slack.com/api/conversations.info"
}
# Get messages from a channel's history since the given timestamp.
convo-hist() {
curl-slack \
-F "channel=$1" \
-F "oldest=$2" \
"https://$WORKSPACE.slack.com/api/conversations.history"
}
# Advance the given channel's read cursor to the message at the given
# timestamp.
convo-mark() {
curl-slack \
-F "channel=$1" \
-F "ts=$2" \
"https://$WORKSPACE.slack.com/api/conversations.mark"
}
# Check if a given channel's earliest unread message matches given criteria
# and, if so, advance the channel's read marker to that message.
mark-read() {
CHAN_ID="$1"
echo -n "[*] id: $CHAN_ID, name: " >&2
INFO=$(convo-info "$CHAN_ID")
echo "$INFO" | jq -r '.channel.name' | tr '\n' ',' >&2
echo -n ' unread: ' >&2
HIST=$(convo-hist "$CHAN_ID" $(echo "$INFO" | jq -r '.channel.last_read'))
echo "$HIST" | jq -r '.messages | length' | tr -d '\n' >&2
if [[ $(echo "$HIST" | jq -r '.messages | length') -eq 0 ]]; then
echo >&2
return
fi
echo -n ', marked: ' >&2
TS=$(echo "$HIST" | jq -r --argjson match "$MATCH" '(.messages | map(select(keys | index("subtype") | not)) | sort_by(.ts)[0]) as $msg |
if ($msg | to_entries | map(select(.key as $key | $match | keys | index($key))) | from_entries) == $match
then $msg.ts else null end')
if [[ "$TS" == 'null' ]]; then
echo 'false' >&2
return
fi
convo-mark "$CHAN_ID" "$TS" | jq -r '.ok' >&2
}
help() {
which bat &>/dev/null && cat() { bat -l md --paging never --style plain; }
cat <<EOF >&2
# Slack un-unreader
## Description
When supplied with message-matching criteria, this script will search through
the channels in your Slack workspace and mark any matching messages as read.
This is especially helpful if you have a bot that posts a bunch of messages all
at once across many different channels--but you don't want to simply "mark all
messages read" in case someone _else_ has posted a message after the bot's
message, since you'd miss that person's message.
So, here's how it works:
1. Gets all channels in the workspace.
2. Looks for any channels with unread messages.
3. If the channel's _earliest_ unread message matches the criteria you
specified, then marks that message as read--leaving all the messages below
it as unread.
## Getting started
### Prerequisites
Install \`jq\`: https://github.com/stedolan/jq
### Configure
Requires a few environment variables:
- \`WORKSPACE\`: The first portion of the workspace hostname in
\`<WORKSPACE>.slack.com\` (i.e., excluding the trailing \`.slack.com\`)
- \`MATCH\`: The criteria you want to match a message against to determine
whether it should be marked as read. This JSON object will be compared
directly against messages retrieved from the conversation history API. You'll
probably only care about \`user\`/\`bot_id\` and \`text\`, but see here for
the full list of fields you can match against:
https://api.slack.com/methods/conversations.history#examples
\`\`\`json
{"bot_id":"B01GS6LUXKT","text":"This message was posted by a bot."}
\`\`\`
- \`COOKIE_TOKEN\`: A token supplied as a cookie; looks like \`xoxd-*\`.
To extract this from your Chromium-based browser, visit your Slack workspace
and open Developer Tools:
- Application tab -> Storage -> Cookies -> \`https://app.slack.com\`
- Find cookie named \`d\` -> Double-click Value -> Copy
- \`FORM_TOKEN\`: A token supplied as a form input; looks like \`xoxc-*\`.
To extract this from your Chromium-based browser, visit your Slack workspace
and open Developer Tools:
- Network tab, Refresh page, Filter \`client.boot\` -> Payload -> Form Data
- Find parameter named \`token\` -> Right-click -> Copy
Export these variables in your shell like this, with a leading space before
\`export\` so they don't show up in your shell history (you _do_ have
\`export HISTCONTROL=ignorespace\` in your \`.bashrc\`, right?):
\`\`\`bash
export COOKIE_TOKEN='xoxd-*'
# etc.
\`\`\`
### Usage
First, use the \`get-channels\` command to get a list of all the channels in your
workspace. This'll generate multiple JSON files containing channel IDs and
names, which you can filter down like this:
\`\`\`bash
./$(basename "$0") get-channels
# [*] page: 0001, results: 0032, total: 0032, cursor:
# [*] page: 0002, results: 0005, total: 0037, cursor: Fk5TmUcC3mG7zMErivbZ0h==
# [*] page: 0003, results: 0004, total: 0041, cursor: IOHKyIS08R949SeHSFuQSC==
# ...
cat channels-* | jq -cs 'map(.[] | select(.name | test("^team-")))[]'
# {"id":"C01ZETLGDYF","name":"team-a"}
# {"id":"C03RHMT5IKA","name":"team-b"}
# {"id":"C03XVZ3H3SR","name":"team-3"}
\`\`\`
Now, pass that filtered list of channel IDs back to the \`mark-read\` command.
In the example below, \`#team-a\` had 2 unread messages where the first matched
the \`MATCH\` criteria; \`#team-b\` had no unread messages; and \`#team-3 \`
had a single message that didn't match the criteria.
\`\`\`bash
cat channels-* |
jq -cs 'map(.[] | select(.name | test("^team-")))[]' |
tee /dev/stderr |
jq -r '.id' |
xargs -I {} ./$(basename "$0") mark-read {}
# {"id":"C01ZETLGDYF","name":"team-a"}
# {"id":"C03RHMT5IKA","name":"team-b"}
# {"id":"C03XVZ3H3SR","name":"team-3"}
# [*] id: C01ZETLGDYF, name: team-a, unread: 2, marked: true
# [*] id: C03RHMT5IKA, name: team-b, unread: 0
# [*] id: C03XVZ3H3SR, name: team-3, unread: 1, marked: false
\`\`\`
EOF
! which bat &>/dev/null && cat <<EOF >&2
By the way, this help message will look _way_ better if you install \`bat\`:
https://github.com/sharkdp/bat
EOF
}
main() {
CMD="${1-}"
if [[ -z "$CMD" || "$CMD" =~ ^-*h(elp)?$ ]]; then
help
return
fi
for VAR in WORKSPACE MATCH COOKIE_TOKEN FORM_TOKEN; do
if [[ -z "${!VAR-}" ]]; then
echo "[-] $VAR not set. See \`./$(basename "$0") help\`." >&2
return
fi
done
case "$CMD" in
'get-channels')
convo-list
;;
'mark-read')
mark-read "$2"
;;
*)
echo "[-] Invalid command. See \`./$(basename "$0") help\`." >&2
;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment