diff --git a/.gitignore b/.gitignore index c06902b..84287ed 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ log/ prezto/ tmux/t-theme/ .directory +scripts/server_bin/adb +scripts/server_bin/Archipelago_0.4.2_linux-x86_64.AppImage +scripts/server_bin/nvim diff --git a/scripts/server_bin/betterdiscordctl b/scripts/server_bin/betterdiscordctl new file mode 100755 index 0000000..bed7c1c --- /dev/null +++ b/scripts/server_bin/betterdiscordctl @@ -0,0 +1,670 @@ +#!/usr/bin/env bash + +set -ueo pipefail +shopt -s dotglob extglob nullglob + +# Constants +VERSION=2.0.1 +SOURCE=$(readlink -f "${BASH_SOURCE[0]}") +DISABLE_SELF_UPGRADE= + +# Options +cmd= +verbosity=0 +d_flavors=('' canary ptb development) +d_modules= +bd_remote=github +bd_remote_dir= +bd_remote_url= +bd_remote_github_owner=BetterDiscord +bd_remote_github_repo=BetterDiscord +bd_remote_github_release=latest +bd_remote_asar=betterdiscord.asar +d_install=traditional +flatpak_bin=flatpak +snap_bin=snap +self_upgrade_url='https://github.com/bb010g/betterdiscordctl/raw/master/betterdiscordctl' + +# Variables +d_flavor= +d_core= +xdg_config= +bdc_data=${XDG_DATA_HOME:-$HOME/.local/share}/betterdiscordctl +d_config= +bd_config= +bd_asar= +bd_asar_escaped= +bd_asar_name= + +show_help() { + cat << EOF +Usage: ${0##*/} [-f d_flavors] \\ + [-D |-U |-H ] \\ + [-i (traditional|flatpak|snap)] + +Manage BetterDiscord installations on Linux. + +Options: + -V, --version display version info and exit + -h, --help display this help message and exit + -v, --verbose increase verbosity + -q, --quiet decrease verbosity + -f, --d-flavors discover Discord installations with the + colon-separated list of suffixes . + Defaults to ':canary:ptb:development'. Flavors + must be lowercase. Stable is flavor '', as + it's unsuffixed. Flavors shouldn't include + spaces. + -m, --d-modules use Discord modules in directory . + Defaults to discovery. Discord's user-specific + storage directory should contain . + -D, --bd-remote-dir reference BetterDiscord files at directory + . Overrides earlier --bd-remote-url + or --bd-remote-github. An empty string keeps a + previous value. + -U, --bd-remote-url download BetterDiscord files at base URL + . Overrides earlier --bd-remote-dir + or --bd-remote-github. An empty string keeps a + previous value. Works like --bd-remote-dir + with files downloaded into BetterDiscord's + data directory. + -H, --bd-remote-github download BetterDiscord files at GitHub + repository release , of form + [~][/][#]. Defaults to + '~BetterDiscord/BetterDiscord#latest'. Overrides + earlier --bd-remote-dir or --bd-remote-github. + An omitted part keeps a previous value. + and must not contain '~', '/', + or '#'. Works like --bd-remote-url with a + GitHub repository release download base URL. + --bd-remote-asar finds "betterdiscord.asar" at path + relative to remote. Defaults to + 'betterdiscord.asar'. + -i, --d-install traditional use a traditional Discord install. Default. + -i, --d-install flatpak use a Discord Flatpak app + -i, --d-install snap use a Discord Snap app + --flatpak-bin invoke Flatpak executable . Defaults + to 'flatpak'. + --snap-bin invoke Snap executable . Defaults to + 'snap'. + --self-upgrade-url query for self-upgrades + + +Commands: + status show the current Discord patch state + install install BetterDiscord + reinstall reinstall BetterDiscord + uninstall uninstall BetterDiscord + self-upgrade upgrade this program +EOF +} + +verbose() { + if (( verbosity >= $1 )); then + shift + >&2 printf '%s\n' "$1" + fi +} + +die() { + while (( $# > 0 )); do + >&2 printf '%s\n' "$1" + shift + done + exit 1 +} + +die_with_help() { + die "$@" "Use \`${0##*/} --help\` for more information." +} + +die_option() { + die_with_help "ERROR: \"$1\" requires an option argument." +} + +die_non_empty_option() { + die_with_help "ERROR: \"$1\" requires a non-empty option argument." +} + +die_non_empty_option_part() { + die_with_help "ERROR: \"$1\" requires a non-empty $2 option argument part." +} + +# arg parsing: top-level: options +while :; do + if [[ -z ${1+x} ]]; then break; fi + case $1 in + -V|--version) + >&2 printf 'betterdiscordctl %s\n' "$VERSION" + exit + ;; + -h|-\?|--help) + show_help; exit + ;; + -v|--verbose) + ((verbosity++)) || : + ;; + -q|--quiet) + ((verbosity--)) || : + ;; + -f|--d-flavors) + if [[ ${2+x} ]]; then + if [[ $2 != "${2,,}" ]]; then + die_with_help "ERROR: Discord flavors list must be lowercase: $2" + else + IFS=':' read -ra d_flavors <<< "$2:"; shift + fi + else die_option "$1"; fi + ;; + -m|--d-modules) + if [[ ${2:+x} ]]; then d_modules=$2; shift + else die_non_empty_option "$1"; fi + ;; + -D|--bd-remote-dir) + bd_remote=dir + if [[ ${2+x} ]]; then [[ ${2:+x} ]] && bd_remote_dir=$2; shift + else die_option "$1"; fi + ;; + -U|--bd-remote-url) + bd_remote=url + if [[ ${2+x} ]]; then [[ ${2:+x} ]] && bd_remote_url=$2; shift + else die_option "$1"; fi + ;; + -H|--bd-remote-github) + bd_remote=github + if [[ ${2+x} ]]; then + if [[ ! $2 =~ (~?[^~/#]*)(/?[^~/#]*)(#?.*) ]]; then + die_with_help "ERROR: \"$1\" requires a valid option argument." + fi + if [[ ${BASH_REMATCH[1]} ]]; then + [[ ${BASH_REMATCH[2]} != '~' ]] || die_non_empty_option_part "$1" '' + bd_remote_github_owner=${BASH_REMATCH[1]:1} + fi + if [[ ${BASH_REMATCH[2]} ]]; then + [[ ${BASH_REMATCH[2]} != '/' ]] || die_non_empty_option_part "$1" '' + bd_remote_github_repo=${BASH_REMATCH[2]:1} + fi + if [[ ${BASH_REMATCH[3]} ]]; then + [[ ${BASH_REMATCH[3]} != '#' ]] || die_non_empty_option_part "$1" '' + bd_remote_github_release=${BASH_REMATCH[3]:1} + fi + shift + else die_option "$1"; fi + ;; + --bd-remote-asar) + if [[ ${2:+x} ]]; then bd_remote_asar=$2; shift + else die_non_empty_option "$1"; fi + ;; + -i|--d-install) + if [[ ${2:+x} ]]; then case "$2" in + traditional|flatpak|snap) d_install=$2 ;; + *) die_with_help "ERROR: Unknown top-level $1 value: $2" ;; + esac; shift; else die_non_empty_option "$1"; fi + ;; + --flatpak-bin) + if [[ ${2:+x} ]]; then flatpak_bin=$2; shift + else die_non_empty_option "$1"; fi + ;; + --snap-bin) + if [[ ${2:+x} ]]; then snap_bin=$2; shift + else die_non_empty_option "$1"; fi + ;; + --self-upgrade-url) + if [[ ${2:+x} ]]; then self_upgrade_url=$2; shift + else die_non_empty_option "$1"; fi + ;; + # footer + -*=*) die "ERROR: Keyed options must not be separated by equals: $1" ;; + --) shift; break ;; + -?|--*) die_with_help "ERROR: Unknown top-level option: $1" ;; + -??*) die "ERROR: Switches must not be ran together: $1" ;; + *) break + esac + shift +done +# arg parsing: top-level: arguments +while :; do + if [[ -z ${1+x} ]]; then break; fi + case "$1" in + status|install|reinstall|uninstall|self-upgrade) + cmd=$1 + shift + break + ;; + *) die_with_help "ERROR: Unknown top-level argument: $1" + esac + shift +done +# arg parsing: top-level: validation +case "$bd_remote" in + github) + [[ $bd_remote_github_owner ]] || die_non_empty_option_part '--bd-remote-github' '' + [[ $bd_remote_github_repo ]] || die_non_empty_option_part '--bd-remote-github' '' + [[ $bd_remote_github_release ]] || die_non_empty_option_part '--bd-remote-github' '' + ;; + url) + [[ $bd_remote_url ]] || die_non_empty_option '--bd-remote-url' + ;; + dir) + [[ $bd_remote_dir ]] || die_non_empty_option '--bd-remote-dir' + ;; +esac +# arg parsing: top-level: command dispatch +case "$cmd" in + status|install|reinstall|uninstall|self-upgrade) + # arg parsing: (status|install|reinstall|uninstall|self-upgrade): options + while :; do + if [[ -z ${1+x} ]]; then break; fi + case "$1" in + # footer + -*=*) die "ERROR: Keyed options must not be separated by equals: $1" ;; + --) shift; break ;; + -?|--*) die_with_help "ERROR: Unknown |$cmd| option: $1" ;; + -??*) die "ERROR: Switches must not be ran together: $1" ;; + esac + shift + done + # arg parsing: (status|install|reinstall|uninstall|self-upgrade): arguments + if [[ -n ${1+x} ]]; then + die_with_help "ERROR: Unknown |$cmd| argument: $1" + fi + ;; + '') + die_with_help "ERROR: Specify a non-empty command." + ;; + *) die "ERROR: [arg parsing: top-level: command dispatch] Unknown command: $cmd" ;; +esac + +# currently unused +# mkdir -p "$bdc_data" + +# Commands + +bdc_status() { + declare asar_install bd_remote_status index_mod + asar_install=no + index_mod=no + verbose 2 "VV: BetterDiscord asar installation: $bd_asar" + if [[ -h $bd_asar && ! -f $bd_asar ]]; then + asar_install='(broken link) no' + elif [[ -f $bd_asar ]]; then + asar_install='(symbolic link) yes' + elif [[ -d $bd_config ]]; then + asar_install='(missing) no' + fi + if grep -Fq "$bd_asar_escaped" "$d_core/index.js"; then + index_mod=yes + elif grep -Fq "$bd_asar_name" "$d_core/index.js"; then + index_mod=noncompliant + elif grep -Fq 'betterdiscord.asar' "$d_core/index.js"; then + index_mod=noncompliant + fi + + bd_remote_status="$bd_remote" + case "$bd_remote" in + github) + bd_remote_status+=" +BetterDiscord remote GitHub: ~$bd_remote_github_owner/$bd_remote_github_repo#$bd_remote_github_release" + ;; + url) + bd_remote_status+=" +BetterDiscord remote URL: $bd_remote_url" + ;; + dir) + bd_remote_status+=" +BetterDiscord remote directory: $bd_remote_dir" + ;; + esac + + printf 'Discord install: %s +Discord flavor: %s +Discord modules: %s +BetterDiscord directory: %s +BetterDiscord asar installed: %s +Discord "index.js" injected: %s +BetterDiscord remote: %s +' "$d_install" "$d_flavor" "$d_modules" "$bd_config" "$asar_install" \ + "$index_mod" "$bd_remote_status" +} + +bdc_install() { + grep -Fq "$bd_asar_escaped" "$d_core/index.js" && die 'ERROR: Already installed.' + bdc_clean_legacy + + bd_remote_install + bd_install + + >&2 printf 'Installed. (Restart Discord if necessary.)\n' +} + +bdc_reinstall() { + grep -Fq "$bd_asar_name" "$d_core/index.js" || die 'ERROR: Not installed.' + bdc_clean_legacy + + bdc_kill + + bd_remote_install + bd_install + + >&2 printf 'Reinstalled.\n' +} + +bdc_uninstall() { + grep -Fq "$bd_asar_name" "$d_core/index.js" || die 'ERROR: Not installed.' + bdc_clean_legacy + + bdc_kill + bd_uninstall + + >&2 printf 'Uninstalled.\n' +} + +bdc_self_upgrade() { + if [[ $DISABLE_SELF_UPGRADE ]]; then + die 'ERROR: Self-upgrading has been disabled.' \ + 'If you installed this from a package, its maintainer should keep it up to date.' + fi + + declare self_upgrade_version semver_diff + self_upgrade_version=$(curl -NLSs "$self_upgrade_url" | sed -n 's/^VERSION=//p') + if [[ ${PIPESTATUS[0]} -ne 0 ]]; then + die "ERROR: The remote script URL couldn't be reached to check the version." + fi + verbose 2 "VV: Local script location: $SOURCE" + verbose 2 "VV: Remote script URL: $self_upgrade_url" + verbose 1 "V: Local version: $VERSION" + verbose 1 "V: Remote version: $self_upgrade_version" + semver_diff=$(Semver::compare "$self_upgrade_version" "$VERSION") + if [[ $semver_diff -eq 1 ]]; then + >&2 printf 'Downloading betterdiscordctl...\n' + if curl -LSso "$SOURCE" "$self_upgrade_url"; then + >&2 printf 'Successfully self-upgraded betterdiscordctl.\n' + else + die 'ERROR: Failed to self-upgrade betterdiscordctl.' \ + "You may want to rerun this command with \`sudo\`." + fi + else + if [[ $semver_diff -eq 0 ]]; then + >&2 printf 'betterdiscordctl is already the latest version (%s).\n' \ + "$VERSION" + else + >&2 printf 'Local version (%s) is higher than remote version (%s).\n' \ + "$VERSION" "$self_upgrade_version" + fi + fi +} + +# Implementation functions + +bdc_main() { + xdg_discover_config + bdc_discover + d_core=$d_modules/discord_desktop_core + [[ -d $d_core ]] || die "ERROR: Directory 'discord_desktop_core' not found in: $d_modules" + bd_remote_init + bd_asar=$bd_config/data/$bd_asar_name + bd_asar_escaped=${bd_asar/\\/\\\\} +} + +xdg_discover_config() { + case "$d_install" in + traditional) + xdg_config=${XDG_CONFIG_HOME:-$HOME/.config} + ;; + snap) + # 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) + ;; + flatpak) + # shellcheck disable=SC2016 + # Expansion should happen inside flatpak's shell. + xdg_config=$("$flatpak_bin" run --command=sh com.discordapp.Discord \ + -c $'printf -- \'%s\n\' "$XDG_CONFIG_HOME"') + xdg_config=${xdg_config:-$HOME/.var/app/com.discordapp.Discord/config} + ;; + *) die "ERROR: [xdg_discover_config] Unknown Discord install variant: $d_install" ;; + esac + [[ $xdg_config ]] || >&2 printf "WARN: XDG user config directory (\$XDG_CONFIG_HOME) not found.\n" +} + +bdc_discover() { + d_discover_config + bd_discover_config + bdc_find_modules +} + +bdc_find_modules() { + if [[ $d_modules ]]; then + [[ -d $d_modules ]] || die "ERROR: Discord modules directory not found: $d_modules" + d_flavor=${d_modules%/*/modules} + d_flavor=${d_flavor##*/discord} + else + [[ -d $d_config ]] || die "ERROR: Discord $d_flavor config directory not found: $d_config" + declare -a all_d_modules + all_d_modules=("$d_config/"+([0-9]).+([0-9]).+([0-9])/modules) + ((${#all_d_modules[@]})) || die 'ERROR: Discord modules directory not found.' \ + 'Try specifying it with --d-modules.' + d_modules=${all_d_modules[-1]} + verbose 1 "V: Found modules in $d_modules" + fi +} + +bdc_kill() { + >&2 printf 'Killing Discord %s processes...\n' "$d_flavor" + pkill -exi -KILL "discord${d_flavor:0:8}" || >&2 printf 'No active processes found.\n' +} + +d_discover_config() { + [[ $xdg_config ]] || die "ERROR: XDG user config directory (\$XDG_CONFIG_HOME) not found." + case "$d_install" in + traditional) + for d_flavor in "${d_flavors[@]}"; do + verbose 2 "VV: Trying flavor '$d_flavor'" + d_config=$xdg_config/discord${d_flavor,,} + if [[ -d $d_config ]]; then + break + fi + >&2 printf 'WARN: Discord %s config directory not found (%s).\n' \ + "$d_flavor" "$d_config" + done + ;; + snap|flatpak) + d_config=$xdg_config/discord + if [[ ! -d $d_config ]]; then + >&2 printf 'WARN: Discord %s config directory not found (%s).\n' \ + "$d_install" "$d_config" + fi + ;; + *) die "ERROR: [d_discover_config] Unknown Discord install variant: $d_install" ;; + esac +} + +bd_discover_config() { + [[ $xdg_config ]] || die "ERROR: XDG user config directory (\$XDG_CONFIG_HOME) not found." + case "$d_install" in + traditional|snap|flatpak) + bd_config=$xdg_config/BetterDiscord + ;; + *) die "ERROR: [bd_discover_config] Unknown Discord install variant: $d_install" ;; + esac +} + +# TODO: Integrate $bd_remote into main & install + +bd_remote_init() { + case "$bd_remote" in + github) bd_remote_init_github ;; + url) bd_remote_init_url ;; + dir) bd_remote_init_dir ;; + *) die "ERROR: [bd remote init] Unknown remote type: $bd_remote" ;; + esac + verbose 2 "VV: BetterDiscord remote asar path: $bd_remote_asar" + bd_asar_name=${bd_remote_asar##*/} +} +bd_remote_init_github() { + bd_remote_url=https://github.com/$bd_remote_github_owner/$bd_remote_github_repo/releases/$bd_remote_github_release/download + verbose 2 "VV: BetterDiscord remote GitHub repository owner: $bd_remote_github_owner" + verbose 2 "VV: BetterDiscord remote GitHub repository name: $bd_remote_github_repo" + verbose 2 "VV: BetterDiscord remote GitHub repository release: $bd_remote_github_release" + bd_remote_init_url +} +bd_remote_init_url() { + bd_remote_dir=$bd_config/data + verbose 2 "VV: BetterDiscord remote URL: $bd_remote_url" + bd_remote_init_dir +} +bd_remote_init_dir() { + verbose 2 "VV: BetterDiscord remote directory: $bd_remote_dir" +} + +bd_remote_install() { + case "$bd_remote" in + github) bd_remote_install_github ;; + url) bd_remote_install_url ;; + dir) bd_remote_install_dir ;; + *) die "ERROR: [bd remote install] Unknown remote type: $bd_remote" ;; + esac +} +bd_remote_install_github() { + verbose 2 "VV: Installing remote BetterDiscord (GitHub)..." + bd_remote_install_url +} +bd_remote_install_url() { + verbose 2 "VV: Installing remote BetterDiscord (URL)..." + verbose 1 "V: Downloading BetterDiscord asar..." + curl -LSso "$bd_remote_dir/$bd_remote_asar" --create-dirs \ + "$bd_remote_url/$bd_remote_asar" + bd_remote_install_dir +} +bd_remote_install_dir() { + verbose 2 "VV: Installing remote BetterDiscord (directory)..." + if [[ "$bd_remote_dir/$bd_remote_asar" != "$bd_asar" ]]; then + verbose 1 "V: Copying BetterDiscord asar..." + install -Dm 644 "$bd_remote_dir/$bd_remote_asar" "$bd_asar" + fi +} + +bdc_clean_legacy() { + if [[ -d $d_core/core ]]; then + >&2 printf 'Removing legacy core directory...\n' + rm -rf "$d_core/core" + fi + if [[ -d $d_core/injector ]]; then + >&2 printf 'Removing legacy injector directory...\n' + rm -rf "$d_core/injector" + fi + if [[ -d $bdc_data ]]; then + if [[ -f "$bdc_data/bd_map" || -d "$bdc_data/bd" ]]; then + >&2 printf 'Removing legacy machine-specific data...\n' + rm -rf "$bdc_data/bd_map" "$bdc_data/bd" + fi + fi +} + +bd_install() { + verbose 1 'V: Injecting into index.js...' + printf $'require("%s"); +module.exports = require(\'./core.asar\'); +' "$bd_asar_escaped" > "$d_core/index.js" +} + +bd_uninstall() { + verbose 1 'V: Removing BetterDiscord injection...' + printf $'module.exports = require(\'./core.asar\'); +' > "$d_core/index.js" +} + +# Included from https://github.com/bb010g/Semver.sh , under the MIT License. + +Semver::validate() { + [[ $1 =~ ^([^+-.]*)\.?([^+-.]*)\.?([^+-]*)(-?)([^+]*)(\+?)(.*)$ ]] + declare -a ver; ver=("${BASH_REMATCH[@]:1}") + + if [[ ${ver[0]} != +([0-9]) ]]; then printf '%s\n' "Semver::validate: invalid major: ${ver[0]}" >&2; return 1; fi + if [[ ${ver[1]} != +([0-9]) ]]; then printf '%s\n' "Semver::validate: invalid minor: ${ver[1]}" >&2; return 1; fi + if [[ ${ver[2]} != +([0-9]) ]]; then printf '%s\n' "Semver::validate: invalid patch: ${ver[2]}" >&2; return 1; fi + + if [[ ${ver[3]} == '-' && ${ver[4]} != +([0-9A-Za-z-])*(.+([0-9A-Za-z-])) ]]; then + printf '%s\n' "Semver::validate: invalid pre-release: ${ver[4]}" >&2; return 1 + fi + if [[ ${ver[5]} == '+' && ${ver[6]} != +([0-9A-Za-z-])*(.+([0-9A-Za-z-])) ]]; then + printf '%s\n' "Semver::validate: invalid build metadata: ${ver[6]}" >&2; return 1 + fi + + if [[ -n $2 ]]; then + printf '%s\n' "$2=(${ver[0]@Q} ${ver[1]@Q} ${ver[2]@Q} ${ver[4]@Q} ${ver[6]@Q})" + else + printf '%s\n' "$1" + fi +} + +Semver::compare() { + declare -a xs ys + eval "$(Semver::validate "$1" xs)" + eval "$(Semver::validate "$2" ys)" + + declare i x y + for i in 0 1 2; do + x=${xs[i]}; y=${ys[i]} + if [[ $x -eq $y ]]; then continue; fi + if [[ $x -gt $y ]]; then echo 1; return; fi + if [[ $x -lt $y ]]; then echo -1; return; fi + done + + x=${xs[3]}; y=${ys[3]} + if [[ -z $x && -n $y ]]; then echo 1; return; fi + if [[ -n $x && -z $y ]]; 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."; x_len=${#x_pre[@]} + IFS=. read -ra y_pre <<< "$y."; 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=${x_pre[i]}; y=${y_pre[i]} + if [[ $x == "$y" ]]; then continue; fi + + if [[ $x == +([0-9]) ]]; then + if [[ $y == +([0-9]) ]]; then + if [[ $x -gt $y ]]; then echo 1; return; fi + if [[ $x -lt $y ]]; then echo -1; return; fi + else echo -1; return; fi + elif [[ $y == +([0-9]) ]]; then echo 1; return + else + if [[ $x > $y ]]; then echo 1; return; fi + if [[ $x < $y ]]; 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 + ;; + uninstall) + bdc_main + bdc_uninstall + ;; + self-upgrade) + bdc_self_upgrade + ;; + *) die "ERROR: Unknown command (in command dispatch): $cmd" ;; +esac