From effa70c19dc5fe2b857d58243e717db5eeb6293b Mon Sep 17 00:00:00 2001 From: Peery Date: Mon, 20 Feb 2023 15:38:59 +0100 Subject: [PATCH] Utility Scripts for the Synapse Admin API A bunch of scripts easing up admin tasks and bigger API usage. For example: - login/logout of a user and saving the credentials to a file - querying a matrix server for account data - local filtering operations on the account data for rooms and users - deleting users via admin API based on the filtered data - deleting rooms via admin API based on the filtered data --- filter_rooms_for_displayname.sh | 20 +++++++++ filter_rooms_for_roomname.sh | 18 ++++++++ filter_rooms_for_userid.sh | 20 +++++++++ filter_users_by_displayname.sh | 19 +++++++++ filter_users_by_userid.sh | 19 +++++++++ log_into_matrix.sh | 20 +++++++++ logout_matrix.sh | 16 +++++++ matrix_delete_rooms_via_admin.sh | 72 ++++++++++++++++++++++++++++++++ matrix_delete_users_via_admin.sh | 68 ++++++++++++++++++++++++++++++ query_account_data.sh | 38 +++++++++++++++++ 10 files changed, 310 insertions(+) create mode 100755 filter_rooms_for_displayname.sh create mode 100755 filter_rooms_for_roomname.sh create mode 100755 filter_rooms_for_userid.sh create mode 100755 filter_users_by_displayname.sh create mode 100755 filter_users_by_userid.sh create mode 100755 log_into_matrix.sh create mode 100755 logout_matrix.sh create mode 100755 matrix_delete_rooms_via_admin.sh create mode 100755 matrix_delete_users_via_admin.sh create mode 100755 query_account_data.sh diff --git a/filter_rooms_for_displayname.sh b/filter_rooms_for_displayname.sh new file mode 100755 index 0000000..ba63c36 --- /dev/null +++ b/filter_rooms_for_displayname.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +input_file=$1 +output_file=$3 +pattern=$2 + +if [ -z $input_file -o -z $pattern ]; then + echo "input file: $input_file, pattern: $pattern" + echo "Usage: $0 <./path/to/matrix-rooms.txt> [./path/to/output.txt]" + exit 0 +fi + +#echo "Converting matrix account data into a comprehensive room list ..." +if [ -z $output_file ]; then + jq ".rooms.join | [to_entries[] | {\"room_id\": .key, \"room_name\": .value.state.events[] | select(.type == \"m.room.name\").content.name, \"users\": [.value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}]} | select(.users[].displayname | match(\"$pattern\")?)] | group_by(.room_id) | map({room_id: .[0].room_id, room_name: .[0].room_name, matches: length, users: .[0].users})" "$input_file" + + +else + jq ".rooms.join | [to_entries[] | {\"room_id\": .key, \"room_name\": .value.state.events[] | select(.type == \"m.room.name\").content.name, \"users\": [.value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}]} | select(.users[].displayname | match(\"$pattern\")?)] | group_by(.room_id) | map({room_id: .[0].room_id, room_name: .[0].room_name, matches: length, users: .[0].users})" "$input_file" > $output_file +fi diff --git a/filter_rooms_for_roomname.sh b/filter_rooms_for_roomname.sh new file mode 100755 index 0000000..909f727 --- /dev/null +++ b/filter_rooms_for_roomname.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +input_file=$1 +output_file=$3 +pattern=$2 + +if [ -z $input_file -o -z $pattern ]; then + echo "input file: $input_file, pattern: $pattern" + echo "Usage: $0 <./path/to/matrix-rooms.txt> [./path/to/output.txt]" + exit 0 +fi + +echo "Converting matrix account data into a comprehensive room list ..." +if [ -z $output_file ]; then + jq ".rooms.join | [to_entries[] | {\"room_id\": .key, \"room_name\": .value.state.events[] | select(.type == \"m.room.name\").content.name} | select(.room_name | match(\"$pattern\"))]" $input_file +else + jq ".rooms.join | [to_entries[] | {\"room_id\": .key, \"room_name\": .value.state.events[] | select(.type == \"m.room.name\").content.name} | select(.room_name | match(\"$pattern\"))]" $input_file > $output_file +fi diff --git a/filter_rooms_for_userid.sh b/filter_rooms_for_userid.sh new file mode 100755 index 0000000..5f6833e --- /dev/null +++ b/filter_rooms_for_userid.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +input_file=$1 +output_file=$3 +pattern=$2 + +if [ -z $input_file -o -z $pattern ]; then + echo "input file: $input_file, pattern: $pattern" + echo "Usage: $0 <./path/to/matrix-rooms.txt> [./path/to/output.txt]" + exit 0 +fi + +#echo "Converting matrix account data into a comprehensive room list ..." +if [ -z $output_file ]; then + jq ".rooms.join | [to_entries[] | {\"room_id\": .key, \"room_name\": .value.state.events[] | select(.type == \"m.room.name\").content.name, \"users\": [.value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}]} | select(.users[].user_id | match(\"$pattern\"))] | group_by(.room_id) | map({room_id: .[0].room_id, room_name: .[0].room_name, matches: length, users: .[0].users})" "$input_file" + + +else + jq ".rooms.join | [to_entries[] | {\"room_id\": .key, \"room_name\": .value.state.events[] | select(.type == \"m.room.name\").content.name, \"users\": [.value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}]} | select(.users[].user_id | match(\"$pattern\"))] | group_by(.room_id) | map({room_id: .[0].room_id, room_name: .[0].room_name, matches: length, users: .[0].users})" "$input_file" > $output_file +fi diff --git a/filter_users_by_displayname.sh b/filter_users_by_displayname.sh new file mode 100755 index 0000000..3d98c7b --- /dev/null +++ b/filter_users_by_displayname.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +input_file=$1 +output_file=$3 +pattern=$2 + +if [ -z $input_file -o -z $pattern ]; then + echo "input file: $input_file, pattern: $pattern" + echo "Usage: $0 <./path/to/matrix-rooms.txt> [./path/to/output.txt]" + exit 0 +fi + +#echo "Converting matrix account data into a comprehensive room list ..." +if [ -z $output_file ]; then + jq ".rooms.join | [to_entries[] | .value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}] | unique_by(.user_id) | map(select(.displayname | match(\"$pattern\")?))" "$input_file" + +else + jq ".rooms.join | [to_entries[] | .value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}] | unique_by(.user_id) | map(select(.displayname | match(\"$pattern\")?))" "$input_file" > "$output_file" +fi diff --git a/filter_users_by_userid.sh b/filter_users_by_userid.sh new file mode 100755 index 0000000..ec469c2 --- /dev/null +++ b/filter_users_by_userid.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +input_file=$1 +output_file=$3 +pattern=$2 + +if [ -z $input_file -o -z $pattern ]; then + echo "input file: $input_file, pattern: $pattern" + echo "Usage: $0 <./path/to/matrix-rooms.txt> [./path/to/output.txt]" + exit 0 +fi + +#echo "Converting matrix account data into a comprehensive room list ..." +if [ -z $output_file ]; then + jq ".rooms.join | [to_entries[] | .value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}] | unique_by(.user_id) | map(select(.user_id | match(\"$pattern\")))" "$input_file" + +else + jq ".rooms.join | [to_entries[] | .value.state.events[] | select(.type == \"m.room.member\") | {\"user_id\": .sender, \"displayname\": .content.displayname}] | unique_by(.user_id) | map(select(.user_id | match(\"$pattern\")))" "$input_file" > "$output_file" +fi diff --git a/log_into_matrix.sh b/log_into_matrix.sh new file mode 100755 index 0000000..1f553a9 --- /dev/null +++ b/log_into_matrix.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +domain=$1 +output_file=$2 +read -p "User: " user +read -p "Password: " password + +if [ -e $output_file ]; then + output_file=matrix-login.txt +fi + +if [ -z $domain -o -z $user -o -z $password ]; then + echo "Domain: $domain, User: $user, Password: $password" + echo "Usage: $0 \"https://matrix.server\" " + exit 0 +fi + +echo "Querying homeserver for new login data ..." +echo curl -XPOST -d "{\"type\":\"m.login.password\", \"user\":\"$user\", \"password\":\"$password\"}" "$domain/_matrix/client/r0/login" -o "$output_file" +curl -XPOST -d "{\"type\":\"m.login.password\", \"user\":\"$user\", \"password\":\"$password\"}" "$domain/_matrix/client/r0/login" -o "$output_file" diff --git a/logout_matrix.sh b/logout_matrix.sh new file mode 100755 index 0000000..136018d --- /dev/null +++ b/logout_matrix.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +domain=$1 +login_data=$2 + + +if [ -z $domain -o -z $login_data ]; then + echo "Usage: $0 \"https://matrix.server\" " + exit 0 +fi + +token=$(cat $login_data | jq -r '.access_token') + +echo "Logging user out ..." +curl -XPOST -H "Authorization: Bearer $token" "$domain/_matrix/client/r0/logout" diff --git a/matrix_delete_rooms_via_admin.sh b/matrix_delete_rooms_via_admin.sh new file mode 100755 index 0000000..d73e149 --- /dev/null +++ b/matrix_delete_rooms_via_admin.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +#' +#This Bash script takes a json array of form [{"room_id": "!asdX:domain.com", "room_name": "nameX"}, {"room_id": "!asdY:domain.com", "room_name": "nameY"}] and iterates over them with the delete room api which is exclusive to Synapse. +#This array is supplied as a file parameter and created with another script (see filter_account_data_rooms_for_pattern.sh) +# +#See: https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#delete-room-api +# +#' + + +set -e + +domain=$1 +login_file=$2 +input_file=$3 +output_folder=$4 + +if [ -z "$domain" -o ! -e "$input_file" -o ! -e "$login_file" ]; then + echo "Domain: $domain, login file: $login_file, input file: $input_file, output folder: $output_folder" + echo "Usage: $0 http(s):// [OUTPUT_FOLDER]" + exit 1 +fi +if [ ! -z "$output_folder" -a ! -d "$output_folder" ]; then + echo "The given output folder ($output_folder) is not a valid directory! Creating it ..." + mkdir $output_folder +fi + +#echo "Querying homeserver for new login data ..." +#echo curl -XPOST -d "{\"type\":\"m.login.password\", \"user\":\"$user\", \"password\":\"$password\"}" "$domain/_matrix/client/r0/login" -o "$output_file" +#curl --globoff -XGET -H "Authorization: Bearer $token" "$domain/_matrix/client/r0/sync?filter={\"room\":{\"timeline\":{\"limit\":1}}}" + +ROOM_COUNT=$(jq '. | length' "$input_file") +echo "Found $ROOM_COUNT rooms to delete in the input file. Example: " +echo $(jq '.[0]' "$input_file") +echo +echo "Other Example: " +echo $(jq '.[-1]' "$input_file") +echo + +read -p "Is this looking correct? (y/n) " reply +if [ "$reply" != "y" -a "$reply" != "Y" ]; then + echo "Aborting ..." + exit 0 +fi + +token=$(jq -r '.access_token' "$login_file") + + +for ((i = 0; i < $ROOM_COUNT; i++ ));do + room_json=$(jq ".[$i]" "$input_file") + room_name=$(echo $room_json | jq -r '.room_name') + room_id=$(echo $room_json | jq -r '.room_id') + room_id_enc=$(echo $room_json | jq -r '.room_id' | sed 's/!/%21/g' | sed 's/:/%3A/g') # room id has to be URL encoded (bootleg version) + echo "Currently purging room $room_name with ID: $room_id ..." + + if [ -d "$output_folder" ]; then + output_file="$output_folder/$room_id_enc-delete_result.txt" + fi + + if [ ! -z "$output_file" ]; then + echo curl --globoff -XDELETE -H "Authorization: Bearer $token" -d "{\"block\": false, \"purge\": true}" "$domain/_synapse/admin/v2/rooms/$room_id_enc" -o "$output_file" + curl --globoff -XDELETE -H "Authorization: Bearer $token" -d "{\"block\": false, \"purge\": true}" "$domain/_synapse/admin/v2/rooms/$room_id_enc" -o "$output_file" + else + echo curl --globoff -XDELETE -H "Authorization: Bearer $token" -d "{\"block\": false, \"purge\": true}" "$domain/_synapse/admin/v2/rooms/$room_id_enc" + curl --globoff -XDELETE -H "Authorization: Bearer $token" -d "{\"block\": false, \"purge\": true}" "$domain/_synapse/admin/v2/rooms/$room_id_enc" + fi + + echo "Sleeping for 5 seconds ..." + sleep 5 + +done diff --git a/matrix_delete_users_via_admin.sh b/matrix_delete_users_via_admin.sh new file mode 100755 index 0000000..9ed2094 --- /dev/null +++ b/matrix_delete_users_via_admin.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +#' +#This Bash script takes a json array of form [{"user_id": "@asdX:domain.com", "displayname": "nameX"}, {"user_id": "@asdY:domain.com", "displayname": null}] and iterates over them with the delete user api which is exclusive to Synapse. +#This array is supplied as a file parameter and created with another script (see filter_users_by_*.sh) +# +#See: https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#deactivate-account +# +#' + + +set -e + +domain=$1 +login_file=$2 +input_file=$3 +output_folder=$4 + +if [ -z "$domain" -o ! -e "$input_file" -o ! -e "$login_file" ]; then + echo "Domain: $domain, login file: $login_file, input file: $input_file, output folder: $output_folder" + echo "Usage: $0 http(s):// [OUTPUT_FOLDER]" + exit 1 +fi +if [ ! -z "$output_folder" -a ! -d "$output_folder" ]; then + echo "The given output folder ($output_folder) is not a valid directory! Creating it ..." + mkdir $output_folder +fi + +USER_COUNT=$(jq '. | length' "$input_file") +echo "Found $USER_COUNT users to delete in the input file. Example: " +echo $(jq '.[0]' "$input_file") +echo +echo "Other Example: " +echo $(jq '.[-1]' "$input_file") +echo + +read -p "Is this looking correct? (y/n) " reply +if [ "$reply" != "y" -a "$reply" != "Y" ]; then + echo "Aborting ..." + exit 0 +fi + +token=$(jq -r '.access_token' "$login_file") + + +for ((i = 0; i < $USER_COUNT; i++ ));do + user_json=$(jq ".[$i]" "$input_file") + user_displayname=$(echo $user_json | jq -r '.displayname') + user_id=$(echo $user_json | jq -r '.user_id') + user_id_enc=$(echo $user_json | jq -r '.user_id' | sed 's/@/%40/g' | sed 's/:/%3A/g') # user id has to be URL encoded (bootleg version) + echo "Currently purging user $user_displayname with ID: $user_id ..." + + if [ -d "$output_folder" ]; then + output_file="$output_folder/$user_id_enc-delete_result.txt" + fi + + if [ ! -z "$output_file" ]; then + echo curl --globoff -XDELETE -H "Authorization: Bearer [TOKEN]" -d "{\"erase\": true}" "$domain/_synapse/admin/v1/deactivate/$user_id_enc" -o "$output_file" + curl --globoff -XDELETE -H "Authorization: Bearer $token" -d "{\"erase\": true}" "$domain/_synapse/admin/v1/deactivate/$user_id_enc" -o "$output_file" + else + echo curl --globoff -XDELETE -H "Authorization: Bearer [TOKEN]" -d "{\"erase\": true}" "$domain/_synapse/admin/v1/deactivate/$user_id_enc" + curl --globoff -XDELETE -H "Authorization: Bearer $token" -d "{\"erase\": true}" "$domain/_synapse/admin/v1/deactivate/$user_id_enc" + fi + + echo "Sleeping for 5 seconds ..." + sleep 5 + +done diff --git a/query_account_data.sh b/query_account_data.sh new file mode 100755 index 0000000..05c2773 --- /dev/null +++ b/query_account_data.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +domain=$2 +login_data=$1 +output_file=$3 + +if [ -z $output_file ]; then + output_file=matrix-rooms.txt +fi + +if [ -z $domain -o ! -f $login_data ]; then + echo "domain: $domain login: $login_data" + echo "Usage: $0 ./path/to/matrix-login.txt \"https://matrix.server\"" + exit 0 +fi + +token=$(cat $login_data | jq -r '.access_token') + +# Queries for a list of room IDs +# jq '.rooms.join' + +# Querying for a list of displaynames +# TODO why are there null's in the list? +# TODO how do I ALSO get the room ID? +# TODO can I just select() in jq to select the room IDs? +# jq '.rooms.join | .[].state.events[].content.channel' matrix-rooms.txt | grep -v null | less + +# Lists all room names out of the m.room.name state event as an array +# jq '.rooms.join | [.[].state.events[] | select(.type == "m.room.name").content.name]' + +# List all room IDs with room names as objects {"room_name": x, "room_id": y} +# jq '.rooms.join | to_entries[] | {"room_id": .key, "room_name": .value.state.events[] | select(.type == "m.room.name").content.name}' + + + +echo "Querying homeserver for room data. This might take a while ..." +echo curl --globoff -XGET -H "Authorization: Bearer [YOUR TOKEN]" "$domain/_matrix/client/r0/sync?filter={"room":{"timeline":{"limit":1}}}" -o "$output_file" +curl --globoff -XGET -H "Authorization: Bearer $token" "$domain/_matrix/client/r0/sync?filter={\"room\":{\"timeline\":{\"limit\":1}}}" -o "$output_file"