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