Skip to content

Instantly share code, notes, and snippets.

@maxgfr
Last active January 23, 2026 18:42
Show Gist options
  • Select an option

  • Save maxgfr/d836a1349b7e807e9aeafc16e3ed2148 to your computer and use it in GitHub Desktop.

Select an option

Save maxgfr/d836a1349b7e807e9aeafc16e3ed2148 to your computer and use it in GitHub Desktop.
Mattermost read all notification script
#!/usr/bin/env bash
set -euo pipefail
# Requirements:
# - bash >= 4 (associative arrays + mapfile)
# - curl
# - jq
if [[ -z "${BASH_VERSINFO:-}" || "${BASH_VERSINFO[0]}" -lt 4 ]]; then
echo "❌ This script requires bash >= 4 (current: ${BASH_VERSION:-unknown})." >&2
echo " On macOS, install a newer bash (e.g. via Homebrew) and ensure it's first in PATH." >&2
exit 2
fi
command -v curl >/dev/null 2>&1 || { echo "❌ Missing dependency: curl" >&2; exit 2; }
command -v jq >/dev/null 2>&1 || { echo "❌ Missing dependency: jq" >&2; exit 2; }
# Default values
MM_USERNAME=""
MM_PASSWORD=""
MM_URL=""
CACHE_MODE="refresh" # Options: use, refresh, clear
# Parse command-line arguments
while getopts "u:p:n:c:" opt; do
case $opt in
u) MM_USERNAME="$OPTARG" ;;
p) MM_PASSWORD="$OPTARG" ;;
n) MM_URL="$OPTARG" ;;
c) CACHE_MODE="$OPTARG" ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
:) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
esac
done
# Check if all required arguments are provided
if [[ -z "$MM_USERNAME" || -z "$MM_PASSWORD" || -z "$MM_URL" ]]; then
echo "Usage: $0 -u <username> -p <password> -n <server_url> [-c <cache_mode>]"
echo ""
echo "Arguments:"
echo " -u Username"
echo " -p Password"
echo " -n Mattermost server URL (e.g. https://mattermost.example.com)"
echo " -c Cache mode (default: refresh)"
echo " use - Use cached channels if available (default)"
echo " refresh - Ignore cache and refetch all channels"
echo " clear - Delete cache file and exit"
exit 1
fi
# Remove trailing slash from URL to avoid 301 redirects
MM_URL="${MM_URL%/}"
echo "=== Mattermost – Mark all channels and DMs as read ==="
echo "Server: $MM_URL"
echo "Username: $MM_USERNAME"
echo "Cache mode: $CACHE_MODE"
COOKIE_JAR=$(mktemp)
HEADERS_LOG=$(mktemp)
CHANNELS_FILE=""
FAIL_LOG=""
cleanup() {
rm -f "$COOKIE_JAR" "$HEADERS_LOG"
[[ -n "${CHANNELS_FILE}" ]] && rm -f "$CHANNELS_FILE"
[[ -n "${FAIL_LOG}" ]] && rm -f "$FAIL_LOG"
}
trap cleanup EXIT
echo
echo "[INFO] Logging in..."
LOGIN_HTTP_CODE=$(curl -s -w "%{http_code}" \
-D "$HEADERS_LOG" \
-c "$COOKIE_JAR" \
-o /dev/null \
-X POST "$MM_URL/api/v4/users/login" \
-H "Content-Type: application/json" \
-H "X-Requested-With: XMLHttpRequest" \
-d "{
\"login_id\": \"$MM_USERNAME\",
\"password\": \"$MM_PASSWORD\"
}")
echo "[DEBUG] Login HTTP status: $LOGIN_HTTP_CODE"
if [[ "$LOGIN_HTTP_CODE" != "200" ]]; then
echo "❌ Login failed"
echo "---- Response headers ----"
cat "$HEADERS_LOG"
rm -f "$COOKIE_JAR" "$HEADERS_LOG"
exit 1
fi
if ! grep -q "MMAUTHTOKEN" "$COOKIE_JAR"; then
echo "❌ Login succeeded but no session cookie found"
echo "---- Cookies ----"
cat "$COOKIE_JAR"
rm -f "$COOKIE_JAR" "$HEADERS_LOG"
exit 1
fi
echo "✅ Logged in (session cookie obtained)"
# Get user ID
echo "[INFO] Fetching user ID..."
USER_ID=$(curl -s -b "$COOKIE_JAR" \
"$MM_URL/api/v4/users/me" | jq -r '.id')
if [[ "$USER_ID" == "null" || -z "$USER_ID" ]]; then
echo "❌ Failed to get user ID"
rm -f "$COOKIE_JAR" "$HEADERS_LOG"
exit 1
fi
# Define cache file path (based on user ID and server URL)
CACHE_FILE="/tmp/mattermost_channels_${USER_ID}_$(echo "$MM_URL" | sed 's/[^a-zA-Z0-9]/_/g').cache"
# Handle cache mode
if [[ "$CACHE_MODE" == "clear" ]]; then
echo "[INFO] Cache mode: clear"
if [[ -f "$CACHE_FILE" ]]; then
echo "[INFO] Deleting cache file..."
rm -f "$CACHE_FILE"
echo "✅ Cache file deleted"
else
echo "[INFO] No cache file found"
fi
echo "🎉 Done!"
exit 0
fi
USE_CACHE=false
# Check if cache file exists
if [[ "$CACHE_MODE" == "use" && -f "$CACHE_FILE" ]]; then
echo "[INFO] Found cached channels from previous run"
echo "[INFO] Using cached channels to skip fetching..."
USE_CACHE=true
fi
# Get all teams user is a member of
echo "[INFO] Fetching all teams..."
TEAMS_RESPONSE=$(curl -s -b "$COOKIE_JAR" \
"$MM_URL/api/v4/users/$USER_ID/teams")
TEAM_COUNT=$(echo "$TEAMS_RESPONSE" | jq -r 'length')
echo "[INFO] Found $TEAM_COUNT teams"
# Display all teams
for i in $(seq 0 $((TEAM_COUNT - 1))); do
TEAM_NAME=$(echo "$TEAMS_RESPONSE" | jq -r ".[$i].name")
TEAM_DISPLAY_NAME=$(echo "$TEAMS_RESPONSE" | jq -r ".[$i].display_name")
echo "[INFO] - $TEAM_DISPLAY_NAME ($TEAM_NAME)"
done
# Build team ID to name lookup table
declare -A TEAM_LOOKUP
for i in $(seq 0 $((TEAM_COUNT - 1))); do
TEAM_ID=$(echo "$TEAMS_RESPONSE" | jq -r ".[$i].id")
TEAM_DISPLAY_NAME=$(echo "$TEAMS_RESPONSE" | jq -r ".[$i].display_name")
TEAM_LOOKUP["$TEAM_ID"]="$TEAM_DISPLAY_NAME"
done
# Get all channels with pagination
if [[ "$USE_CACHE" == true ]]; then
echo "[INFO] Loading channels from cache..."
ALL_CHANNELS=()
while IFS= read -r CHANNEL_ID; do
[[ -n "$CHANNEL_ID" ]] && ALL_CHANNELS+=("$CHANNEL_ID")
done < "$CACHE_FILE"
# De-duplicate (cache file may contain duplicates if it was appended)
mapfile -t ALL_CHANNELS < <(printf "%s\n" "${ALL_CHANNELS[@]}" | sort -u)
echo "[INFO] Loaded ${#ALL_CHANNELS[@]} channels from cache"
else
echo "[INFO] Fetching all channels..."
PAGE=0
PER_PAGE=200
ALL_CHANNELS=()
DM_CHANNELS=()
declare -A CHANNELS_BY_TEAM
while true; do
echo "[INFO] Fetching page $PAGE..."
CHANNELS_RESPONSE=$(curl -s -b "$COOKIE_JAR" \
"$MM_URL/api/v4/users/$USER_ID/channels?page=$PAGE&per_page=$PER_PAGE")
# Get channel data from this page
PAGE_COUNT=$(echo "$CHANNELS_RESPONSE" | jq -r 'length')
if [[ "$PAGE_COUNT" -eq 0 ]]; then
echo "[INFO] No more channels on page $PAGE"
break
fi
echo "[INFO] Found $PAGE_COUNT channels on page $PAGE"
# Process each channel
for i in $(seq 0 $((PAGE_COUNT - 1))); do
CHANNEL_ID=$(echo "$CHANNELS_RESPONSE" | jq -r ".[$i].id")
TEAM_ID=$(echo "$CHANNELS_RESPONSE" | jq -r ".[$i].team_id")
CHANNEL_TYPE=$(echo "$CHANNELS_RESPONSE" | jq -r ".[$i].type")
ALL_CHANNELS+=("$CHANNEL_ID")
# Track DM channels separately
if [[ "$CHANNEL_TYPE" == "D" || "$CHANNEL_TYPE" == "G" ]]; then
DM_CHANNELS+=("$CHANNEL_ID")
fi
# Get team name from lookup table
if [[ "$TEAM_ID" != "null" && -n "$TEAM_ID" && -n "${TEAM_LOOKUP[$TEAM_ID]-}" ]]; then
TEAM_NAME="${TEAM_LOOKUP[$TEAM_ID]}"
else
TEAM_NAME="Direct Messages"
fi
# Group by team
if [[ -z "${CHANNELS_BY_TEAM[$TEAM_NAME]-}" ]]; then
CHANNELS_BY_TEAM[$TEAM_NAME]=0
fi
CHANNELS_BY_TEAM[$TEAM_NAME]=$(( ${CHANNELS_BY_TEAM[$TEAM_NAME]-0} + 1 ))
done
# Check if we've fetched all channels
if [[ "$PAGE_COUNT" -lt "$PER_PAGE" ]]; then
echo "[INFO] Last page reached"
break
fi
PAGE=$((PAGE + 1))
# Safety limit: max 100 pages
if [[ "$PAGE" -gt 100 ]]; then
echo "[WARNING] Reached maximum page limit (100)"
break
fi
done
# Save channels to cache (overwrite to avoid stale / duplicated IDs)
echo "[INFO] Saving channels to cache for future runs..."
printf "%s\n" "${ALL_CHANNELS[@]}" | sort -u > "$CACHE_FILE"
echo "[INFO] Total channels found: ${#ALL_CHANNELS[@]}"
echo "[INFO] Channels by team:"
for TEAM_NAME in "${!CHANNELS_BY_TEAM[@]}"; do
echo "[INFO] - $TEAM_NAME: ${CHANNELS_BY_TEAM[$TEAM_NAME]} channels"
done
echo "[INFO] Direct messages found: ${#DM_CHANNELS[@]}"
fi
if [[ ${#ALL_CHANNELS[@]} -eq 0 ]]; then
echo "❌ No channels found for this user"
rm -f "$CACHE_FILE"
exit 1
fi
# Mark all channels as read in parallel
echo "[INFO] Marking all channels as read (parallel processing, 20 at a time)..."
# Create a temporary file to store channel IDs
CHANNELS_FILE=$(mktemp)
for CHANNEL_ID in "${ALL_CHANNELS[@]}"; do
echo "$CHANNEL_ID" >> "$CHANNELS_FILE"
done
FAIL_LOG=$(mktemp)
# Mattermost has (at least) two relevant endpoints:
# - POST /api/v4/channels/members/me/view -> clears unread + mention counts (badge/pastille)
# - POST /api/v4/users/me/channels/{channel_id}/posts/read -> marks posts as read (older clients)
mark_channel_read() {
local channel_id="$1"
# 1) Prefer "view" because it clears mention counts too.
local view_code
view_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST -b "$COOKIE_JAR" \
"$MM_URL/api/v4/channels/members/me/view" \
-H "Content-Type: application/json" \
-d "{\"channel_id\":\"$channel_id\",\"prev_channel_id\":\"$channel_id\"}" \
) || view_code="000"
if [[ "$view_code" != "200" ]]; then
# Some servers ignore/complain about prev_channel_id; retry without it.
view_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST -b "$COOKIE_JAR" \
"$MM_URL/api/v4/channels/members/me/view" \
-H "Content-Type: application/json" \
-d "{\"channel_id\":\"$channel_id\"}" \
) || view_code="000"
fi
# 2) Extra: mark posts read (best-effort; some servers use only one of the two).
local posts_code
posts_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST -b "$COOKIE_JAR" \
"$MM_URL/api/v4/users/me/channels/$channel_id/posts/read" \
) || posts_code="000"
if [[ "$view_code" == "200" || "$posts_code" == "200" ]]; then
return 0
fi
echo "channel_id=$channel_id view=$view_code posts_read=$posts_code" >> "$FAIL_LOG"
return 0
}
export MM_URL COOKIE_JAR FAIL_LOG
export -f mark_channel_read
# Process channels in parallel using xargs (bash needed for exported function)
cat "$CHANNELS_FILE" | xargs -P 20 -n 1 bash -c 'mark_channel_read "$1"' _
FAIL_COUNT=$(wc -l < "$FAIL_LOG" | tr -d ' ')
TOTAL_COUNT=${#ALL_CHANNELS[@]}
SUCCESS_COUNT=$((TOTAL_COUNT - FAIL_COUNT))
echo "[INFO] Processed $SUCCESS_COUNT/$TOTAL_COUNT channels"
if [[ "$FAIL_COUNT" -gt 0 ]]; then
echo "[WARNING] $FAIL_COUNT channels could not be marked as read/viewed. First failures:"
head -n 10 "$FAIL_LOG" | sed 's/^/[WARNING] /'
fi
rm -f "$CHANNELS_FILE"
echo "✅ All ${#ALL_CHANNELS[@]} channels are now marked as read"
# Best-effort: also mark all threads as read (if the server supports it)
for i in $(seq 0 $((TEAM_COUNT - 1))); do
TEAM_ID=$(echo "$TEAMS_RESPONSE" | jq -r ".[$i].id")
THREADS_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X PUT -b "$COOKIE_JAR" \
"$MM_URL/api/v4/users/me/teams/$TEAM_ID/threads/read" \
) || THREADS_CODE="000"
if [[ "$THREADS_CODE" == "200" ]]; then
echo "[INFO] Marked threads as read for team_id=$TEAM_ID"
fi
done
# Note: Mentions endpoint not available on this server
echo "[INFO] Note: Mentions endpoint not available on this Mattermost server"
echo "[INFO] Skipping mentions processing"
echo "🎉 All channels and DMs are now marked as read!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment