#!/bin/bash

# Bail on error
set -e

# Constants
NAME="twiping"
DEFAULT_CONFIG_FILE='/etc/twilio.conf'
BASE_URL='https://api.twilio.com/'
ACCOUNTS_PATH='/2010-04-01/Accounts/${ACCOUNT_SID}/Messages'
RESULT_XSL='/usr/share/twilio-utils/result.xsl'
TWIPING_XSL='/usr/share/twilio-utils/twiping.xsl'
FORMAT_STRING="TEST%s"
SLEEP_INTERVAL='3'
REPLY_PATTERN="@TESTCODE@"
CURL="/usr/bin/curl"
SED="/usr/bin/sed"
GREP="/usr/bin/grep"
EXPR="/usr/bin/expr"
DATE="/usr/bin/date"
SLEEP="/usr/bin/sleep"
HEXDUMP="/usr/bin/hexdump"
XSLTPROC="/usr/bin/xsltproc"
MAX_WAIT_TIME="30"
PAGE_SIZE="500"
DATE_PARSING_STYLE="Linux"

# Usage message
usage()
{
    echo "Usage: ${NAME} [options] dest-number" 1>&2
    echo "Options:" 1>&2
    echo "    -C curlflag       Pass curlflag to curl(1)" 1>&2
    echo "    -c file           Specify config file (default \"${DEFAULT_CONFIG_FILE}\")" 1>&2
    echo "    -F from-number    Specify sending phone number (E.164 format)" 1>&2
    echo "    -f format         Specify alternate printf(1) format string (default \"${FORMAT_STRING}\")" 1>&2
    echo "    -m reply-pattern  Specify extended regular expression with which to match response" 1>&2
    echo "    -p auth-token     Specify auth token (default read AUTH_TOKEN from config file)" 1>&2
    echo "    -P                Ask for auth token from the terminal" 1>&2
    echo "    -t testcode       Assume outgoing SMS message with \"testcode\" has already been sent" 1>&2
    echo "    -u account-sid    Specify Account SID (default read ACCOUNT_SID from config file)" 1>&2
    echo "    -w timeout        Specify maximum response timeout in seconds (default ${MAX_WAIT_TIME})" 1>&2
}

# Function to normalize a phone number to the way Twilio likes it
normalize()
{
    # If already in E.164 format, then leave it alone
    if [[ "${1}" =~ ^\+ ]]; then
        echo "${1}"
        return
    fi

    # Clean up NANP numbers
    echo ${1+"$@"} | sed -r \
      -e 's/[^0-9]//g' \
      -e 's/^1?([2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4})$/+1\1/g'
}

# Parse a timestamp and print the result in UTC:
#   $1 = Twilio date
#   $2 = Output format string
parse_date()
{
    case "${DATE_PARSING_STYLE}" in
        Linux)
            "${DATE}" -u -d "${1}" "+${2}"
            ;;
        BSD)
            "${DATE}" -u -j -f '%a, %d %b %Y %T %z' "${1}" "+${2}"
            ;;
        *)
            echo "twilog: unknown date parsing style: ${DATE_PARSING_STYLE}" 1>&2
            exit 1
            ;;
    esac
}

# Parse flags passed in on the command line
CONFIG_FILE="${DEFAULT_CONFIG_FILE}"
TRUNCATE_LIMIT="${DEFAULT_TRUNCATE_LIMIT}"
OVERRIDE_FROM_NUMBER=""
OVERRIDE_ACCOUNT_SID=""
OVERRIDE_AUTH_TOKEN=""
OVERRIDE_REPLY_PATTERN=""
CURL_FLAGS=""
TESTCODE=""
while [ ${#} -gt 0 ]; do
    case "$1" in
        -c)
            shift
            CONFIG_FILE="${1}"
            shift
            ;;
        -C)
            shift
            CURL_FLAGS="${CURL_FLAGS} ${1}"
            shift
            ;;
        -F)
            shift
            OVERRIDE_FROM_NUMBER="${1}"
            shift
            ;;
        -f)
            shift
            FORMAT_STRING="${1}"
            shift
            ;;
        -m)
            shift
            OVERRIDE_REPLY_PATTERN="${1}"
            shift
            ;;
        -p)
            shift
            OVERRIDE_AUTH_TOKEN="${1}"
            shift
            ;;
        -P)
            OVERRIDE_AUTH_TOKEN="PROMPT"
            shift
            ;;
        -t)
            shift
            TESTCODE="${1}"
            shift
            ;;
        -u)
            shift
            OVERRIDE_ACCOUNT_SID="${1}"
            shift
            ;;
        -w)
            shift
            MAX_WAIT_TIME="${1}"
            shift
            ;;
        -h|--help)
            usage
            exit
            ;;
        --)
            shift
            break
            ;;
        *)
            break
            ;;
    esac
