Last active
January 23, 2026 19:38
-
-
Save maxgfr/ff21298a82c6e89199bd33446e518e7c to your computer and use it in GitHub Desktop.
Mattermost teams leaving script
This file contains hidden or 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 | |
| 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