Browse Source

reorganize files

oauth
Wesley Kerfoot 3 years ago
parent
commit
9d823aa2c1
  1. 1
      src/tweetlog.nim
  2. 131
      src/tweetlogpkg/auth.nim
  3. 144
      src/tweetlogpkg/twitter.nim
  4. 2
      tweetlog.nimble

1
src/tweetlog.nim

@ -1,5 +1,6 @@
import tweetlogpkg/twitter, tweetlogpkg/server
import threadpool
import tweetlogpkg/auth
import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options
import timezones, times

131
src/tweetlogpkg/auth.nim

@ -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"]))

144
src/tweetlogpkg/twitter.nim

@ -1,6 +1,6 @@
import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options, sugar, timezones, times, types
import tables, algorithm, base64, math, options
import nimcrypto
import httpClient, uri, json, os, strformat, sequtils, strutils, options, sugar, types
import tables, options, times
import auth
from nimcrypto.sysrand import randomBytes
from xmltree import escape
@ -8,47 +8,10 @@ from xmltree import escape
proc parseTwitterTS(ts : string) : DateTime =
ts.parse("ddd MMM dd hh:mm:ss YYYY")
# echo "Sun Feb 16 18:19:17 +0000 2020".parseTwitterTS.repr
proc buildAuthHeader() : string =
let consumerKey = "TWITTER_CONSUMER_KEY".getEnv
let secret = "TWITTER_CONSUMER_SECRET".getEnv
"Basic " & (consumerKey.encodeUrl & ":" & secret.encodeUrl).encode
proc getToken*() : 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
proc tweetClient() : HttpClient =
var client = newHttpClient()
client.headers = newHttpHeaders(
{
"Authorization" : getToken()
}
)
client
proc listTweets*(user : string) : JsonNode =
# Lists tweets from a given user
# XXX use Tweet type
let client = tweetClient()
let client = tweetClient(getBearerToken())
let userIdReq = fmt"/2/users/by?usernames={user}"
var url = fmt"https://api.twitter.com{userIdReq}"
@ -60,7 +23,7 @@ proc listTweets*(user : string) : JsonNode =
proc getTweetConvo*(tweetID : string) : JsonNode =
# Gets the conversation info for a given tweet
let client = tweetClient()
let client = tweetClient(getBearerToken())
let userIdReq = fmt"/2/tweets?ids={tweetID}&tweet.fields=conversation_id,author_id"
var url = fmt"https://api.twitter.com{userIdReq}"
@ -71,7 +34,7 @@ proc getTweetConvo*(tweetID : string) : JsonNode =
proc getTweet*(tweetID : string) : string =
# Grabs a single tweet
# XXX use Tweet type
let client = tweetClient()
let client = tweetClient(getBearerToken())
let reqTarget = fmt"/1.1/statuses/show.json?id={tweetID}&tweet_mode=extended"
let url = fmt"https://api.twitter.com{reqTarget}"
@ -79,14 +42,14 @@ proc getTweet*(tweetID : string) : string =
proc getHome*(count: int) : string =
# Gets your home timeline
let client = tweetClient()
let client = tweetClient(getBearerToken())
let reqTarget = fmt"/1.1/statuses/user_timeline.json?count={count}&trim_user=1&exclude_replies=1"
let url = fmt"https://api.twitter.com{reqTarget}"
client.request(url, httpMethod = HttpGet).body
iterator getThread*(tweetStart : string) : Tweet =
let client = tweetClient()
let client = tweetClient(getBearerToken())
var reqTarget = fmt"/2/tweets/search/recent?query=conversation_id:{tweetStart}&tweet.fields=in_reply_to_user_id,author_id,created_at,conversation_id"
var url = fmt"https://api.twitter.com{reqTarget}"
@ -140,94 +103,3 @@ proc renderThread*(tweetID : string) : Option[seq[string]] =
if thread.len == 0:
return none(seq[string])
some(thread)
# 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 client = tweetClient()
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
headers["Authorization"] = @[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"]))

2
tweetlog.nimble

@ -12,6 +12,8 @@ bin = @["tweetlog"]
requires "nim >= 1.0"
requires "https://github.com/dom96/jester"
requires "https://github.com/pragmagic/karax"
requires "https://github.com/GULPF/timezones"
requires "https://github.com/cheatfate/nimcrypto"
task bookmark, "Builds the minified bookmarklet":
"echo -n 'javascript:' > ./bookmarklet.min.js".exec

Loading…
Cancel
Save