#!/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