dottie/2024-03-10/bin/betterdiscordctl

671 lines
21 KiB
Plaintext
Raw Normal View History

2024-03-10 20:37:11 -04:00
#!/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 <bd_r_dir>|-U <bd_r_url>|-H <bd_r_github>] \\
[-i (traditional|flatpak|snap)] <command>
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 <d_flavors> discover Discord installations with the
colon-separated list of suffixes <d_flavors>.
Defaults to ':canary:ptb:development'. Flavors
must be lowercase. Stable is flavor '', as
it's unsuffixed. Flavors shouldn't include
spaces.
-m, --d-modules <d_modules> use Discord modules in directory <d_modules>.
Defaults to discovery. Discord's user-specific
storage directory should contain <d_modules>.
-D, --bd-remote-dir reference BetterDiscord files at directory
<bd_r_dir> <bd_r_dir>. 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
<bd_r_url> <bd_r_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
<bd_r_github> repository release <bd_r_github>, of form
[~<owner>][/<repo>][#<release>]. Defaults to
'~BetterDiscord/BetterDiscord#latest'. Overrides
earlier --bd-remote-dir or --bd-remote-github.
An omitted part keeps a previous value.
<owner> and <repo> must not contain '~', '/',
or '#'. Works like --bd-remote-url with a
GitHub repository release download base URL.
--bd-remote-asar <bd_r_asar> finds "betterdiscord.asar" at path <bd_r_asar>
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 <flatpak> invoke Flatpak executable <flatpak>. Defaults
to 'flatpak'.
--snap-bin <snap> invoke Snap executable <snap>. Defaults to
'snap'.
--self-upgrade-url query <self_upgrade_url> for self-upgrades
<self_upgrade_url>
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" '<owner>'
bd_remote_github_owner=${BASH_REMATCH[1]:1}
fi
if [[ ${BASH_REMATCH[2]} ]]; then
[[ ${BASH_REMATCH[2]} != '/' ]] || die_non_empty_option_part "$1" '<repo>'
bd_remote_github_repo=${BASH_REMATCH[2]:1}
fi
if [[ ${BASH_REMATCH[3]} ]]; then
[[ ${BASH_REMATCH[3]} != '#' ]] || die_non_empty_option_part "$1" '<release>'
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' '<owner>'
[[ $bd_remote_github_repo ]] || die_non_empty_option_part '--bd-remote-github' '<repo>'
[[ $bd_remote_github_release ]] || die_non_empty_option_part '--bd-remote-github' '<release>'
;;
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