Last active
August 9, 2017 19:12
-
-
Save jcttrll/28eb7817e07950acdd4b975ee0e453a8 to your computer and use it in GitHub Desktop.
Quick-'n'-dirty shell script to hit the Twitter REST APIs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash -e | |
set -o pipefail | |
fail() { | |
echo "$*" >&2 | |
return 1 | |
} | |
verify-utilities-available() { | |
local util | |
for util in "$@"; do | |
which "$util" &>/dev/null || | |
fail "ERROR: This script requires the $util utility, which was not found" | |
done | |
} | |
show-usage() { | |
echo "Usage: twitter.sh -?/--help" >&2 | |
echo " or: twitter.sh" >&2 | |
echo " -m,--http-method <method>" >&2 | |
echo " -u,--url <URL>" >&2 | |
echo " --consumer-key <key>" >&2 | |
echo " --consumer-secret <secret>" >&2 | |
echo " --access-token <oauth-token>" >&2 | |
echo " --access-token-secret <oauth-token-secret>" >&2 | |
echo " [-q,--query-param <key=value>]" >&2 | |
echo " [-b,--body-param <key=value>]" >&2 | |
} | |
parse-arg() { | |
local name="$1" | |
local -n _opts="$2" | |
shift 2 | |
if [ -n "${_opts[$name]}" ]; then | |
fail "ERROR: $1 can only be specified once" | |
fi | |
if [ $# -lt 2 ]; then | |
fail "ERROR: $1 requires an argument" | |
fi | |
if [ -z "$2" ]; then | |
fail "ERROR: Argument to $1 cannot be empty" | |
fi | |
_opts["$name"]="$2" | |
} | |
parse-url-param-arg() { | |
local -n _assoc="$1" | |
local key | |
local value | |
shift | |
if [ $# -lt 2 ]; then | |
fail "ERROR: $1 requires an argument" | |
fi | |
if [ -z "$2" ]; then | |
fail "ERROR: Argument to $1 cannot be empty" | |
fi | |
if [[ ! "$2" =~ .= ]]; then | |
fail "ERROR: Argument to $1 must be key=value pair; got: $2" | |
fi | |
key=$(url-encode "${2%%=*}") | |
value=$(url-encode "${2#*=}") | |
_assoc+=(["$key"]="$value") | |
} | |
require-param() { | |
local name="$1" | |
local value="$2" | |
if [ -z "$value" ]; then | |
fail "ERROR: $name required" | |
fi | |
} | |
url-encode() { | |
local raw="$1" | |
local encoded | |
encoded=$(while IFS= read -r -n 1 char; do | |
if [ -z "$char" ]; then | |
char=$'\n' | |
fi | |
case "$char" in | |
[0-9A-Za-z-._~]) | |
echo -n "$char" | |
;; | |
*) | |
echo -n "$char" | od -t x1 -A n -w4 | sed 's/ /%/g;s/./\U&/g' | |
;; | |
esac | |
done <<< "$raw" | sed ':a;N;s/\n//;ta') | |
echo "${encoded:0:-3}" # read loop always picks up extra \n at end | |
} | |
hex-encode() { | |
local value="$1" | |
echo -n "$value" | od -t x1 -A n -w${#value} | sed 's/ //g' | |
} | |
hmac-sha1() { | |
local key | |
key=$(hex-encode "$1") | |
local data="$2" | |
echo -n "$data" | openssl sha1 -binary -mac HMAC -macopt "hexkey:$key" | base64 | |
} | |
create-query-string() { | |
local -n _params="$1" | |
local key | |
for key in "${!_params[@]}"; do | |
echo -e "$key=${_params[$key]}" | |
done | sed ':a;N;s/\n/\&/;ta' | |
} | |
create-auth-header() { | |
local -n _opts="$1" | |
local -n _query_params="$2" | |
local -n _body_params="$3" | |
local consumer_secret | |
local token_secret | |
local signature_base | |
local signing_key | |
local signature | |
local header | |
local -n ref | |
local key | |
local -A oauth_headers=( | |
[oauth_signature_method]=HMAC-SHA1 | |
[oauth_version]=1.0 | |
[oauth_timestamp]=$(date +%s) | |
[oauth_nonce]=$(dd if=/dev/urandom bs=16 count=1 2>/dev/null | od -t x1 -A n -w16 | sed 's/ //g') | |
[oauth_consumer_key]="${_opts[oauth_consumer_key]}" | |
[oauth_token]="${_opts[oauth_token]}" | |
) | |
consumer_secret=$(url-encode "${_opts[oauth_consumer_secret]}") | |
token_secret=$(url-encode "${_opts[oauth_token_secret]}") | |
signing_key="$consumer_secret&$token_secret" | |
signature_base="${_opts[http_method]}&$(url-encode "${_opts[url]}")&" | |
signature_base+=$(url-encode $( | |
for ref in oauth_headers _query_params _body_params; do | |
for key in "${!ref[@]}"; do | |
echo "$key=${ref[$key]}" | |
done | |
done | sort -t = -k1,2 | sed ':a;N;s/\n/\&/;ta' | |
)) | |
signature=$(url-encode $(hmac-sha1 "$signing_key" "$signature_base")) | |
header="Authorization: OAuth " | |
for key in "${!oauth_headers[@]}"; do | |
header+="$key=\"${oauth_headers[$key]}\", " | |
done | |
header+="oauth_signature=\"$signature\"" | |
echo "$header" | |
} | |
request() { | |
local -A options | |
local -A query_params | |
local -A body_params | |
local auth_header | |
local query_string | |
local body_string | |
while [ $# -ne 0 ]; do | |
case "$1" in | |
-m|--http-method) | |
parse-arg http_method options "$@" | |
shift 2 | |
;; | |
-u|--url) | |
parse-arg url options "$@" | |
shift 2 | |
;; | |
--consumer-key) | |
parse-arg oauth_consumer_key options "$@" | |
shift 2 | |
;; | |
--consumer-secret) | |
parse-arg oauth_consumer_secret options "$@" | |
shift 2 | |
;; | |
--access-token) | |
parse-arg oauth_token options "$@" | |
shift 2 | |
;; | |
--access-token-secret) | |
parse-arg oauth_token_secret options "$@" | |
shift 2 | |
;; | |
-q|--query-param) | |
parse-url-param-arg query_params "$@" | |
shift 2 | |
;; | |
-b|--body-param) | |
parse-url-param-arg body_params "$@" | |
shift 2 | |
;; | |
-?|--help) | |
show-usage | |
return 0 | |
;; | |
*) | |
fail "ERROR: Unrecognized parameter $1" | |
;; | |
esac | |
done | |
require-param "-m,--http-method" "${options[http_method]}" | |
require-param "-u,--url" "${options[url]}" | |
require-param "--consumer-key" "${options[oauth_consumer_key]}" | |
require-param "--consumer-secret" "${options[oauth_consumer_secret]}" | |
require-param "--access-token" "${options[oauth_token]}" | |
require-param "--access-token-secret" "${options[oauth_token_secret]}" | |
if [[ "${options[url]}" =~ \? ]]; then | |
fail "ERROR: URL cannot contain query parameters" \ | |
"(use -q/--query-param); got: ${options[url]}" | |
fi | |
auth_header="$(create-auth-header options query_params body_params)" | |
query_string="$(create-query-string query_params)" | |
body_string="$(create-query-string body_params)" | |
curl -X "${options[http_method]}" --http1.1 -sS --fail --connect-timeout 10 --max-time 20 \ | |
-H "Accept-Encoding: identity;q=0.0" \ | |
-H "Content-Type: application/x-www-form-urlencoded" \ | |
-H "$auth_header" \ | |
--data-raw "$body_string" \ | |
"${options[url]}?$query_string" | | |
jq | |
} | |
if [ $# -eq 0 ]; then | |
show-usage | |
exit 1 | |
fi | |
verify-utilities-available dd od jq sed openssl base64 curl | |
request "$@" |
Author
jcttrll
commented
Aug 8, 2017
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment