Created
November 18, 2022 22:36
-
-
Save noperator/deeefa2e03b196e088217127cefa24ea to your computer and use it in GitHub Desktop.
Slack un-unreader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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