diff options
Diffstat (limited to 'betterdiscordctl')
-rwxr-xr-x | betterdiscordctl | 661 |
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 |