|
@ -1,7 +1,8 @@ |
|
|
import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options |
|
|
import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options, sugar, timezones, times, types |
|
|
import timezones, times |
|
|
import tables, algorithm, base64, math, options |
|
|
import types |
|
|
import nimcrypto |
|
|
|
|
|
|
|
|
|
|
|
from nimcrypto.sysrand import randomBytes |
|
|
from xmltree import escape |
|
|
from xmltree import escape |
|
|
|
|
|
|
|
|
proc parseTwitterTS(ts : string) : DateTime = |
|
|
proc parseTwitterTS(ts : string) : DateTime = |
|
@ -44,7 +45,9 @@ proc tweetClient() : HttpClient = |
|
|
) |
|
|
) |
|
|
client |
|
|
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 client = tweetClient() |
|
|
let userIdReq = fmt"/2/users/by?usernames={user}" |
|
|
let userIdReq = fmt"/2/users/by?usernames={user}" |
|
|
var url = fmt"https://api.twitter.com{userIdReq}" |
|
|
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 |
|
|
return client.request(url, httpMethod = HttpGet).body.parseJson |
|
|
|
|
|
|
|
|
proc getTweetConvo*(tweetID : string) : JsonNode = |
|
|
proc getTweetConvo*(tweetID : string) : JsonNode = |
|
|
|
|
|
# Gets the conversation info for a given tweet |
|
|
let client = tweetClient() |
|
|
let client = tweetClient() |
|
|
let userIdReq = fmt"/2/tweets?ids={tweetID}&tweet.fields=conversation_id,author_id" |
|
|
let userIdReq = fmt"/2/tweets?ids={tweetID}&tweet.fields=conversation_id,author_id" |
|
|
var url = fmt"https://api.twitter.com{userIdReq}" |
|
|
var url = fmt"https://api.twitter.com{userIdReq}" |
|
@ -64,16 +68,19 @@ proc getTweetConvo*(tweetID : string) : JsonNode = |
|
|
|
|
|
|
|
|
tweetInfo |
|
|
tweetInfo |
|
|
|
|
|
|
|
|
proc listTweets*(user : string) : JsonNode = |
|
|
proc getTweet*(tweetID : string) : string = |
|
|
|
|
|
# Grabs a single tweet |
|
|
|
|
|
# XXX use Tweet type |
|
|
let client = tweetClient() |
|
|
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}" |
|
|
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 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}" |
|
|
let url = fmt"https://api.twitter.com{reqTarget}" |
|
|
|
|
|
|
|
|
client.request(url, httpMethod = HttpGet).body |
|
|
client.request(url, httpMethod = HttpGet).body |
|
@ -133,3 +140,94 @@ proc renderThread*(tweetID : string) : Option[seq[string]] = |
|
|
if thread.len == 0: |
|
|
if thread.len == 0: |
|
|
return none(seq[string]) |
|
|
return none(seq[string]) |
|
|
some(thread) |
|
|
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"])) |
|
|