#!/bin/bash
# This file is in the public domain.
# Transmit a Challenger TAN by SMS using the Telesign Messaging API.
#
# Usage:
#   echo "MESSAGE" | $0 +41791234567
#
# Note that unlike challenger-send-sms(1), the argument is the bare phone
# number: this script is invoked by the dispatcher, which has already extracted
# and syntax-checked it.
#
# Environment:
#   TELESIGN_AUTH_TOKEN
#
# Exit codes (see challenger-send-sms-telesign(1)).  Success is 0 or the
# 200-210 band; every other value is a failure.  The success band is up at
# 200 and not at 1 because libgnunetutil reports a failed exec() of the
# helper as exit code 1, which must not be mistaken for a delivery.
# Telesign cannot distinguish "too short" from "too long" (both are
# 11000) and cannot detect a landline without its separate PhoneID product, so
# this helper never reports 11 or 12.
#
#    0 delivered    201 accepted
#   13 not mobile    14 invalid number    15 blocked
#   20 offline       21 expired           22 carrier
#   30 provider      31 rejected          32 rate limited     33 no verdict
#   40 config        41 authorization     42 balance
#   50 unclassified

set -eu

if [ -z "${TELESIGN_AUTH_TOKEN:-}" ]
then
    echo "TELESIGN_AUTH_TOKEN not set in environment" 1>&2
    exit 40
fi

if [ $# -ne 1 ]
then
    echo "Usage: $0 <phone_number>" 1>&2
    exit 40
fi

PHONE_NUMBER="$1"
MESSAGE=$(cat -)

API="https://rest-ww.telesign.com/v1/messaging"

# Total time we are willing to wait for a delivery confirmation.  A message
# still in flight when this expires is reported as accepted (1), not failed.
DEADLINE=$(( SECONDS + 5 ))

BODY=$(mktemp "${TMPDIR:-/tmp}/telesign-sms-XXXXXX")
trap 'rm -f "$BODY"' EXIT

# Map a Telesign status code onto our exit codes.  Telesign's documentation is
# explicit that callers should branch on this field rather than on the HTTP
# status, and it is present in the body of failed requests too -- which is why
# curl --fail must not be used here: it would discard exactly this field, and
# an invalid number (11000, HTTP 400) would be indistinguishable from any other
# error.  A return of 201 means "no verdict yet, keep polling".
classify ()
{
    case "$1" in
        200)
            echo 0 ;;
        # Accepted, in progress, or Telesign lost the delivery receipt.
        203 | 250 | 251 | 290 | 292 | 293 | 295)
            echo 201 ;;
        # SMS not supported by this phone, carrier, plan or user.
        222)
            echo 13 ;;
        # 11000 covers malformed, too short and too long alike.
        11000 | 11001 | 211 | 220 | 507 | 513)
            echo 14 ;;
        229 | 230 | 233 | 237 | 238 | 510 | 515 | 518 | 520 | 521 | 10032)
            echo 15 ;;
        # 511 is the *subscriber* running out of credit, not our account.
        210 | 512 | 514 | 511)
            echo 20 ;;
        221)
            echo 21 ;;
        207 | 502 | 503 | 504)
            echo 22 ;;
        500 | 505 | 506 | 599 | 10000)
            echo 30 ;;
        231 | 508 | 509 | 517 | 11003 | 11004 | 11009 | 11010 | 11011 | 11013 | 11016 | 11017 | 11027 | 11034 | 11035 | 12000 | 12009 | 12017)
            echo 31 ;;
        10019 | 10020)
            echo 32 ;;
        501 | 10002 | 10003 | 10004 | 10005 | 10006 | 10007 | 10008 | 10009 | 10010 | 10011 | 10014 | 10015 | 10022 | 10028 | 10031 | 10033 | 10035)
            echo 41 ;;
        10030)
            echo 42 ;;
        *)
            echo 50 ;;
    esac
}

HTTP=$(curl --silent --show-error \
            --output "$BODY" \
            --write-out '%{http_code}' \
            --url "$API" \
            --request POST \
            --header "Authorization: Basic $TELESIGN_AUTH_TOKEN" \
            --header "Content-Type: application/x-www-form-urlencoded" \
            --data account_lifecycle_event=transact \
            --data-urlencode "phone_number=$PHONE_NUMBER" \
            --data-urlencode "message=$MESSAGE" \
            --data message_type=OTP) || {
    echo "Telesign: could not be reached" 1>&2
    exit 30
}

STATUS_CODE=$(jq -r '.status.code // empty' < "$BODY" 2>/dev/null || true)

if [ -z "$STATUS_CODE" ]
then
    echo "Telesign: unexpected reply (HTTP $HTTP) sending to $PHONE_NUMBER" 1>&2
    cat "$BODY" 1>&2
    case "$HTTP" in
        401 | 403) exit 41 ;;
        402)       exit 42 ;;
        429)       exit 32 ;;
        4??)       exit 31 ;;
        *)         exit 30 ;;
    esac
fi

RESULT=$(classify "$STATUS_CODE")

if [ 0 -eq "$RESULT" ]
then
    echo "Telesign: delivered to $PHONE_NUMBER" 1>&2
    exit 0
fi

if [ 201 -ne "$RESULT" ]
then
    echo "Telesign: status $STATUS_CODE sending to $PHONE_NUMBER" 1>&2
    cat "$BODY" 1>&2
    exit "$RESULT"
fi

REFERENCE_ID=$(jq -r '.reference_id // empty' < "$BODY" 2>/dev/null || true)

if [ -z "$REFERENCE_ID" ]
then
    echo "Telesign: accepted message for $PHONE_NUMBER but returned no reference_id" 1>&2
    exit 33
fi

while [ "$SECONDS" -lt "$DEADLINE" ]
do
    sleep 1
    curl --silent --show-error \
         --output "$BODY" \
         --url "$API/$REFERENCE_ID" \
         --header "Authorization: Basic $TELESIGN_AUTH_TOKEN" || continue
    STATUS_CODE=$(jq -r '.status.code // empty' < "$BODY" 2>/dev/null || true)
    if [ -z "$STATUS_CODE" ]
    then
        continue
    fi
    RESULT=$(classify "$STATUS_CODE")
    if [ 201 -ne "$RESULT" ]
    then
        if [ 0 -eq "$RESULT" ]
        then
            echo "Telesign: delivered to $PHONE_NUMBER" 1>&2
        else
            echo "Telesign: status $STATUS_CODE for message to $PHONE_NUMBER" 1>&2
        fi
        exit "$RESULT"
    fi
done

echo "Telesign: accepted message for $PHONE_NUMBER, delivery not confirmed within the poll window" 1>&2
exit 201