done
case "${#}" in
    1)
        TO_NUMBER="$1"
        ;;
    *)
        usage
        exit 1
        ;;
esac

# Ensure config file is readable; if not, then what we need must be provided on the command line
if ! test -r "${CONFIG_FILE}"; then
    if test -z "${OVERRIDE_FROM_NUMBER}" -o -z "${OVERRIDE_ACCOUNT_SID}" -o -z "${OVERRIDE_AUTH_TOKEN}"; then
        echo "${NAME}: can't read ${CONFIG_FILE}" 1>&2
        exit 1
    fi
else
    # Parse config file
    . "${CONFIG_FILE}"
fi

# Override acccount ID from command line
if [ -n "${OVERRIDE_ACCOUNT_SID}" ]; then
    ACCOUNT_SID="${OVERRIDE_ACCOUNT_SID}"
fi

# Input auth token
if [ "${OVERRIDE_AUTH_TOKEN}" = 'PROMPT' ]; then
    read -s -p "Enter auth token for ${ACCOUNT_SID}: " OVERRIDE_AUTH_TOKEN
    echo 1>&2
fi

# Override some config with command line flags
if [ -n "${OVERRIDE_FROM_NUMBER}" ]; then
    FROM_NUMBER="${OVERRIDE_FROM_NUMBER}"
fi
if [ -n "${OVERRIDE_AUTH_TOKEN}" ]; then
    AUTH_TOKEN="${OVERRIDE_AUTH_TOKEN}"
fi
if [ -n "${OVERRIDE_REPLY_PATTERN}" ]; then
    REPLY_PATTERN="${OVERRIDE_REPLY_PATTERN}"
fi

# Normalize numbers
FROM_NUMBER=`normalize "${FROM_NUMBER}"`
TO_NUMBER=`normalize "${TO_NUMBER}"`

# Sanity check stuff
if ! [[ "${FROM_NUMBER}" =~ ^(\+1[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}|[0-9]{5,6})$ ]]; then
    echo "${NAME}: invalid source phone number \`${FROM_NUMBER}'" 1>&2
    exit 1
fi
if ! [[ "${TO_NUMBER}" =~ ^(\+1[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}|[0-9]{5,6})$ ]]; then
    echo "${NAME}: invalid destination phone number \`${TO_NUMBER}'" 1>&2
    exit 1
fi
if ! [[ "${ACCOUNT_SID}" =~ ^AC[0-9a-f]{32}$ ]]; then
    echo "${NAME}: invalid account SID \`${ACCOUNT_SID}'" 1>&2
    exit 1
fi
if ! [[ "${AUTH_TOKEN}" =~ ^[0-9a-f]{32}$ ]]; then
    echo "${NAME}: invalid authentication token" 1>&2
    exit 1
fi

# Build URL
URL="`echo ${BASE_URL} | sed 's|/$||g'`""`eval echo ${ACCOUNTS_PATH}`"

# Create temporary files for message response and error
RESPONSE_FILE=`mktemp -q /tmp/${NAME}.XXXXXX`
if [ $? -ne 0 ]; then
    echo "${NAME}: can't create temporary file" 1>&2
    exit 1
fi
ERROR_FILE=`mktemp -q /tmp/${NAME}.XXXXXX`
if [ $? -ne 0 ]; then
    rm -f ${RESPONSE_FILE}
    echo "${NAME}: can't create temporary file" 1>&2
    exit 1
