Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save maxgfr/ff21298a82c6e89199bd33446e518e7c to your computer and use it in GitHub Desktop.
Mattermost teams leaving script
#!/usr/bin/env bash
set -euo pipefail
# Leave a Mattermost "organization" (team) interactively.
#
# Usage:
# ./script.sh -u <username> -p <password> -n <server_url> [-y]
#
# Notes:
# - Uses the REST API v4:
# - POST /api/v4/users/login
# - GET /api/v4/users/me
# - GET /api/v4/users/me/teams?page=...&per_page=...
# - DELETE /api/v4/teams/{team_id}/members/{user_id}
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; }
MM_USERNAME=""
MM_PASSWORD=""
MM_URL=""
ASSUME_YES=false
while getopts "u:p:n:y" opt; do
case "$opt" in
u) MM_USERNAME="$OPTARG" ;;
p) MM_PASSWORD="$OPTARG" ;;
n) MM_URL="$OPTARG" ;;
y) ASSUME_YES=true ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
:) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
esac
done
if [[ -z "$MM_USERNAME" || -z "$MM_PASSWORD" || -z "$MM_URL" ]]; then
echo "Usage: $0 -u <username> -p <password> -n <server_url> [-y]" >&2
echo " -y Do not prompt for confirmation (leave immediately)" >&2
exit 1
fi
MM_URL="${MM_URL%/}"
COOKIE_JAR=$(mktemp)
HEADERS_LOG=$(mktemp)
BODY_TMP=$(mktemp)
cleanup() {
rm -f "$COOKIE_JAR" "$HEADERS_LOG" "$BODY_TMP"
}
trap cleanup EXIT
# Some Mattermost deployments require using the auth token returned in the
# "Token:" response header, not only the session cookie.
MM_TOKEN=""
mm_curl() {
# Wrapper to consistently send auth (cookie + token when available)
# and follow redirects.
if [[ -n "$MM_TOKEN" ]]; then
curl -sS -L -b "$COOKIE_JAR" \
-H "Authorization: Bearer $MM_TOKEN" \
-H "X-Requested-With: XMLHttpRequest" \
"$@"
else
curl -sS -L -b "$COOKIE_JAR" \
-H "X-Requested-With: XMLHttpRequest" \
"$@"
fi
}
team_index_by_id() {
# Prints the index if found, otherwise prints -1
local search_id="$1"
local i
for i in "${!TEAM_IDS[@]}"; do
if [[ "${TEAM_IDS[$i]}" == "$search_id" ]]; then
echo "$i"
return 0
fi
done
echo "-1"
}
add_team() {
local id="$1"
local name="$2"
local display="$3"
local source="$4"
TEAM_IDS+=("$id")
TEAM_NAMES+=("$name")
TEAM_DISPLAYS+=("$display")
TEAM_SOURCES+=("$source")
}
ensure_team_in_list() {
local id="$1"
local source="$2"
local idx
idx=$(team_index_by_id "$id")
if [[ "$idx" != "-1" ]]; then
case ",${TEAM_SOURCES[$idx]}," in
*",$source,"*) : ;;
*) TEAM_SOURCES[$idx]="${TEAM_SOURCES[$idx]},$source" ;;
esac
return 0
fi
# Not in list: fetch team info best-effort
local team_json name display
team_json=$(mm_curl "$MM_URL/api/v4/teams/$id" || true)
if [[ "$(echo "$team_json" | jq -r 'type' 2>/dev/null || echo other)" == "object" ]]; then
name=$(echo "$team_json" | jq -r '.name // empty')
display=$(echo "$team_json" | jq -r '.display_name // empty')
else
name=""
display=""
fi
[[ -z "$name" ]] && name="$id"
[[ -z "$display" ]] && display="$name"
add_team "$id" "$name" "$display" "$source"
}
echo "=== Mattermost – Leave an organization (team) ==="
echo "Server: $MM_URL"
echo "Username: $MM_USERNAME"
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\"}")
if [[ "$LOGIN_HTTP_CODE" != "200" ]]; then
echo "❌ Login failed (HTTP $LOGIN_HTTP_CODE)" >&2
echo "---- Response headers ----" >&2
cat "$HEADERS_LOG" >&2
exit 1
fi
MM_TOKEN=$(grep -i '^Token:' "$HEADERS_LOG" | head -n 1 | awk '{print $2}' | tr -d '\r' || true)
if ! grep -q "MMAUTHTOKEN" "$COOKIE_JAR" && [[ -z "$MM_TOKEN" ]]; then
echo "❌ Login succeeded but no session cookie and no token header found" >&2
echo "---- Response headers ----" >&2
cat "$HEADERS_LOG" >&2
echo "---- Cookies ----" >&2
cat "$COOKIE_JAR" >&2
exit 1
fi
if [[ -n "$MM_TOKEN" ]]; then
echo "[INFO] Session token obtained (header Token:)"
else
echo "[INFO] Session cookie obtained (MMAUTHTOKEN)"
fi
echo "[INFO] Fetching user ID..."
USER_ID=$(mm_curl "$MM_URL/api/v4/users/me" | jq -r '.id')
if [[ -z "$USER_ID" || "$USER_ID" == "null" ]]; then
echo "❌ Failed to get user ID" >&2
exit 1
fi
echo "[INFO] Fetching your organizations (teams)..."
TEAM_IDS=()
TEAM_NAMES=()
TEAM_DISPLAYS=()
TEAM_SOURCES=()
PAGE=0
PER_PAGE=200
while true; do
TEAMS_PAGE_JSON=$(mm_curl \
"$MM_URL/api/v4/users/me/teams?page=$PAGE&per_page=$PER_PAGE")
if [[ "$(echo "$TEAMS_PAGE_JSON" | jq -r 'type' 2>/dev/null || echo other)" != "array" ]]; then
echo "❌ Failed to fetch teams (unexpected response)." >&2
echo "---- Response ----" >&2
echo "$TEAMS_PAGE_JSON" | jq . >&2 || echo "$TEAMS_PAGE_JSON" >&2
exit 1
fi
COUNT=$(echo "$TEAMS_PAGE_JSON" | jq -r 'length')
if [[ "$COUNT" -eq 0 ]]; then
break
fi
# Extract: id, name, display_name
while IFS=$'\t' read -r id name display; do
[[ -z "$id" || "$id" == "null" ]] && continue
ensure_team_in_list "$id" "member"
# Prefer the membership endpoint's names/display names
idx=$(team_index_by_id "$id")
if [[ "$idx" != "-1" ]]; then
[[ -n "$name" && "$name" != "null" ]] && TEAM_NAMES[$idx]="$name"
[[ -n "$display" && "$display" != "null" ]] && TEAM_DISPLAYS[$idx]="$display"
fi
done < <(echo "$TEAMS_PAGE_JSON" | jq -r '.[] | [.id, .name, .display_name] | @tsv')
if [[ "$COUNT" -lt "$PER_PAGE" ]]; then
break
fi
PAGE=$((PAGE + 1))
if [[ "$PAGE" -gt 100 ]]; then
echo "[WARNING] Reached maximum team page limit (100)." >&2
break
fi
done
# Also include any teams referenced by unread endpoints (useful when /users/me/teams is incomplete)
UNREAD_TEAMS_JSON=$(mm_curl \
"$MM_URL/api/v4/users/me/teams/unread?include_collapsed_threads=true" || true)
if [[ "$(echo "$UNREAD_TEAMS_JSON" | jq -r 'type' 2>/dev/null || echo other)" == "array" ]]; then
while IFS= read -r unread_team_id; do
[[ -z "$unread_team_id" || "$unread_team_id" == "null" ]] && continue
ensure_team_in_list "$unread_team_id" "teams_unread"
done < <(echo "$UNREAD_TEAMS_JSON" | jq -r '.[].team_id' | sort -u)
fi
THREADS_UNREAD_JSON=$(mm_curl \
"$MM_URL/api/v4/users/me/threads/unread" || true)
# Response shape may vary by server version; extract any "team_id" keys recursively.
if [[ "$(echo "$THREADS_UNREAD_JSON" | jq -r 'type' 2>/dev/null || echo other)" != "other" ]]; then
while IFS= read -r threads_team_id; do
[[ -z "$threads_team_id" || "$threads_team_id" == "null" ]] && continue
ensure_team_in_list "$threads_team_id" "threads_unread"
done < <(echo "$THREADS_UNREAD_JSON" | jq -r '.. | .team_id? // empty' | sort -u)
fi
if [[ "${#TEAM_IDS[@]}" -eq 0 ]]; then
echo "[INFO] No teams found for this user. Nothing to leave."
exit 0
fi
echo "[INFO] Teams:"
for i in "${!TEAM_IDS[@]}"; do
idx=$((i + 1))
printf " %2d) %s (%s) [%s]\n" "$idx" "${TEAM_DISPLAYS[$i]}" "${TEAM_NAMES[$i]}" "${TEAM_SOURCES[$i]}"
done
echo
echo "Select the organization (team) to leave."
read -r -p "Enter number (1-${#TEAM_IDS[@]}) or 'q' to quit: " CHOICE
if [[ "$CHOICE" == "q" || "$CHOICE" == "Q" ]]; then
echo "[INFO] Aborted."
exit 0
fi
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]]; then
echo "❌ Invalid choice: $CHOICE" >&2
exit 1
fi
if (( CHOICE < 1 || CHOICE > ${#TEAM_IDS[@]} )); then
echo "❌ Choice out of range." >&2
exit 1
fi
SEL_INDEX=$((CHOICE - 1))
TEAM_ID="${TEAM_IDS[$SEL_INDEX]}"
TEAM_NAME="${TEAM_NAMES[$SEL_INDEX]}"
TEAM_DISPLAY="${TEAM_DISPLAYS[$SEL_INDEX]}"
echo
echo "You are about to leave: $TEAM_DISPLAY ($TEAM_NAME)"
if [[ "$ASSUME_YES" != true ]]; then
read -r -p "Confirm leave? [y/N]: " CONFIRM
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
echo "[INFO] Cancelled."
exit 0
fi
fi
echo "[INFO] Leaving team..."
LEAVE_HTTP_CODE=$(mm_curl -o "$BODY_TMP" -w "%{http_code}" \
-X DELETE \
"$MM_URL/api/v4/teams/$TEAM_ID/members/$USER_ID")
if [[ "$LEAVE_HTTP_CODE" == "200" ]]; then
echo "✅ Left: $TEAM_DISPLAY ($TEAM_NAME)"
exit 0
fi
echo "❌ Failed to leave team (HTTP $LEAVE_HTTP_CODE)" >&2
echo "---- Response ----" >&2
cat "$BODY_TMP" >&2
echo >&2
echo "Tip: if this team is set to 'Invite only' or you're the last team admin, the server may refuse the request." >&2
exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment