scripts/twtxt/txtsh

538 lines
17 KiB
Bash
Executable File

#!/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