summaryrefslogtreecommitdiff
path: root/betterdiscordctl
diff options
context:
space:
mode:
Diffstat (limited to 'betterdiscordctl')
-rwxr-xr-xbetterdiscordctl661
1 files changed, 661 insertions, 0 deletions
diff --git a/betterdiscordctl b/betterdiscordctl
new file mode 100755
index 0000000..00d9a19
--- /dev/null
+++ b/betterdiscordctl
@@ -0,0 +1,661 @@
+#!/usr/bin/env bash
+
+set -ueo pipefail
+shopt -s dotglob extglob nullglob
+
+# Constants
+VERSION=1.7.0
+SOURCE=$(readlink -f "${BASH_SOURCE[0]}")
+DISABLE_UPGRADE=
+
+# Options
+cmd=status
+verbosity=0
+scan=(/opt /usr/share /usr/lib64)
+flavors=('' canary ptb)
+discord=
+modules=
+bd_repo='https://github.com/rauenzi/BetterDiscordApp'
+bd_repo_branch=injector
+bd=
+copy_bd=
+snap=
+flatpak=
+upgrade_url='https://github.com/bb010g/betterdiscordctl/raw/master/betterdiscordctl'
+
+# Variables
+flavor=
+core=
+xdg_config=${XDG_CONFIG_HOME:-$HOME/.config}
+data=${XDG_DATA_HOME:-$HOME/.local/share}/betterdiscordctl
+snap_bin=snap
+flatpak_bin=flatpak
+
+show_help() {
+ cat << EOF
+Usage: ${0##*/} [COMMAND] [OPTION...]
+
+Options:
+ -V, --version Display version info and exit
+ -h, --help Display this help message and exit
+ -v, --verbose Increase verbosity
+ -s, --scan=DIRECTORIES Colon-separated list of directories to scan for
+ a Discord installation
+ (default '/opt:/usr/share')
+ -f, --flavors=FLAVORS Colon-separated list of Discord flavors
+ (default ':canary:ptb')
+ -d, --discord=DIRECTORY Use the specified Discord directory
+ (requires --modules)
+ -m, --modules=DIRECTORY Use the specified Discord modules directory
+ -r, --bd-repo=REPOSITORY Use the specified Git repo for BetterDiscord
+ --bd-repo-branch=BRANCH Use the specified Git branch for BetterDiscord
+ (default 'stable16')
+ -b, --betterdiscord=DIRECTORY Use the specified BetterDiscord directory
+ -c, --copy-bd Copy BD directory instead of symlinking
+ --snap[=COMMAND] Use the Snap version of Discord (optionally
+ using the specified snap command)
+ --flatpak[=COMMAND] Use the Flatpak version of Discord (optionally
+ using the specified flatpak command)
+ --upgrade-url=URL Custom URL to upgrade betterdiscordctl with
+
+Commands:
+ status (default) Show the current Discord patch state.
+ install Install BetterDiscord.
+ reinstall Reinstall BetterDiscord.
+ update Update BetterDiscord.
+ uninstall Uninstall BetterDiscord.
+ upgrade Update betterdiscordctl.
+EOF
+}
+
+verbose() {
+ if (( verbosity >= $1 )); then
+ shift
+ printf '%s\n' "$1" >&2
+ fi
+}
+
+die() {
+ while [ $# -gt 0 ]; do
+ printf '%s\n' "$1" >&2
+ shift
+ done
+ exit 1
+}
+
+die_with_help() {
+ die "$@" 'Use "--help" for more information.'
+}
+
+die_non_empty() {
+ die_with_help "ERROR: \"$1\" requires a non-empty option argument."
+}
+
+while :; do
+ if [[ -z ${1+x} ]]; then break; fi
+ case $1 in
+ status|install|reinstall|update|uninstall|upgrade)
+ cmd=$1
+ ;;
+ -V|--version)
+ printf 'betterdiscordctl %s\n' "$VERSION" >&2
+ exit
+ ;;
+ -h|-\?|--help)
+ show_help; exit
+ ;;
+ -v|--verbose)
+ ((++verbosity))
+ ;;
+ -s|--scan)
+ if [[ ${2+x} ]]; then IFS=':' read -ra scan <<< "$2"; shift
+ else die_non_empty '--scan'; fi
+ ;;
+ --scan=?*)
+ IFS=':' read -ra scan <<< "${1#*=}"
+ ;;
+ --scan=)
+ die_non_empty '--scan'
+ ;;
+ -f|--flavors)
+ if [[ ${2+x} ]]; then IFS=':' read -ra flavors <<< "$2"; shift
+ else die_non_empty '--flavors'; fi
+ ;;
+ --flavors=?*)
+ IFS=':' read -ra flavors <<< "${1#*=}"
+ ;;
+ --flavors=)
+ die_non_empty '--flavors'
+ ;;
+ -d|--discord)
+ if [[ ${2+x} ]]; then discord=$2; shift
+ else die_non_empty '--discord'; fi
+ ;;
+ --discord=?*)
+ discord=${1#*=}
+ ;;
+ --discord=)
+ die_non_empty '--discord'
+ ;;
+ -m|--modules)
+ if [[ ${2+x} ]]; then modules=$2; shift
+ else die_non_empty '--modules'; fi
+ ;;
+ --modules=?*)
+ modules=${1#*=}
+ ;;
+ --modules=)
+ die_non_empty '--modules'
+ ;;
+ --bd-repo-branch)
+ if [[ ${2+x} ]]; then bd_repo_branch=$2; shift
+ else die_non_empty '--bd-repo-branch'; fi
+ ;;
+ --bd-repo-branch=?*)
+ bd_repo_branch=${1#*=}
+ ;;
+ --bd-repo-branch=)
+ die_non_empty '--bd-repo-branch'
+ ;;
+ -r|--bd-repo)
+ if [[ ${2+x} ]]; then bd_repo=$2; shift
+ else die_non_empty '--bd-repo'; fi
+ ;;
+ --bd-repo=?*)
+ bd_repo=${1#*=}
+ ;;
+ --bd-repo=)
+ die_non_empty '--bd-repo'
+ ;;
+ -b|--betterdiscord)
+ if [[ ${2+x} ]]; then bd=$2; shift
+ else die_non_empty '--betterdiscord'; fi
+ ;;
+ --betterdiscord=?*)
+ bd=${1#*=}
+ ;;
+ --betterdiscord=)
+ die_non_empty '--betterdiscord'
+ ;;
+ -c|--copy-bd)
+ copy_bd=yes
+ ;;
+ --snap)
+ snap=yes
+ copy_bd=yes
+ ;;
+ --snap=?*)
+ snap=yes
+ copy_bd=yes
+ snap_bin=${1#*=}
+ ;;
+ --snap=)
+ die_non_empty '--snap'
+ ;;
+ --flatpak)
+ flatpak=yes
+ copy_bd=yes
+ ;;
+ --flatpak=?*)
+ flatpak=yes
+ copy_bd=yes
+ flatpak_bin=${1#*=}
+ ;;
+ --flatpak=)
+ die_non_empty '--flatpak'
+ ;;
+ --upgrade-url)
+ if [[ ${2+x} ]]; then upgrade_url=$2; shift
+ else die_non_empty '--upgrade-url'; fi
+ ;;
+ --upgrade-url=?*)
+ upgrade_url=${1#*=}
+ ;;
+ --upgrade-url=)
+ die_non_empty '--upgrade-url'
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -?*)
+ printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
+ ;;
+ *)
+ break
+ esac
+ shift
+done
+
+mkdir -p "$data"
+[[ -f $data/bd_map ]] || touch "$data/bd_map"
+
+# Commands
+
+bdc_status() {
+ index_mod=no
+ linked_dir=no
+ linked_repo=no
+ if [[ -d $core/injector ]]; then
+ if [[ -h $core/injector ]]; then
+ linked_dir=$(readlink "$core/injector")
+ if pushd "$core/injector" >/dev/null; then
+ linked_repo=$(git remote get-url origin 2>/dev/null || printf 'no\n')
+ popd >/dev/null
+ else
+ linked_dir="(broken link) $linked_dir"
+ fi
+ fi
+ fi
+ if [[ ! -f $core/index.js ]]; then
+ index_mod='(missing) no'
+ else
+ grep -q 'injector' "$core/index.js" && index_mod=yes
+ fi
+
+ printf 'Discord: %s
+Modules: %s
+Index modified: %s
+Linked injector directory: %s
+Linked injector repository: %s\n' \
+ "$discord" "$modules" "$index_mod" "$linked_dir" "$linked_repo"
+}
+
+bdc_install() {
+ [[ -d $core/injector ]] && die 'ERROR: Already installed.'
+
+ # Clean up legacy cruft
+ if [[ -d $core/core ]]; then
+ printf 'Removing legacy core directory...\n' >&2
+ rm -rf "$core/core"
+ fi
+
+ bd_patch
+ bd_injector
+
+ printf 'Installed. (Restart Discord if necessary.)\n' >&2
+}
+
+bdc_reinstall() {
+ [[ -d $core/injector ]] || die 'Not installed.'
+
+ bdc_kill
+
+ verbose 1 'V: Removing old injector folder.'
+ rm -rf "$core/injector"
+
+ bd_patch
+ bd_injector
+
+ printf 'Reinstalled.\n' >&2
+}
+
+bdc_update() {
+ [[ -d $core/injector ]] || die 'Not installed.'
+
+ if ! pushd "$core/injector" >/dev/null; then
+ if [[ -h $core/injector ]]; then
+ die 'ERROR: BetterDiscord injector symbolic link is broken.'
+ else
+ die 'ERROR: BetterDiscord injector location is not a directory.'
+ fi
+ fi
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+ printf 'Updating Git repository...\n' >&2
+ git fetch origin "$bd_repo_branch"
+ git reset --hard FETCH_HEAD
+ else
+ printf 'WARN: No Git repository found.\n' >&2
+ fi
+ popd >/dev/null
+}
+
+bdc_uninstall() {
+ [[ -d $core/injector ]] || die 'Not installed.'
+
+ bdc_kill
+ bd_unpatch
+
+ # Remove managed BD repo if applicable
+ bd_n=$(bd_map_get_dir "$discord" | bd_map_entry_n)
+ bd_map_remove "$discord"
+ if [[ -z $(bd_map_get_n "$bd_n") ]]; then
+ verbose 2 "VV: Removing $data/bd/$bd_n"
+ rm -rf "$data/bd/$bd_n"
+ fi
+
+ printf 'Uninstalled.\n' >&2
+}
+
+bdc_upgrade() {
+ if [[ $DISABLE_UPGRADE ]]; then
+ die 'ERROR: Upgrading has been disabled.' \
+ 'If you installed this from a package, its maintainer should keep it up to date.'
+ fi
+
+ github_version=$(curl -NLSs "$upgrade_url" | sed -n 's/^VERSION=//p')
+ if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
+ die "ERROR: GitHub couldn't be reached to check the version."
+ fi
+ verbose 2 "VV: Script location: $SOURCE"
+ verbose 2 "VV: Upgrade URL: $upgrade_url"
+ verbose 1 "V: Local version: $VERSION"
+ verbose 1 "V: GitHub version: $github_version"
+ semver_diff=$(Semver::compare "$github_version" "$VERSION")
+ if [[ $semver_diff -eq 1 ]]; then
+ printf 'Downloading betterdiscordctl...\n' >&2
+ if curl -LSso "$SOURCE" "$upgrade_url"; then
+ printf 'Successfully updated betterdiscordctl.\n' >&2
+ else
+ die 'ERROR: Failed to update betterdiscordctl.' \
+ 'You may want to rerun this command with sudo.'
+ fi
+ else
+ if [[ $semver_diff -eq 0 ]]; then
+ printf 'betterdiscordctl is already the latest version (%s).\n' "$VERSION" >&2
+ else
+ printf 'Local version (%s) is higher than GitHub version (%s).\n' "$VERSION" "$github_version" >&2
+ fi
+ fi
+}
+
+# Implementation functions
+
+bdc_main() {
+ if [[ -z $discord ]]; then
+ if [[ $snap ]]; then bdc_snap
+ elif [[ $flatpak ]]; then bdc_flatpak
+ else bdc_scan; fi
+ else
+ flavor=$flavors
+ # --discord and --modules
+ [[ -z $modules ]] && die_with_help 'ERROR: "--discord" requires "--modules" to also be set.'
+ [[ -d $discord ]] || die 'ERROR: Discord installation not found.'
+ [[ -d $modules ]] || die 'ERROR: Discord modules directory not found.'
+ fi
+ [[ -d $discord ]] || die 'ERROR: Discord installation not found. Try specifying it with "--discord".'
+ core=$modules/discord_desktop_core
+ [[ -d $core ]] || die "ERROR: Directory 'discord_desktop_core' not found in $(readlink -f "$modules")"
+}
+
+bdc_scan() {
+ for scandir in "${scan[@]}"; do
+ verbose 2 "VV: Scanning $scandir"
+ for flavor in "${flavors[@]}"; do
+ verbose 2 "VV: Trying flavor '$flavor'"
+ shopt -s nocaseglob
+ for discord in "$scandir"/discord?(-)"$flavor"; do
+ shopt -u nocaseglob
+ if [[ -d $discord ]]; then
+ verbose 1 "V: Using Discord at $discord"
+ discord_config=$xdg_config/discord${flavor,,}
+ if [[ ! -d $discord_config ]]; then
+ printf 'WARN: Config directory not found for Discord %s (%s, %s).\n' \
+ "$flavor" "$discord" "$discord_config" >&2
+ continue 2
+ fi
+ if [[ -z $modules ]]; then
+ bdc_find_modules
+ else
+ # --modules
+ [[ -d $modules ]] || die 'ERROR: Discord modules directory not found.'
+ fi
+ break 3
+ fi
+ done
+ done
+ done
+}
+
+bdc_find_modules() {
+ declare -a all_modules
+ all_modules=("$discord_config/"+([0-9]).+([0-9]).+([0-9])/modules)
+ ((${#all_modules[@]})) || die 'ERROR: Discord modules directory not found.'
+ modules=${all_modules[-1]}
+ verbose 1 "V: Found modules in $modules"
+}
+
+bdc_snap() {
+ # shellcheck disable=SC2016
+ # Expansion should happen inside snap's shell.
+ snap_location=$("$snap_bin" run --shell discord <<< $'printf -- \'%s\n\' "$SNAP" 1>&3' 3>&1)
+ discord=${snap_location:?}/usr/share/discord
+ verbose 2 "VV: Checking $discord"
+ if [[ -d $discord ]]; then
+ verbose 1 "V: Using Discord at $discord"
+ # shellcheck disable=SC2016
+ # Expansion should happen inside snap's shell.
+ xdg_config=$("$snap_bin" run --shell discord <<< $'printf -- \'%s/.config\n\' "$SNAP_USER_DATA" 1>&3' 3>&1)
+ discord_config=$xdg_config/discord
+ bdc_find_modules
+ else
+ die 'ERROR: Discord installation not found.'
+ fi
+}
+
+bdc_flatpak() {
+ flatpak_version=$("$flatpak_bin" --version | sed -n 's/Flatpak //p')
+ if [[ $(Semver::compare "$flatpak_version" '1.0.0') -eq -1 ]]; then
+ die 'ERROR: You are using an unsupported version of Flatpak.' \
+ 'See https://github.com/bb010g/betterdiscordctl/issues/45'
+ fi
+ # flatpak sucks and doesn't use stderr for warnings.
+ # https://github.com/flatpak/flatpak/blob/13e449b/app/flatpak-main.c#L259-L286
+ # This really should be better for directories with newlines, but...
+ # We're just going to grab the last line and hope for the best.
+ flatpak_location=$("$flatpak_bin" info --show-location com.discordapp.Discord)
+ flatpak_location=${flatpak_location##*$'\n'}
+ if [[ -d ${flatpak_location:?}/files/discord ]]; then
+ discord=$flatpak_location/files/discord
+ else
+ discord=$flatpak_location/files/extra
+ fi
+ verbose 2 "VV: Checking $discord"
+ if [[ -d $discord ]]; then
+ verbose 1 "V: Using Discord at $discord"
+ # shellcheck disable=SC2016
+ # Expansion should happen inside flatpak's shell.
+ flatpak_config=$("$flatpak_bin" run --command=sh com.discordapp.Discord -c $'printf -- \'%s\n\' "$XDG_CONFIG_HOME"')
+ discord_config=${flatpak_config:-$HOME/.var/app/com.discordapp.Discord/config}/discord
+ if [[ ! -d $discord_config ]]; then
+ printf 'WARN: Config directory not found for Discord (%s, %s).\n' "$discord" "$discord_config" >&2
+ fi
+ bdc_find_modules
+ else
+ die 'ERROR: Discord installation not found.'
+ fi
+}
+
+bdc_kill() {
+ declare process_name=Discord
+ [[ $flavor ]] && process_name+=" $flavor"
+ printf 'Killing %s processes...\n' "$process_name" >&2
+ pkill -exi -KILL "discord-?$flavor" || printf 'No active processes found.\n' >&2
+}
+
+bd_injector() {
+ if [[ -z $bd ]]; then
+ bd=$data/bd/$(bd_map_add "$discord" "$bd_repo")
+ if [[ ! -d $bd ]]; then
+ printf 'Cloning %s...\n' "$bd_repo" >&2
+ git clone "$bd_repo" -b "$bd_repo_branch" --depth=1 --single-branch "$bd"
+ fi
+ fi
+
+ if [[ $copy_bd ]]; then
+ verbose 1 'V: Copying BetterDiscord injector...'
+ cp -r "$bd" "$core/injector"
+ else
+ verbose 1 'V: Linking BetterDiscord injector...'
+ ln -s "$bd" "$core/injector"
+ fi
+}
+
+bd_patch() {
+ if ! grep -q 'injector' "$core/index.js"; then
+ verbose 1 'V: Injecting into index.js...'
+ sed -i "$core/index.js" \
+ -e "1i require('./injector');" \
+ -e "s/core'/core.asar'/"
+ fi
+}
+
+bd_unpatch() {
+ verbose 1 'V: Removing BetterDiscord injection...'
+ sed -i "$core/index.js" \
+ -e '/injector/d' \
+ -e "s/core'/core.asar'/"
+ rm -rf "$core/injector"
+}
+
+bd_map_entry_n() {
+ sed 's/^.*\t\t.*\t\(.*\)$/\1/' "$@"
+}
+
+bd_map_fresh() {
+ verbose 2 'VV: Generating fresh bd_map number...'
+ bd_map_entry_n "$data/bd_map" | sort | awk \
+ 'BEGIN {max=-1} NF != 0 {if ($1>max+1) {exit}; max=$1} END {print max+1}'
+}
+
+bd_map_add() {
+ entry=$(bd_map_get_repo "$2")
+ if [[ $entry ]]; then
+ num=$(head -n1 <<< "$entry" | bd_map_entry_n)
+ else
+ num=$(bd_map_fresh)
+ fi
+ printf '%s\t\t%s\t%s\n' "$1" "$2" "$num" >> "$data/bd_map"
+ printf '%s\n' "$num"
+}
+
+bd_map_get_dir() {
+ grep -F "$1"$'\t\t' "$data/bd_map"
+}
+
+bd_map_get_repo() {
+ grep -F $'\t\t'"$1"$'\t' "$data/bd_map"
+}
+
+bd_map_get_n() {
+ grep $'\t'"$1\$" "$data/bd_map"
+}
+
+bd_map_remove() {
+ sed -i "$data/bd_map" -e "\\%$1\\t\\t%d"
+}
+
+# Included from https://github.com/bb010g/Semver.sh , under the MIT License.
+
+Semver::validate() {
+ # shellcheck disable=SC2064
+ trap "$(shopt -p extglob)" RETURN
+ shopt -s extglob
+
+ declare normal=${1%%[+-]*}
+ declare extra=${1:${#normal}}
+
+ declare major=${normal%%.*}
+ if [[ $major != +([0-9]) ]]; then echo "Semver::validate: invalid major: $major" >&2; return 1; fi
+ normal=${normal:${#major}+1}
+ declare minor=${normal%%.*}
+ if [[ $minor != +([0-9]) ]]; then echo "Semver::validate: invalid minor: $minor" >&2; return 1; fi
+ declare patch=${normal:${#minor}+1}
+ if [[ $patch != +([0-9]) ]]; then echo "Semver::validate: invalid patch: $patch" >&2; return 1; fi
+
+ declare -r ident="+([0-9A-Za-z-])"
+ declare pre=${extra%%+*}
+ declare pre_len=${#pre}
+ if [[ $pre_len -gt 0 ]]; then
+ pre=${pre#-}
+ if [[ $pre != $ident*(.$ident) ]]; then echo "Semver::validate: invalid pre-release: $pre" >&2; return 1; fi
+ fi
+ declare build=${extra:pre_len}
+ if [[ ${#build} -gt 0 ]]; then
+ build=${build#+}
+ if [[ $build != $ident*(.$ident) ]]; then echo "Semver::validate: invalid build metadata: $build" >&2; return 1; fi
+ fi
+
+ if [[ $2 ]]; then
+ echo "$2=(${major@Q} ${minor@Q} ${patch@Q} ${pre@Q} ${build@Q})"
+ else
+ echo "$1"
+ fi
+}
+
+Semver::compare() {
+ declare -a x y
+ eval "$(Semver::validate "$1" x)"
+ eval "$(Semver::validate "$2" y)"
+
+ declare x_i y_i i
+ for i in 0 1 2; do
+ x_i=${x[i]}; y_i=${y[i]}
+ if [[ $x_i -eq $y_i ]]; then continue; fi
+ if [[ $x_i -gt $y_i ]]; then echo 1; return; fi
+ if [[ $x_i -lt $y_i ]]; then echo -1; return; fi
+ done
+
+ x_i=${x[3]}; y_i=${y[3]}
+ if [[ -z $x_i && $y_i ]]; then echo 1; return; fi
+ if [[ $x_i && -z $y_i ]]; then echo -1; return; fi
+
+ declare -a x_pre; declare x_len
+ declare -a y_pre; declare y_len
+ IFS=. read -ra x_pre <<< "$x_i"; x_len=${#x_pre[@]}
+ IFS=. read -ra y_pre <<< "$y_i"; y_len=${#y_pre[@]}
+
+ if (( x_len > y_len )); then echo 1; return; fi
+ if (( x_len < y_len )); then echo -1; return; fi
+
+ for (( i=0; i < x_len; i++ )); do
+ x_i=${x_pre[i]}; y_i=${y_pre[i]}
+ if [[ $x_i = "$y_i" ]]; then continue; fi
+
+ declare num_x num_y
+ num_x=$([[ $x_i = +([0-9]) ]] && echo "$x_i")
+ num_y=$([[ $y_i = +([0-9]) ]] && echo "$y_i")
+ if [[ $num_x && $num_y ]]; then
+ if [[ $x_i -gt $y_i ]]; then echo 1; return; fi
+ if [[ $x_i -lt $y_i ]]; then echo -1; return; fi
+ else
+ if [[ $num_y ]]; then echo 1; return; fi
+ if [[ $num_x ]]; then echo -1; return; fi
+ if [[ $x_i > $y_i ]]; then echo 1; return; fi
+ if [[ $x_i < $y_i ]]; then echo -1; return; fi
+ fi
+ done
+
+ echo 0
+}
+
+# Run command
+
+case "$cmd" in
+ status)
+ bdc_main
+ bdc_status
+ ;;
+ install)
+ bdc_main
+ bdc_install
+ ;;
+ reinstall)
+ bdc_main
+ bdc_reinstall
+ ;;
+ update)
+ bdc_main
+ bdc_update
+ ;;
+ uninstall)
+ bdc_main
+ bdc_uninstall
+ ;;
+ upgrade)
+ bdc_upgrade
+ ;;
+ *)
+ die "ERROR: Unknown command: $cmd"
+ ;;
+esac