#!/usr/bin/env bash # Hopefully one day this will be a full featured Pleroma client. # Let's see how far we can get. :) # Handle subprocesses that may not close with the main program. trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT # Display usage information. help() { echo "${0##*/}" echo "Released under the terms of the WTFPL License" echo -e "Usage:\n" echo "With no arguments, open the interactive client." for i in "${!command[@]}" ; do echo "-${i/:/ }: ${command[${i}]}" done | sort echo echo "Configuration files can be found in ${configPath}" echo exit 0 } # Get an oauth token get_oauth_token() { echo "Welcome to ${softwareName}!" echo echo "Let's get you connected to your instance." while [[ -z "${instanceURL}" ]]; do echo read -er -p "Enter the URL of a Pleroma instance: " instanceURL if [[ ! "${instanceURL}" =~ ^https:// ]]; then instanceURL="https://${instanceURL}" fi done redirectURI="urn:ietf:wg:oauth:2.0:oob" website="https://git.stormux.org/storm/ratatoskr" # get client id and secret curl -s -X POST -d client_name="${softwareName}" -d "redirect_uris=${redirectURI}" -d "scopes=read write follow push" -d "website=${website}" "${instanceURL}/api/v1/apps" | jq --raw-output '"client_id=\"\(.client_id)\"\nclient_secret=\"\(.client_secret)\""' > "${configPath}/${configFile}" # Load the new variables from the configuration file source "${configPath}/${configFile}" # Create the url to get the oauth token local url="${instanceURL}/oauth/authorize?client_id=${client_id}&redirect_uri=${redirectURI}&response_type=code&scope=read%20write%20follow%20push" echo "Please open the following url in your browser." echo "Copy the generated token, and paste it here, then press enter to continue." echo if command -v xclip &> /dev/null ; then echo "${url}" | tee >(xclip -selection clipboard -d "${DISPLAY:-:0}" 2> /dev/null && echo "For convenience the url has been copied to your clipboard.") else echo "${url}" fi echo read -er oauth_token # This is actually a authorization token, so get the actual token. oauth_token="$(curl -sS -d "client_id=${client_id}" -d "client_secret=${client_secret}" -d "code=${oauth_token}" -d "grant_type=authorization_code" "${instanceURL}/oauth/token" | jq --raw-output '(.access_token)')" echo "oauth_token=\"${oauth_token}\"" >> "${configPath}/${configFile}" echo "instanceURL=\"${instanceURL}\"" >> "${configPath}/${configFile}" } play_sound() { if [[ "${enable_sound}" == "false" ]]; then return fi local soundFile="${configPath}/soundpacks/${sound_pack:-default}/${1}.opus" if [[ -e "${soundFile}" ]]; then sox -qV0 "${soundFile}" -d &> /dev/null & fi } # Functions that deal with posting. # Scrobble music with -S flag scrobble_music() { local result result="$(curl -sS --oauth2-bearer "${oauth_token}" \ -d "$(playerctl metadata -f 'album={{album}}')" \ -d "$(playerctl metadata -f 'artist={{artist}}')" \ -d "$(playerctl metadata -f 'title={{title}}')" \ "${instanceURL}/api/v1/pleroma/scrobble")" # Check for errors if [[ $? -ne 0 ]]; then echo "there was a problem contacting the server" play_sound error exit 1 fi local error="$(echo "$result" | jq -r '.error')" if [[ "$error" != "null" ]]; then echo "Error: $error" play_sound error exit 1 fi echo "Track scrobbled!" play_sound scrobble } # Post music with -M flag requires playerctl. post_music() { local text="$(playerctl metadata -f 'Now playing "{{title}}" by "{{artist}}" from "{{album}}" via "{{playerName}}"')" text="${text//Now playing \"\"/Now Playing}" text="${text// by \"\"/}" text="${text// from \"\"/}" if [[ "${text}" =~ ^"Now playing via" ]]; then echo "Error, no music was detected. Maybe it is not properly tagged?" play_sound error exit 1 fi local link="$(playerctl metadata -f '{{xesam:url}}')" if [[ "${link}" =~ ^file:// ]]; then link="https://www.youtube.com/results?search_query=$(playerctl metadata -f '{{artist}} {{title}}' | urlencode -b)" fi local json=$(jq -n --arg status "[${text}](${link})" --arg spoiler_text "Music" --arg content_type "text/markdown" '{status: $status, spoiler_text: $spoiler_text, content_type: $content_type}') local result result="$(curl -sS --oauth2-bearer "${oauth_token}" -H "Content-Type: application/json" \ -d "$json" \ "${instanceURL}/api/v1/statuses")" # Check for errors if [[ $? -ne 0 ]]; then echo "there was a problem contacting the server" play_sound error exit 1 fi local error="$(echo "$result" | jq -r '.error')" if [[ "$error" != "null" ]]; then echo "Error: $error" play_sound error exit 1 fi echo "Music posted!" play_sound post_music } # Post status with -p flag, command line. post_status() { if [[ "${1}" == "" ]]; then return fi local statusText="$@" local statusVisibility="${statusVisibility:-public}" local statusContent_type="${statusContent_type:-text/markdown}" local statusSpoiler_text="" local statusJson="$(jq -n --arg status "$statusText" \ --arg spoiler_text "$statusSpoiler_text" \ --arg visibility "$statusVisibility" \ --arg content_type "$statusContent_type" \ '{ status: $status, spoiler_text: $spoiler_text, visibility: $visibility, content_type: $content_type }')" local statusResult statusResult="$(curl -sS --oauth2-bearer "${oauth_token}" -H "Content-Type: application/json" \ -d "$(echo "$statusJson" | jq 'if .spoiler_text == "" then del(.spoiler_text) else . end | if .visibility == "" then del(.visibility) else . end')" \ "${instanceURL}/api/v1/statuses")" if [[ $? -ne 0 ]]; then echo "there was a problem contacting the server" play_sound error exit 1 fi local error="$(echo "$statusResult" | jq -r '.error')" if [[ "$error" != "null" ]]; then echo "Error: $error" play_sound error exit 1 fi echo "Status posted!" play_sound post } # Display timelines show_timeline() { result="$(curl -sS --oauth2-bearer "${oauth_token}" "${instanceURL}/api/v1/timelines/${timeline:-home}")" # Error checking # Check if the result is a valid JSON if ! echo "$result" | jq '.' >/dev/null 2>&1; then echo "Error: The response from the server was invalid." play_sound error return fi if [[ "${result}" == "${oldResult}" ]]; then return elif [[ ${#oldResult} -gt 5 ]]; then local temperaryVariable="${result}" result="${result//${oldResult%\]}/}" result="${result%,\]}]" oldResult="${temperaryVariable}" play_sound new_${timeline:-home} fi error=$(echo "$result" | jq -r '.error // "null"' 2> /dev/null) if [[ "${error:-null}" != "null" ]]; then echo "Error fetching ${timeline:-home} timeline: $error" play_sound error return fi # handle new events events="$(jq -r '.[].content' <<< "$result")" spoiler_text="$(jq -r '.[].spoiler_text' <<< "$result")" created_at="$(jq -r '.[].created_at' <<< "$result")" stripped_events="$(echo "$events" | sed -E 's/<[^>]+>//g')" stripped_events="${stripped_events//'/\'}" usernames="$(jq -r '.[].account.username' <<< "$result")" echo -e "$usernames" | while read -r username; do spoiler="$(echo "$spoiler_text" | head -n 1)" created="$(echo "$created_at" | head -n 1)" created="${created%.*}" created="${created/T/ at }" if [ -z "$spoiler" ]; then echo "$username: $(echo "$stripped_events" | head -n 1)" echo "Posted $created" else echo "$username [$spoiler]: $(echo "$stripped_events" | head -n 1)" echo "Posted $created" fi stripped_events="$(echo "$stripped_events" | tail -n +2)" spoiler_text="$(echo "$spoiler_text" | tail -n +2)" created_at="$(echo "$created_at" | tail -n +2)" echo done oldResult="${result}" } # Variable initialization configPath="${XDG_CONFIG_HOME:-$HOME/.config}/ratatoskr" # Path for settings, usually ~/.config/ratatoskr configFile="default.conf" # The default config file, eventually will support multiple accounts. softwareName="Ratatoskr" # The name of the client. # Main code starts here # Check for dependencies dependencies=( "jq" "sox" "urlencode" ) for i in "${dependencies[@]}" ; do if ! command -v "$i" &> /dev/null ; then echo "Missing dependency: $i" exit 2 fi done # make sure the configuration and soundpack paths exist: mkdir -p "${configPath}/soundpacks" # if the default file doesn't exist, create it if [[ ! -e "${configPath}/${configFile}" ]]; then get_oauth_token else # Read configuration file source "${configPath}/${configFile}" fi # Associative array of command line parameters and short description of what they do. declare -A command=( [C]="Recreate default configuration file. Acquire new oauth token." [h]="Help, show usage information for ${0##*/}." [M]="Post the currently playing music track, requires playerctl." [p::]="Post from the command line, e.g. ${0##*/} -p \"hello world\"" [S]="Scrobble the currently playing music track, requires playerctl." ) # Handle command line parameters # Convert the keys of the associative array to a format usable by getopt shortOptions="${!command[*]}" shortOptions="${shortOptions//[[:space:]]/}" options="$(getopt -o "$shortOptions" -- "$@")" while true; do case $1 in -C) get_oauth_token shift;; -h) help exit 0;; -M) post_music exit 0;; -p) if [ -z "$2" ]; then post_status "$(cat)" else shift post_status "$@" fi exit 0;; -S|--scrobble-music) scrobble_music exit 0;; *) # unexpected option help exit 1;; esac done # Main loops # Display timelines and requested information. while : ; do show_timeline sleep "${interval:-300}" done & # Handle commands while : ; do # Command prompt: read -er command if [[ ! "${command}" =~ ^/ ]]; then post_status "${command}" continue fi case "${command}" in "/exit"|"/quit") exit 0 ;; "/refresh") show_timeline ;; *) echo "Error: '${command}' is not a valid command." play_sound error ;; esac done exit 0