You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Botspot-Pi-Apps/api

573 lines
22 KiB
Plaintext

#!/bin/bash
error() {
echo -e "\e[91m$1\e[39m" 1>&2
exit 1
}
#if this script is being run standalone, not sourced
if [[ "$0" == */api ]];then
DIRECTORY="$(readlink -f "$(dirname "$0")")"
fi
if [ -z "$DIRECTORY" ] || [ "$DIRECTORY" == "$HOME" ];then
echo "api: DIRECTORY variable must be set to valid pi-apps folder. Default folder: $HOME/pi-apps"
return 1
fi
repo_url="$(cat "${DIRECTORY}/etc/git_url" || echo 'https://github.com/Botspot/pi-apps')"
#determine if host system is 64 bit arm64 or 32 bit armhf
if [ "$(od -An -t x1 -j 4 -N 1 "$(readlink -f /sbin/init)")" = ' 02' ];then
arch=64
elif [ "$(od -An -t x1 -j 4 -N 1 "$(readlink -f /sbin/init)")" = ' 01' ];then
arch=32
else
error "Failed to detect OS CPU architecture! Something is very wrong."
fi
list_intersect() { #Outputs only the apps that appear in both stdin and in $1
# change \n to \| | remove last "\|"
grep -x "$(echo "$1" | sed -z 's/\n/\\|/g' | sed -z 's/\\|$/\n/g')"
}
list_subtract() { #Outputs a list of apps from stdin, minus the ones that appear in $1
# change \n to \| | remove last "\|"
grep -vx "$(echo "$1" | sed -z 's/\n/\\|/g' | sed -z 's/\\|$/\n/g')"
}
list_apps() { # $1 can be: installed, uninstalled, corrupted, cpu_installable, hidden, visible, online, online_only, local, local_only
if [ -z "$1" ] || [ "$1" == local ];then
#list all apps
ls "${DIRECTORY}/apps"
elif [ "$1" == all ];then
#combined list of apps, both online and local. Removes duplicate apps from the list.
echo -e "$(list_apps local)\n$(list_apps online)" | sort | uniq
elif [ "$1" == installed ];then
#list installed apps
#list apps| only show ( list of installed apps | remove match string | basename )
list_apps local | list_intersect "$(grep -rx 'installed' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
elif [ "$1" == corrupted ];then
#list corrupted apps
#list apps|only show ( list of corrupted apps | remove match string | basename )
list_apps local | list_intersect "$(grep -rx 'corrupted' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
elif [ "$1" == disabled ];then
#list corrupted apps
#list apps|only show ( list of disabled apps | remove match string | basename )
list_apps local | list_intersect "$(grep -rx 'disabled' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
elif [ "$1" == uninstalled ];then
#list uninstalled apps
#list apps that have a status file containing "uninstalled"
list_apps local | list_intersect "$(grep -rx 'uninstalled' "${DIRECTORY}/data/status" | awk -F: '{print $1}' | sed 's!.*/!!')"
#also list apps that don't have a status file
list_apps local | list_subtract "$(ls "${DIRECTORY}/data/status")"
elif [ "$1" == cpu_installable ];then
#list apps that can be installed on the device's OS architecture (32-bit or 64-bit)
#find all apps that have install-XX script or an install script
find "${DIRECTORY}/apps" -type f \( -name "install-$arch" -o -name "install" \) | sed "s+/install-$arch++g" | sed "s+/install++g" | sed "s+${DIRECTORY}/apps/++g" | sort | uniq
elif [ "$1" == hidden ];then
#list apps that are hidden
cat "${DIRECTORY}/data/categories/structure" | grep '|hidden' | awk -F'|' '{print $1}'
elif [ "$1" == visible ];then
#list apps that are in any other category but 'hidden', and aren't disabled
cat "${DIRECTORY}/data/categories/structure" | grep -v '|hidden' | awk -F'|' '{print $1}' # | list_subtract "$(list_apps disabled)"
elif [ "$1" == online ];then
#list apps that exist on the online git repo
if [ -d "${DIRECTORY}/update/pi-apps/apps" ];then
#if update folder exists, just use that
ls "${DIRECTORY}/update/pi-apps/apps" | grep .
else
#if update folder doesn't exist, then parse github HTML to get a list of online apps. Horrible idea, but it works!
wget -qO- "${repo_url}/tree/master/apps" | grep 'title=".*" data-pjax=' -o | sed 's/title="//g' | sed 's/" data-pjax=//g'
fi
elif [ "$1" == online_only ];then
#list apps that exist only on the git repo, and not locally
list_apps online | list_subtract "$(list_apps local)"
elif [ "$1" == local_only ];then
#list apps that exist only locally, and not on the git repo
list_apps local | list_subtract "$(list_apps online)"
fi
}
app_categories() { #lists all apps in a virtual filesystem based on categories file
#cat "${DIRECTORY}/data/categories/structure" | awk -F'|' '{print $2"/"$1}'
#find apps not in categories file
{
missingapps="$(list_apps | list_subtract "$(cat "${DIRECTORY}/data/categories/structure" | awk -F'|' '{print $1}')")"
if [ ! -z "$missingapps" ];then
PREIFS="$IFS"
IFS=$'\n'
for app in $missingapps ;do
echo "WARNING: $app not found in categories file." 1>&2
if list_apps online | grep -qx "$app" ;then
#if app found online, then use online category line
if [ -z "$onlinestructurefile" ];then
onlinestructurefile="$(wget -qO- 'https://raw.githubusercontent.com/Botspot/pi-apps/master/data/categories/structure')"
fi
if echo "$onlinestructurefile" | grep -q '^'"$app|" ;then
#if line found in online structure file
echo "Putting $app in the $(echo "$onlinestructurefile" | grep '^'"$app|" | awk -F'|' '{print $2}') category." 1>&2
echo "$(echo "$onlinestructurefile" | grep '^'"$app|")" >> "${DIRECTORY}/data/categories/structure"
else
#app exists online, but no structure line found
echo -e "\e[33mHUGE WARNING: the $app exists on github, but no category was found for it on github!\nPlease report this to Botspot.\e[39m" 1>&2
echo "Putting $app in the / category." 1>&2
#put the app in root directory - no category
echo "$app|" >> "${DIRECTORY}/data/categories/structure"
fi
else
#app not found online
echo "Putting $app in the / category." 1>&2
#put the app in root directory - no category
echo "$app|" >> "${DIRECTORY}/data/categories/structure"
fi
done
IFS="$PREIFS"
fi
}
#find apps in categories file that don't exist
{
ghostapps="$(cat "${DIRECTORY}/data/categories/structure" | awk -F'|' '{print $1}' | list_subtract "$(list_apps)")"
if [ ! -z "$ghostapps" ];then
PREIFS="$IFS"
IFS=$'\n'
for app in $ghostapps ;do
echo "WARNING: $app does not exist but it was found in categories file." 1>&2
echo "Removing $app from the categories file..." 1>&2
#put the app in root directory - no category
sed -i "/$app/d" "${DIRECTORY}/data/categories/structure"
done
IFS="$PREIFS"
fi
}
#category file cleaned up past this point
#show normal categories
cat "${DIRECTORY}/data/categories/structure" | grep . | awk -F'|' '{print $2"/"$1}' | sed 's+^/++g'
#show special Installed category
list_apps installed | sed 's+^+Installed/+g'
#show special All Apps category
list_apps cpu_installable | list_intersect "$(list_apps visible)" | sed 's+^+All Apps/+g'
}
usercount() { #Return number of users for specified app. $1 is app name. if empty, all are shown.
clicklist="$(wget -qO- 'https://raw.githubusercontent.com/Botspot/pi-apps-analytics/main/clicklist')"
[ -z "$clicklist" ] && error "usercount(): clicklist empty. Likely no internet connection"
if [ -z "$1" ];then
echo "$clicklist"
else
# $1 is app
echo "$clicklist" | grep " $1"'$' | awk '{print $1}' | head -n1
fi
}
text_editor() { #Open user-preferred text editor. $1 is file to open
[ -z "$1" ] && error "text_editor(): no file specified"
#find the best text editor
preferrededitor="$(cat "${DIRECTORY}/data/settings/Preferred text editor")"
#change preferred editor if user-default doesn't exist
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=geany
fi
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=mousepad
fi
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=leafpad
fi
if ! command -v "$preferrededitor" >/dev/null;then
preferrededitor=nano
fi
if [ "$preferrededitor" == nano ];then
#terminal-based text editor
"${DIRECTORY}/etc/terminal-run" "nano "\""$1"\""" "Editing $(basename "$1")"
else
#non-terminal text editor
"$preferrededitor" "$1"
fi
}
script_name() { #returns name of install script(s) for the $1 app. outputs: '', 'install-32', 'install-64', 'install', 'install-32 install-64'
[ -z "$1" ] && error 'script_name(): requires an argument'
#ensure $1 is valid app name
[ ! -d "${DIRECTORY}/apps/$1" ] && error "script_name: '$1' is an invalid app name.\n${DIRECTORY}/apps/$1 does not exist."
if [ -f "${DIRECTORY}/apps/$1/install-32" ] && [ ! -f "${DIRECTORY}/apps/$1/install-64" ];then
echo 'install-32'
elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ ! -f "${DIRECTORY}/apps/$1/install-32" ];then
echo 'install-64'
elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ -f "${DIRECTORY}/apps/$1/install-32" ];then
echo 'install-32 install-64'
elif [ -f "${DIRECTORY}/apps/$1/install" ];then
echo 'install'
else
true
#error "No install script found for the $app app! Please report this to Botspot."
fi
}
script_name_cpu() { #get script name to run based on detected CPU arch
[ -z "$1" ] && error 'script_name_cpu(): requires an argument.'
#ensure $1 is valid app name
if ! list_apps all | grep -q "$1" ;then
error "script_name_cpu: '$1' is an invalid app name."
fi
#this is used by the updater so we need to check the update folder too
if [ -f "${DIRECTORY}/apps/$1/install-32" ] && [ $arch == 32 ];then
echo 'install-32'
elif [ -f "${DIRECTORY}/apps/$1/install-64" ] && [ $arch == 64 ];then
echo 'install-64'
elif [ -f "${DIRECTORY}/apps/$1/install" ];then
echo 'install'
elif [ -f "${DIRECTORY}/update/pi-apps/apps/$1/install-32" ] && [ $arch == 32 ];then
echo 'install-32'
elif [ -f "${DIRECTORY}/update/pi-apps/apps/$1/install-64" ] && [ $arch == 64 ];then
echo 'install-64'
elif [ -f "${DIRECTORY}/update/pi-apps/apps/$1/install" ];then
echo 'install'
else
true #app not compatible with current arch
fi
}
app_status() { #Gets the $1 app's current status. installed, uninstalled, corrupted, disabled
[ -z "$1" ] && error 'app_status(): requires an argument.'
#don't check if app exists, it may be a new app in an update
if [ -f "${DIRECTORY}/data/status/${1}" ];then
cat "${DIRECTORY}/data/status/${1}"
else
echo 'uninstalled' #if app status file doesn't exist, assume uninstalled
fi
}
will_reinstall() { #return 0 if $1 app will be reinstalled during an update, otherwise return 1.
[ -z "$1" ] && error 'will_reinstall(): requires an argument'
#detect which installation script exists and get the hash for that one
scriptname="$(script_name_cpu "$1")"
oldinstallhash="$(sha1sum "${DIRECTORY}/apps/${1}/${scriptname}" 2>/dev/null | awk '{print $1}')"
newinstallhash="$(sha1sum "${DIRECTORY}/update/pi-apps/apps/${1}/${scriptname}" 2>/dev/null | awk '{print $1}')"
#if install script was changed #if installed already
if [ "$newinstallhash" != "$oldinstallhash" ] && [ "$(app_status "${1}")" == 'installed' ];then
return 0
else
return 1
fi
}
app_search() { #search all apps for $1
[ -z "$1" ] && error "app_search(): requires a search query."
#search description and website
results="$(find "${DIRECTORY}/apps" \( -name description -o -name website -o -name credits \) -exec grep -Fi "$1" {} + | awk -F: '{print $1}' | sed "s+${DIRECTORY}/apps/++g" | sed "s+/description++g" | sed "s+/website++g")"
#hide incompatible and hidden/disabled apps
results="$(echo "$results" | list_intersect "$(list_apps visible)" | list_intersect "$(list_apps cpu_installable)" | sort)"
#search app names
results="$(list_apps cpu_installable | list_intersect "$(list_apps visible)" | grep -i "$1" | sort)
$results"
#remove duplicate entries
echo "$results" | awk '!seen[$0]++'
}
app_search_gui() {
output="$(yad --title=Search --center --width=310 --window-icon="${DIRECTORY}/icons/logo.png" \
--text="Searches app names, descriptions, websites, and credits."$'\n'"Case-insensitive." \
--form --entry='')" || exit 0
results="$(app_search "$output")"
if [ ! -z "$results" ];then
IFS=$'\n'
LIST=''
for app in $results ;do
LIST="$LIST
${DIRECTORY}/apps/${app}/icon-24.png
$app
$(echo "$(cat "${DIRECTORY}/apps/${app}/description" || echo "Description unavailable")" | head -n1)"
done
LIST="${LIST:1}" #remove first empty newline
output="$(echo "$LIST" | yad --title=Results --center --width=310 --height=250 --window-icon="${DIRECTORY}/icons/logo.png" \
--list --no-headers --column=:IMG --column=name --column=tooltip:HD \
--print-column=2 --tooltip-column=3 --separator='\n')" || exit 0
echo "$output"
else
yad --title=Results --center --width=310 --window-icon="${DIRECTORY}/icons/logo.png" \
--text="No results found for "\""<b>$output</b>"\""." \
--button=OK:0
exit 0
fi
}
#non-app functions below
runonce() { #run command only if it's never been run before. Useful for one-time migration or setting changes.
#Runs a script in the form of stdin
script="$(cat /dev/stdin)"
runonce_hash="$(echo "$script" | sha256sum | awk '{print $1}')"
if grep -qx '^'"$runonce_hash"'$' "${DIRECTORY}/data/runonce_hashes" ;then
#hash found
#echo "runonce: '$script' already run before. Skipping."
true
else
#run the script.
bash <(echo "$script")
#if it succeeds, add the hash to the list to never run it again
if [ $? == 0 ];then
echo "$runonce_hash" >> "${DIRECTORY}/data/runonce_hashes"
echo "'$script' succeeded. Added to list."
else
echo "'$script' failed. Not adding hash to list."
fi
fi
}
apt_lock_wait() { #Wait until other apt processes are finished before proceeding
echo -n "Waiting until APT locks are released... "
#while sudo fuser /var/lib/dpkg/lock &>/dev/null ; do
# sleep 0.5
#done
#while sudo fuser /var/lib/dpkg/lock-frontend &>/dev/null ; do
# sleep 0.5
#done
#while sudo fuser /var/lib/apt/lists/lock &>/dev/null ; do
# sleep 0.5
#done
#if [ -f /var/log/unattended-upgrades/unattended-upgrades.log ]; then
# while sudo fuser /var/log/unattended-upgrades/unattended-upgrades.log &>/dev/null ; do
# sleep 0.5
# done
#fi
while [ ! -z "$(sudo fuser /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock /var/log/unattended-upgrades/unattended-upgrades.log /var/lib/dpkg/lock-frontend 2>/dev/null)" ];do
sleep 1
done
sleep 0.5
echo "Done"
}
is_supported_system() {
if uname -m | grep -q 'x86' || uname -m | grep -q 'i686' || uname -m | grep -q 'i386';then
echo "Pi-Apps is not supported on x86 processors. Expect almost all apps to fail. Consider switching to this x86 port of Pi-Apps: https://github.com/MCRaspRBX/pi-apps-x86"
return 1
elif cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}' | grep -qi 'stretch\|wheezy\|jessie';then
echo "Pi-Apps is not supported on your outdated operating system. Expect many apps to fail. Consider upgrading your operating system."
return 1
elif cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}' | grep -qi 'manjaro';then
echo "Pi-Apps is not supported on Manjaro."
return 1
elif [[ "$(uname -m)" == armv6* ]];then
echo "Pi-Apps is not supported on ARMv6 Raspberry Pi boards. Expect some apps to fail."
return 1
elif [ "$(id -u)" == 0 ]; then
echo "Pi-Apps is not designed to be run as root user."
return 1
elif [ "$(df -a / -B 1 --output=avail | tail -1 | tr -d ' ')" -lt $((1*1024*1024*1024)) ];then
echo "Your system drive has less than 1GB of free space. Watch out for "\""disk full"\"" errors."
return 1
else
return 0
fi
}
format_log_file() { #remove ANSI escape sequences from a given file, and add OS information to beginning of file
[ -z "$1" ] && error "format_log_file: no filename given!"
[ ! -f "$1" ] && error "format_log_file: given filename ($1) does not exist or is not a file!"
echo -e "$(get_device_info)\n\nBEGINNING OF LOG FILE:\n-----------------------\n\n$(cat "$1" | tr '\r' '\n' | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\x1b\[[0-9;]*//g' | sed "s,\x1B\[[0-9;]*[a-zA-Z],,g" | grep -vF '.......... .......... .......... .......... ..........')" > "$1"
}
get_device_info() { #returns information about current install and hardware
echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | tr -d '"' | awk -F= '{print $2}')"
echo "OS architecture: ${arch}-bit"
[ ! -z "$DIRECTORY" ] && echo "Pi-Apps local commit ID: $(cd "$DIRECTORY"; git rev-parse HEAD)"
echo "Kernel: $(uname -m) $(uname -r)"
echo "Device model: $(cat /proc/cpuinfo | grep Model | sed 's/Model.*: //g')"
if [ -f /etc/rpi-issue ];then
echo "Raspberry Pi OS image version: $(cat /etc/rpi-issue | grep 'Raspberry Pi reference' | sed 's/Raspberry Pi reference //g')"
fi
if [ ! -z "$LANG" ];then
echo "Language: $LANG"
elif [ ! -z "$LC_ALL" ];then
echo "Language: $LC_ALL"
fi
}
send_error_report() { #non-interactively send a Pi-Apps error log file to the Botspot discord server
[ -z "$1" ] && error "send_error_report(): requires an argument"
[ ! -f "$1" ] && error "send_error_report(): '$1' is not a valid file."
command -v curl >/dev/null || error "send_error_report(): Cannot send report: curl command not found!"
errors="$(bash <(base64 -d <<<"Y3VybCAtRiAiZmlsZT1AXCIK$(base64 <<<"$1")XCI7ZmlsZW5hbWU9XCIK$(base64 <<<"$(basename "$1" | sed 's/\.log.*/.txt/g')")XCIiICIkKHdnZXQgLXFPLSAiJChiYXNlNjQgLWQgPDw8ImFIUjBjSE02THk5eVlYY3VaMmwwYUhWaWRYTmxjbU52Ym5SbGJuUXVZMjl0TDBKdmRITndiM1F2Y0drdFlYQndjeTFoYm1Gc2VYUnBZM012YldGcGJpOWxjbkp2Y2kxc2IyY3RkMlZpYUc5dmF3bz0iKSIgfCAkKGJhc2U2NCAtZCA8PDwiWW1GelpUWTBJQzFrQ2c9PSIpKSIK" | tr -d '\n') 2>&1)"
[ $? != 0 ] && error "curl failed to upload log file!\nErrors:\n$errors"
}
send_error_report_gui() { #Ask user for permission to send error report
[ -z "$1" ] && error "send_error_report_gui(): requires an argument for error report file!"
[ ! -f "$1" ] && error "send_error_report_gui(): Given error report file ($1) does not exist!"
[ -z "$2" ] && error "send_error_report_gui(): requires an argument for window text!"
command -v curl >/dev/null || error "send_error_report_gui(): curl is not installed!"
export -f text_editor
export DIRECTORY
yad --center --title="Send error report?" --window-icon="${DIRECTORY}/icons/logo.png" \
--text="$2"$'\n'"Send anonymous error report to Pi-Apps developers?"$'\n'"Support is available on <a href=\"https://discord.gg/RXSTvaUvuu\">Discord</a> and <a href=\"https://github.com/Botspot/pi-apps/issues/new/choose\">Github</a>." --on-top \
--button='Send report'!"${DIRECTORY}/icons/upload.png":0 \
--button='View report'!"${DIRECTORY}/icons/log-file.png"!"View the log file to be sent."$'\n'"Feel free to edit the file with more debug information to help us.":"bash -c 'text_editor "\""$1"\""'" \
--button="Don't send"!"${DIRECTORY}/icons/exit.png":1
button=$?
#echo "Button: $button"
if [ "$button" == 0 ];then
send_error_report "$1"
fi
}
generate_logo() { #display colorized Pi-Apps logo in terminal
#generate pi-apps logo
#https://misc.flogisoft.com/bash/tip_colors_and_formatting
blue='\e[38;5;21m' #blue='\e[38;5;27m' #lighter
green='\e[38;5;46m'
red='\e[38;5;197m'
echo -e " ${green}┏━━━┓
${blue}┏━━${green}┻${blue}━━━${green}┻${blue}━━┓
${blue}┃ ${red}⬛⬛⬛ ${blue}┃\e[97m ▕ᑐ • ▁ ʌ
${blue}┃ ${red}⬛⬛⬛ ${blue}┃\e[97m ▕ │ ╱‾╲▕ᑐ ▕ᑐ
${blue}┃ ${red}⬛⬛⬛ ${blue}┃\e[97m ▕ ▕
\e[38;5;27m╰${blue}━━━━━━━━━\e[38;5;27m╯\e[49m
\e[0m\e[0m"
}
wget() { #this function intercepts all wget commands being used in app scripts. It uses aria2c if possible.
local file=''
local url=''
local use=aria2c
if command -v aria2c >/dev/null; then
#convert wget arguments to newline-separated list
local IFS=$'\n'
local opts="$(IFS=$'\n'; echo "$*")"
for opt in $opts ;do
if [[ "$opt" == '-'* ]] || [ "$opt" == '-' ];then
#this opt is the beginning of a flag
if [ "$opt" == '-qO' ] || [ "$opt" == '-O' ] || [ "$opt" == '-q' ]; then
true
else #any other wget command-flags other than '-qO', '-O', '-q'
use=wget
break
fi
elif [[ "$opt" == *'://'* ]]; then
#this opt is web address
url="$opt"
elif [[ "$opt" == '/'* ]]; then
#this opt is file output
if [ -z "$file" ];then
file="$opt"
else #file var already populated
use=wget
break
fi
else
#this opt does not begin with '-', contain '://', or begin with '/'. Assume output file specified shorthand
if [ -z "$file" ];then
file="$(pwd)/${opt}"
else #file var already populated
use=wget
break
fi
fi
done
else
#aria2c command not found
use=wget
fi
if [ "$use" == wget ];then
#run the true wget binary with all this function's args
command wget "$@"
elif [ "$use" == aria2c ];then
#make default filename if $file empty
if [ -z "$file" ];then
file="$(pwd)/$(basename "$url")"
fi
#suppress output if -q flag passed
if echo "$@" | grep -q '\-q' ;then
aria2c -c -x 16 -s 16 -m 10 --retry-wait 30 "$url" --dir '/' -o "${file:1}" --allow-overwrite --summary-interval=1 >/dev/null
else
aria2c -c -x 16 -s 16 -m 10 --retry-wait 30 "$url" --dir '/' -o "${file:1}" --allow-overwrite --summary-interval=1
fi
fi
}
#if this script is being run standalone, not sourced
if [[ "$0" == */api ]];then
if [ ! -z "$1" ];then
#if user input a function command, then run it with arguments.
#Keep in mind this could run any command the user wanted, not necessarily exclusively function commands.
"$@"
fi
fi