From 50151fcb49a22b53cbe4730f906e15453e5b0280 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Sat, 27 Feb 2021 23:22:03 -0500 Subject: [PATCH] wip new api and 3-legged oauth --- src/tweetlog.nim | 12 ++-- src/tweetlogpkg/server.nim | 4 ++ src/tweetlogpkg/twitter.nim | 116 +++++++++++++++++++++++++++++++++--- 3 files changed, 119 insertions(+), 13 deletions(-) diff --git a/src/tweetlog.nim b/src/tweetlog.nim index 4099d1b..d76e1b3 100644 --- a/src/tweetlog.nim +++ b/src/tweetlog.nim @@ -8,10 +8,14 @@ from xmltree import escape when isMainModule: echo "Running" - for tweet in "1355971359168466945".getThread: - echo "" - echo tweet.text - echo "" + echo "https://api.twitter.com/oauth/request_token".requestToken("POST", "") + + #echo "weskerfoot".listTweets + #echo 10.getHome + #for tweet in "1355971359168466945".getThread: + #echo "" + #echo tweet.text + #echo "" #for tweet in "strivev4".listTweets2(){"data"}: #echo tweet diff --git a/src/tweetlogpkg/server.nim b/src/tweetlogpkg/server.nim index 29fb1d6..5749c8b 100644 --- a/src/tweetlogpkg/server.nim +++ b/src/tweetlogpkg/server.nim @@ -170,6 +170,10 @@ router twitblog: let threads = toSeq(threadIDs(author)) resp author.listThreads(threads) + get "/tweetlog/auth": + echo request + resp "" + # Entry points proc startServer* = diff --git a/src/tweetlogpkg/twitter.nim b/src/tweetlogpkg/twitter.nim index be65184..666e81e 100644 --- a/src/tweetlogpkg/twitter.nim +++ b/src/tweetlogpkg/twitter.nim @@ -1,7 +1,8 @@ -import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options -import timezones, times -import types +import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options, sugar, timezones, times, types +import tables, algorithm, base64, math, options +import nimcrypto +from nimcrypto.sysrand import randomBytes from xmltree import escape proc parseTwitterTS(ts : string) : DateTime = @@ -44,7 +45,9 @@ proc tweetClient() : HttpClient = ) client -proc listTweets2*(user : string) : JsonNode = +proc listTweets*(user : string) : JsonNode = + # Lists tweets from a given user + # XXX use Tweet type let client = tweetClient() let userIdReq = fmt"/2/users/by?usernames={user}" var url = fmt"https://api.twitter.com{userIdReq}" @@ -56,6 +59,7 @@ proc listTweets2*(user : string) : JsonNode = return client.request(url, httpMethod = HttpGet).body.parseJson proc getTweetConvo*(tweetID : string) : JsonNode = + # Gets the conversation info for a given tweet let client = tweetClient() let userIdReq = fmt"/2/tweets?ids={tweetID}&tweet.fields=conversation_id,author_id" var url = fmt"https://api.twitter.com{userIdReq}" @@ -64,16 +68,19 @@ proc getTweetConvo*(tweetID : string) : JsonNode = tweetInfo -proc listTweets*(user : string) : JsonNode = +proc getTweet*(tweetID : string) : string = + # Grabs a single tweet + # XXX use Tweet type let client = tweetClient() - let reqTarget = fmt"/1.1/statuses/user_timeline.json?count=100&screen_name={user}" + let reqTarget = fmt"/1.1/statuses/show.json?id={tweetID}&tweet_mode=extended" let url = fmt"https://api.twitter.com{reqTarget}" - client.request(url, httpMethod = HttpGet).body.parseJson + client.request(url, httpMethod = HttpGet).body -proc getTweet*(tweetID : string) : string = +proc getHome*(count: int) : string = + # Gets your home timeline let client = tweetClient() - let reqTarget = fmt"/1.1/statuses/show.json?id={tweetID}&tweet_mode=extended" + 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 @@ -133,3 +140,94 @@ 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"]))