fi
trap "rm -f ${RESPONSE_FILE} ${ERROR_FILE}" 0 2 3 5 10 13 15

# Generate and send test code if not already sent
if [ -z "${TESTCODE}" ]; then

    # Generate random test string
    TESTCODE="`${HEXDUMP} -n 4 -v -e '1/1 "%02x"' /dev/urandom`"

    # Send test code to Twilio
    printf "${FORMAT_STRING}" "${TESTCODE}" \
      | "${CURL}" --silent \
      --user "${ACCOUNT_SID}:${AUTH_TOKEN}" \
      --data "From=${FROM_NUMBER}" \
      --data "To=${TO_NUMBER}" \
      --data-urlencode "Body@-" \
      --output "${RESPONSE_FILE}" \
      ${CURL_FLAGS} \
      "${URL}"
fi

# Check result
CURL_RESULT="$?"
if [ "${CURL_RESULT}" -ne 0 ]; then
    echo "${NAME}: error sending request (curl returned ${CURL_RESULT})" 1>&2
    exit 1
fi

# Apply XSLT to returned XML to get error message
"${XSLTPROC}" "${RESULT_XSL}" "${RESPONSE_FILE}" > "${ERROR_FILE}" 2>&1
if [ $? -ne 0 ]; then
    echo -n "${NAME}: error parsing result:" 1>&2
    cat "${ERROR_FILE}" 1>&2
    exit 1
fi

# Was there an error returned?
if [ -s "${ERROR_FILE}" ]; then
    echo -n "${NAME}: " 1>&2
    cat "${ERROR_FILE}" 1>&2
    exit 1
fi

# Substitute test code into ${REPLY_PATTERN}
REPLY_PATTERN=`echo "${REPLY_PATTERN}" | sed 's|@TESTCODE@|'"${TESTCODE}"'|g'`

# Wait for response
START_TIME=`"${DATE}" -u +%s`
while true; do

    # Check elapsed time
    CURRENT_TIME=`"${DATE}" -u +%s`
    ELAPSED_TIME=`"${EXPR}" "${CURRENT_TIME}" - "${START_TIME}" || true`
    if [ "${ELAPSED_TIME}" -ge "${MAX_WAIT_TIME}" ]; then
        echo "${NAME}: no response after ${ELAPSED_TIME} seconds" 1>&2
        exit 2
    fi

    # Query messages
    "${CURL}" \
      --silent --compressed \
      --user "${ACCOUNT_SID}:${AUTH_TOKEN}" \
      --output "${RESPONSE_FILE}" \
      ${CURL_FLAGS} \
      "${URL}?PageSize=${PAGE_SIZE}"

    # Check result
    CURL_RESULT="$?"
    if [ "${CURL_RESULT}" -ne 0 ]; then
        echo "${NAME}: error retrieving messages (curl returned ${CURL_RESULT})" 1>&2
        exit 1
    fi

    # Update elapsed time
    CURRENT_TIME=`"${DATE}" -u +%s`
    ELAPSED_TIME=`"${EXPR}" ${CURRENT_TIME} - ${START_TIME} || true`

    # Apply XSLT to returned XML to extract incoming messages
    "${XSLTPROC}" "${TWIPING_XSL}" "${RESPONSE_FILE}" > "${ERROR_FILE}" 2>&1
    if [ $? -ne 0 ]; then
        echo -n "${NAME}: error parsing messages: " 1>&2
        cat "${ERROR_FILE}" 1>&2
        exit 1
    fi

    # Search for reply containing our test code detected after ${START_TIME}, if any
    while IFS=/ read _SID _FROM _TO _DATE _BODY; do
        RESPONSE_TIME=`parse_date "${_DATE}" '%s'`
        if [ "${RESPONSE_TIME}" -ge "${START_TIME}" ]; then
            echo "${NAME}: response from ${_FROM} after ${ELAPSED_TIME} seconds: ${_BODY}"
            exit 0
        fi
    done < <("${GREP}" -E "${REPLY_PATTERN}" "${ERROR_FILE}")

    # Pause and try again
    "${SLEEP}" "${SLEEP_INTERVAL}"
done
