4 changed files with 142 additions and 136 deletions
@ -0,0 +1,131 @@ |
|||
import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options, sugar, times, types |
|||
import tables, algorithm, base64, math, options |
|||
import nimcrypto |
|||
|
|||
proc tweetClient*(token : string) : HttpClient = |
|||
var client = newHttpClient() |
|||
client.headers = newHttpHeaders( |
|||
{ |
|||
"Authorization" : token |
|||
} |
|||
) |
|||
client |
|||
|
|||
# client credentials flow |
|||
proc buildAuthHeader() : string = |
|||
let consumerKey = "TWITTER_CONSUMER_KEY".getEnv |
|||
let secret = "TWITTER_CONSUMER_SECRET".getEnv |
|||
"Basic " & (consumerKey.encodeUrl & ":" & secret.encodeUrl).encode |
|||
|
|||
proc getBearerToken*() : string = |
|||
var client = newHttpClient() |
|||
client.headers = newHttpHeaders( |
|||
{ |
|||
"Content-Type" : "application/x-www-form-urlencoded;charset=UTF-8", |
|||
"Authorization" : buildAuthHeader() |
|||
} |
|||
) |
|||
|
|||
let body = "grant_type=client_credentials" |
|||
|
|||
let response = client.request("https://api.twitter.com/oauth2/token", |
|||
httpMethod = HttpPost, |
|||
body = body).body.parseJson |
|||
|
|||
let responseType = response["token_type"].getStr |
|||
|
|||
assert(responseType == "bearer") |
|||
|
|||
"Bearer " & response["access_token"].getStr |
|||
|
|||
# 3-legged OAuth stuff |
|||
type Params = Table[string, string] |
|||
type OAuthToken = tuple[token: string, token_secret: string] |
|||
|
|||
proc generateNonce() : string = |
|||
let alphabet = map(toSeq(65..90).concat( |
|||
toSeq(97..122)).concat( |
|||
toSeq(49..57)), (c) => char(c)) |
|||
|
|||
var randBytes : array[50, uint8] |
|||
discard randomBytes(randBytes) |
|||
return map(randBytes, (c) => alphabet[(c.int %% alphabet.len)]).join |
|||
|
|||
proc constructEncodedString(params : Params, sep : string, include_quotes : bool) : string = |
|||
var encodedPairs : seq[string] = @[] |
|||
var keyPairs : seq[tuple[key: string, value: string]] = toSeq(params.pairs) |
|||
|
|||
keyPairs.sort((a, b) => cmp(a.key.encodeUrl, b.key.encodeUrl)) |
|||
|
|||
for pair in keyPairs: |
|||
if include_quotes: |
|||
encodedPairs &= pair[0].encodeUrl & "=" & "\"" & pair[1].encodeUrl & "\"" |
|||
else: |
|||
encodedPairs &= pair[0].encodeUrl & "=" & pair[1].encodeUrl |
|||
|
|||
encodedPairs.join(sep) |
|||
|
|||
proc constructParameterString(params : Params) : string = |
|||
params.constructEncodedString("&", include_quotes=false) |
|||
|
|||
proc constructHeaderString(params : Params) : string = |
|||
"OAuth " & params.constructEncodedString(", ", include_quotes=true) |
|||
|
|||
proc sign(reqMethod : string, |
|||
paramString : string, |
|||
baseUrl : string, |
|||
accessToken : string = "") : string = |
|||
let sigBaseString : string = reqMethod.toUpperAscii & "&" & baseUrl.encodeUrl & "&" & paramString.encodeUrl |
|||
|
|||
let signingKey : string = getEnv("TWITTER_CONSUMER_SECRET").encodeUrl & "&" & accessToken |
|||
sha256.hmac(signingKey, sigBaseString).data.encode |
|||
|
|||
proc requestToken*(requestUrl : string, requestMethod : string, requestBody : string) : Option[OAuthToken] = |
|||
# Obtain a request token for OAuth |
|||
# as well as a request token secret |
|||
# these are used to authenticate a specific user |
|||
let callback = getEnv("TWITTER_OAUTH_CALLBACK") |
|||
let consumerKey = getEnv("TWITTER_CONSUMER_KEY") |
|||
|
|||
var headers = newHttpHeaders([]) |
|||
|
|||
let oauth_nonce = generateNonce() |
|||
|
|||
# The twitter documentation uses SHA1, but this works and is future-proof |
|||
let oauth_signature_method = "HMAC-SHA256" |
|||
let oauth_timestamp : string = $trunc(epochTime()).uint64 |
|||
|
|||
var params : Params = { |
|||
"oauth_nonce" : oauth_nonce, |
|||
"oauth_signature_method" : oauth_signature_method, |
|||
"oauth_callback" : callback, |
|||
"oauth_timestamp" : oauth_timestamp, |
|||
"oauth_consumer_key" : consumerKey, |
|||
"oauth_version" : "1.0" |
|||
}.toTable |
|||
|
|||
let paramString = params.constructParameterString |
|||
|
|||
let signature = sign(requestMethod, paramString, requestUrl) |
|||
|
|||
params["oauth_signature"] = signature |
|||
|
|||
let client = tweetClient(params.constructHeaderString) |
|||
|
|||
let resp = client.request(requestUrl, httpMethod = HttpPost, headers = headers, body = requestBody) |
|||
|
|||
if resp.status != "200 OK": |
|||
echo resp.body |
|||
return none(OAuthToken) |
|||
|
|||
let keyPairs : Table[string, string] = toTable( |
|||
map(resp.body.split("&"), |
|||
proc(pair : string) : tuple[a: string, b: string] = |
|||
let split = pair.split("=") |
|||
(split[0], split[1]))) |
|||
|
|||
if not (keyPairs.hasKey("oauth_token") and keyPairs.hasKey("oauth_token_secret")): |
|||
return none(OAuthToken) |
|||
|
|||
some((token: keyPairs["oauth_token"], token_secret: keyPairs["oauth_token_secret"])) |
|||
|
Loading…
Reference in new issue