538 lines
17 KiB
Bash
Executable File
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
|