#!/bin/bash _name="txtsh" _author="mio" _desc="(t[e^]kst\"ish), n. a Bash shell client for twtxt, a microblogging \ service." _version="0.3" _moddate="2017-03-05" _license="BSD-3" # Defaults _def_conf="$HOME/.config/$_name/config" # Config options _options=(\ "nick = user" \ "twtfile = $HOME/.config/$_name/twtxt.txt" \ "twturl = https://example.tld/twtxt.txt" \ "check_following = True" \ "use_pager = False" \ "porcelain = False" \ "disclose_identity = False" \ "character_limit = 140" \ "character_warning = 140" \ "limit_timeline = 20" \ "timeout = 5.0" \ "sorting = descending" \ "pre_tweet_hook = \"\"" \ "post_tweet_hook = \"\"" \ # The options below are not part of twtxt spec "character_limit_on = True" \ "character_warning_on = True" \ "limit_search = 20" \ "editor = vi" \ ) _registries=(\ "https://registry.twtxt.org/api/plain/" \ ) # (Formatting) Unicode symbols and ANSI escape codes _fmt_mkr="🞚 " _fmt_url="⛺ " _fmt_date="⌚ " _fmt_bold="\033[1m" _fmt_reset="\033[0m" _fmt_ts="+%Y-%m-%d %H:%M %Z" # Create config and data files create_config() { mkdir -p "$(dirname $_def_conf)" # Replace default values with user input if [ -n "$1" ]; then _options[0]="nick = $1" else _options[0]="nick = $(whoami)"; fi if [ -n "$2" ]; then _options[1]="twtfile = $2"; fi if [ -n "$3" ]; then _options[2]="twturl = $3"; fi # Output config echo -e "[twtxt]" > $_def_conf for opt in "${_options[@]}"; do echo -e "$opt" >> $_def_conf done echo -e "\n[following]\n\n[registries]" >> $_def_conf for opt in "${_registries[@]}"; do echo -e "$opt" >> $_def_conf done # Create twtfile if it doesn't exist if [ ! -f "${_options[1]/* = /}" ]; then mkdir -p "$(dirname ${_options[1]/* = /})" touch "${_options[1]/* = /}" fi } # Load config file load_config() { if [ ! -f $_def_conf ]; then echo -e "Error: no config file found. Try running \ \"$_name quickstart\" first."; exit 1 else # Load config into array local old_ifs=$IFS local csection readarray buffer < $_def_conf IFS=$'\n' for line in ${buffer[@]}; do case "$line" in \[twtxt\]) csection="twtxt";; \[following\]) csection="following";; \[registries\]) csection="registries";; esac # Store followings and registries in one variable each if [ "$csection" = "following" ] && \ [ ! "$line" = "[following]" ]; then following="${following}$line\n" fi if [ "$csection" = "registries" ] && \ [ ! "$line" = "[registries]" ]; then registries=("${registries[@]}" "$line") fi # Set variables for the other options if [ -n "$line" ] && [[ ! "$line" =~ "[" ]] && \ [ ! "$csection" = "following" ] && \ [ ! "$csection" = "registries" ]; then # if: user config value is empty, reset to default value # else: assign value if [[ "${line/* = /}" =~ "=" || -z "${line/* = /}" ]] && \ [ "$csection" = "twtxt" ]; then for opt in "${_options[@]}"; do if [[ "$opt" =~ "${line/ */}" ]]; then eval "${opt/ = */}=${opt/* = /}" fi done else eval "${line/ = */}=${line/* = /}" fi fi done IFS=$old_ifs fi } # Edit config values edit_config() { if [ -z "$1" ]; then echo -e "Please specify a setting and value."; exit 1 elif [ -n "$1" ] && [ -n "$2" ]; then # Edit values load_config case "$1" in nick) sed -i "s|nick = $nick|nick = $2|" $_def_conf;; twtfile) sed -i "s|twtfile = $twtfile|twtfile = $2|" $_def_conf;; twturl) sed -i "s|twturl = $twturl|twturl = $twturl|" $_def_conf;; *) echo -e "See \"$_name config\" for options." esac else # Output current values load_config case "$1" in nick) echo -e "$nick";; twtfile) echo -e "$twtfile";; twturl) echo -e "$twturl";; edit) eval $editor "$_def_conf";; *) cat $_def_conf;; esac fi } # Wrap curl commands with presets # to manage flags for all outgoing requests curlp() { local ccmd="curl -L -s --connect-timeout $timeout" # Include user-agent string with outgoing requests if enabled if [ "$disclose_identity" = "True" ]; then ccmd="${ccmd} -A $_name (twtxt)/$_version (+$twturl; @$nick)" fi case $1 in post) echo -e "$($ccmd -X POST $2)";; src) echo -e "$($ccmd $2)";; status) echo -e $($ccmd -I $2 | head -n 1 | cut -f 2 -d " ");; esac } # Format a string containing twtxt data for display format_twdata() { local tdata ts twt local old_ifs=$IFS # Limit number of results and sort order case $1 in search) tdata=`echo -e "$2" | tail -n $limit_search` if [ "$sorting" = "descending" ]; then tdata=$(echo -e "$tdata" | sort -r) fi;; timeline*) tdata=`echo -e "$2" | tail -n $limit_timeline` if [ "$sorting" = "descending" ]; then tdata=$(echo -e "$tdata" | sort -r) fi;; esac # Display data # Porcelain view if [ "$porcelain" = "True" ]; then echo -e "$tdata"; exit 0 fi # Formatted view IFS=$'\n' for item in ${tdata[@]}; do case $1 in search|timeline) # Format: nick, twturl, timestamp, tweet # Disable ANSI codes when use_pager is enabled if [ "$use_pager" = "True" ]; then _fmt_bold=""; _fmt_reset="" fi # Format fields ts=$(echo ${item} | cut -f 3) echo -e $_fmt_mkr${_fmt_bold}$(echo ${item} | cut -f 1)\ ${_fmt_reset} echo -e $_fmt_url$(echo ${item} | cut -f 2) echo -e $_fmt_date$(date -d$ts $_fmt_ts) twt=`echo ${item} | cut -f 4` if [ "$character_limit_on" = "True" ] && \ [ ${#twt} -gt $character_limit ]; then twt=$(echo -e "$twt" | cut -c 1-$character_limit) fi # Check tweet is not empty string # e.g. user search has no tweets if [ -z "$twt" ]; then echo ""; else echo -e $twt"\n"; fi;; timeline_me) # Format: timestamp, tweet ts=$(echo ${item} | cut -f 1) echo -e $_fmt_date$(date -d$ts $_fmt_ts) echo -e $(echo ${item} | cut -f 2)"\n";; esac done IFS=$old_ifs } # Check for pager before output if available and enabled # Takes data formatting type and twtxt data string as inputs output_twdata() { local fdata local if_pager=`whereis less | grep "/less"` if [ "$use_pager" = "True" ] && [ -n "$if_pager" ]; then fdata=`format_twdata "$1" "$2"` echo -e "$fdata" > $(dirname $_def_conf)/pager.tmp less $(dirname $_def_conf)/pager.tmp rm -rf $(dirname $_def_conf)/pager.tmp else format_twdata "$1" "$2" fi } # Add a source to following list follow() { local is_following=$(echo $following | grep "$2") if [ -n "$1" ] && [ -n "$2" ] && [ -z "$is_following" ]; then sed -i "s|\[following\]|\[following\]\n$1\ =\ $2|" $_def_conf echo -e "You are now following ${_fmt_bold}$1${_fmt_reset}." elif [ -n "$is_following" ]; then echo -e "You are already following ${_fmt_bold}$1${_fmt_reset}." else echo -e "Usage: $_name follow [nick] [url]" fi } # List sources user is following following() { if [ -z "$following" ]; then echo -e "You haven't followed anyone yet." else local fol local fstatus="404" printf "$following" | while read line; do # Format: nick @ twturl (status_code) fol="${_fmt_bold}${line/ = */}${_fmt_reset} @ ${line/* = /}" if [ "$check_following" = "True" ]; then fstatus=`curlp "status" "${line/* = /}"` fol="${fol} ($fstatus)" fi echo -e "$fol" done fi } # Interactive setup quickstart() { echo -e "---------- $_name quickstart ----------\n" # Check for existing conf if [ -f "$_def_conf" ]; then read -p "A config file already exists. Overwrite it? [Y/n] " ow_config while [[ ! "$ow_config" = "y" && ! "$ow_config" = "n" ]]; do read -p "Please answer 'y' or 'n': " ow_config done if [ "$ow_config" = "n" ]; then mv $_def_conf $_def_conf-$(date +"%Y-%m-%d-%H%M"); echo -e "Old config copied to $_def_conf-$(date +"%Y-%m-%d-%H%M")" fi fi read -p "What's your nick? [$(whoami)] " nick read -p "Where do you want to store your tweets? (Enter full path) \ [${_options[1]/* = /}] " twtfile read -p "Where can others find your tweets? (File should end in .txt) \ [${_options[2]/* = /}] " twturl create_config "$nick" "$twtfile" "$twturl" echo -e "Config created in $(dirname $_def_conf). Ready to tweet!" } # Add user to registry api_add_user() { local resp=`curlp "post" \ "${registries[0]}users?url=$twturl&nickname=$nick"` case $resp in OK) echo -e "Your nick has been added to the registry. Hooray!";; *) echo -e "Error: $resp";; esac } # Search registry for a tag, tweet or user api_search() { if [ -n "$1" ] && [ -n "$2" ]; then local src_data case $1 in keyword) src_data=`curlp "src" "${registries[0]}tweets?q=$2"`;; tag) src_data=`curlp "src" "${registries[0]}tags/$2"`;; user) src_data=`curlp "src" "${registries[0]}users?q=$2"`;; *) echo "Invalid search option. Options: keyword, tag, user";; esac if [ -z "$src_data" ]; then echo -e "No search results found."; exit 0 else output_twdata "search" "$src_data" fi else echo -e "Usage: $_name search keyword|tag|user [keyword]" fi } # Get timeline by type: user, mentions (registry), public (registry) api_timeline() { local ftype src_data case $1 in me) ftype="timeline_me" src_data=`cat $twtfile` if [ -z "$src_data" ]; then echo -e "Nothing to see yet. Try tweeting first?"; exit 0 fi;; mentions) ftype="timeline" src_data=`curlp "src" "${registries[0]}mentions?url=$twturl"` if [ -z "$src_data" ]; then echo -e "No mentions found."; exit 0 fi;; public) ftype="timeline" src_data=`curlp "src" "${registries[0]}tweets"` if [ -z "$src_data" ]; then echo -e "No tweets found."; exit 0 fi;; *) echo -n "Usage: $_name timeline me|mentions|public"; exit 1;; esac output_twdata "$ftype" "$src_data" } # View another user's timeline given a nick or source url view() { local src src_data if [ -n "$1" ]; then # if: check whether it's a source and attempt to resolve url # elif: check for nick match # else: exit with no match found if [[ "$1" =~ "http://" || "$1" =~ "https://" ]]; then src_data=`curlp "src" "$1"` elif [ -z "$src_data" ]; then src=`cat $_def_conf | grep "$1"` if [ -n "$src" ]; then src_data=`curlp "src" "${src/* = /}"` fi else echo -e "No tweets found from nick/source."; exit 1 fi echo -e "Timeline of ${_fmt_bold}$1${_fmt_reset}:\n" output_twdata "timeline_me" "$src_data" else echo -e "Usage: $_name view [source]" fi } # Load tweet hook commands tweet_hook() { local hook msg case "$1" in pre) hook="$pre_tweet_hook" msg="Running pre-tweet hook ...";; post) hook="$post_tweet_hook" msg="Running post-tweek hook ...";; esac hook=`echo -e "$hook" | sed "s|\"||g"` if [ -n "$hook" ]; then echo -e "$msg" eval "$hook" || exit 1 echo -e "Done." fi } # Add a tweet to user timeline tweet() { if [ ! -f "$twtfile" ]; then touch "$twtfile"; fi # Launch external editor to compose local buffer=$(dirname $_def_conf)/tweet.tmp $editor $buffer # Check for empty tweet if [ ! -f $buffer ] || [ -z "$(cat $buffer)" ]; then exit 0; fi # Alert user if character limit warning is enabled and limit exceeded local twt="$(cat $buffer)" while [ ${#twt} -gt $character_warning ] && \ [ "$character_warning_on" = "True" ]; do read -p "Your tweet is ${#twt} chars long (limit $character_warning \ chars). What would you like to do? [e]dit / [i]gnore : " resp_limit while [[ ! "$resp_limit" = "e" && ! "$resp_limit" = "i" ]]; do read -p "Please answer 'e' (edit) or 'i' (ignore) : " \ resp_limit done case $resp_limit in e) $editor $buffer;; i) break;; esac done # Save tweet tweet_hook "pre" echo -e "$(date +%FT%T%:z)\t$(cat $buffer)" >> $twtfile rm -rf $buffer echo -e "Tweet added. Cheers!" tweet_hook "post" } # Add new tweet (inline variant) # Limitation: like most bash shell scripts, using special characters at # the prompt without manual escaping will cause bash to throw an error. # Retained for convenience. quicktweet() { if [ ! -f "$twtfile" ]; then touch "$twtfile"; fi if [ -z "$1" ]; then echo -e "No empty tweets, please. Try again?"; exit 0 fi # Alert user if character limit warning is enabled and limit exceeded local twt="$1" while [ ${#twt} -gt $character_warning ] && \ [ "$character_warning_on" = "True" ]; do read -p "Your tweet is ${#twt} chars long (limit $character_warning \ chars). What would you like to do? [e]dit / [i]gnore : " resp_limit while [[ ! "$resp_limit" = "e" && ! "$resp_limit" = "i" ]]; do read -p "Please answer 'e' (edit) or 'i' (ignore) : " \ resp_limit done case $resp_limit in e) read -e -i "$twt" -p "Edit tweet: " twt;; i) break;; esac done # Save tweet tweet_hook "pre" echo -e "`date +%FT%T%:z`\t$twt" >> $twtfile echo -e "Tweet added. Cheers!" tweet_hook "post" } # Unfollow a source unfollow() { local is_following=$(echo $following | grep "$1") if [ -n "$1" ] && [ -n "$is_following" ]; then sed -i "s|.*$1|__unfollowdelete|" $_def_conf printf "$(cat $_def_conf | sed "/__unfollowdelete/d")" > $_def_conf echo -e "You have unfollowed \ ${_fmt_bold}${is_following/ = */}${_fmt_reset} @ $1." elif [ -z "$is_following" ]; then echo -e "You're not currently following this source." else echo -e "Please specify a source url." fi } # Command switch case "$1" in config) edit_config $2 $3;; follow) load_config; follow $2 $3;; following) load_config; following;; qt) load_config; quicktweet "$2";; quickstart) quickstart;; register) load_config; api_add_user;; search) load_config; api_search $2 $3;; timeline) load_config; api_timeline $2;; tweet) load_config; tweet;; unfollow) load_config; unfollow $2;; view) load_config; view $2 $3;; --version|version) echo -e "$_name v. $_version";; *) echo -e "$_name — $_desc\n\n\ Usage: $_name [command] [args]\n\n\ Commands:\n\ config\t\tEdit the config file\n\ follow\t\tAdd a new source to follow\n\ following\t\tList sources you are following\n\ qt\t\t\tQuickly add a new tweet\n\ quickstart\t\tSet up $_name (run this first!)\n\ register\t\tAdd your nick to registry (optional for discoverability)\n\ search\t\tSearch registry by keyword, tag or user\n\ timeline\t\tView your timeline, mentions or public timeline\n\ tweet\t\tAdd a new tweet from a preferred text editor\n\ unfollow\t\tRemove a source from your following list\n\ view\t\tView a source by url\n\ version\t\tShow the version\n\ help\t\tShow this help message\n\n\ Examples:\n\ $_name config nick MyNick\t\t\tChange your nick to \"MyNick\"\n\ $_name config edit\t\t\t\tEdit config file in an editor (default: vi)\n\ $_name follow Foo https://foo.tld/twtxt.txt\tFollow user \"Foo\"\n\ $_name search tag blog\t\t\tSearch registry for tweets with \"blog\" tag\n\ $_name timeline me\t\t\t\tView your timeline\n\ $_name qt \"Hello world!\"\t\t\tTweet a message\n\ $_name unfollow https://foo.tld/twtxt.txt\tRemove url from followings\n\ $_name view Foo\t\t\t\tView user Foo's twtxt if already on followings\n\ $_name view https://foo.tld/twtxt.txt\tView source url\n\ ";; esac