Browse Source

refactoring into a twitter client

oauth
Wesley Kerfoot 3 years ago
parent
commit
76496901e4
  1. 10
      src/tweetlog.nim
  2. 180
      src/tweetlogpkg/auth.nim
  3. 85
      src/tweetlogpkg/server.nim
  4. 105
      src/tweetlogpkg/twitter.nim
  5. 129
      src/tweetlogpkg/twitter_api.nim
  6. 10
      src/tweetlogpkg/types.nim
  7. 2
      tweetlog.nimble

10
src/tweetlog.nim

@ -1,16 +1,12 @@
import tweetlogpkg/twitter, tweetlogpkg/server import tweetlogpkg/twitter_api, tweetlogpkg/server
import threadpool import threadpool
import tweetlogpkg/auth import tweetlogpkg/auth
import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options
import timezones, times import timezones, times
from xmltree import escape from xmltree import escape
when isMainModule: when isMainModule:
echo "Running"
echo "https://api.twitter.com/oauth/request_token".requestToken("POST", "")
#echo "weskerfoot".listTweets #echo "weskerfoot".listTweets
#echo 10.getHome #echo 10.getHome
#for tweet in "1355971359168466945".getThread: #for tweet in "1355971359168466945".getThread:
@ -21,5 +17,5 @@ when isMainModule:
#for tweet in "strivev4".listTweets2(){"data"}: #for tweet in "strivev4".listTweets2(){"data"}:
#echo tweet #echo tweet
#echo tweet{"id"}.getStr.getTweetConvo #echo tweet{"id"}.getStr.getTweetConvo
#spawn handleRenders() spawn handleRenders()
#startServer() startServer()

180
src/tweetlogpkg/auth.nim

@ -1,9 +1,23 @@
import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options, sugar, times, types import httpClient, base64, uri, json, os, strformat, sequtils, strutils, options, sugar, times, types
import tables, algorithm, base64, math, options import tables, algorithm, base64, math, options
import nimcrypto import nimcrypto
import threadpool
proc realEncodeUrl*(s: string): string =
## Exclude A..Z a..z 0..9 - . _ ~
## See https://dev.twitter.com/oauth/overview/percent-encoding-parameters
result = newStringOfCap(s.len + s.len shr 2) # assume 12% non-alnum-chars
for i in 0..s.len-1:
case s[i]
of 'a'..'z', 'A'..'Z', '0'..'9', '_', '-', '.', '~':
add(result, s[i])
else:
add(result, '%')
add(result, toHex(ord(s[i]), 2))
proc tweetClient*(token : string) : HttpClient = proc tweetClient*(token : string) : HttpClient =
var client = newHttpClient() var client = newHttpClient()
client.headers = newHttpHeaders( client.headers = newHttpHeaders(
{ {
"Authorization" : token "Authorization" : token
@ -11,41 +25,19 @@ proc tweetClient*(token : string) : HttpClient =
) )
client 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 # 3-legged OAuth stuff
type Params = Table[string, string]
type OAuthToken = tuple[token: string, token_secret: string]
proc generateNonce() : string = proc parseQueryString(qstr : string) : Table[string, string] =
toTable(
map(qstr.split("&"),
proc(pair : string) : tuple[a: string, b: string] =
let split = pair.split("=")
(split[0], split[1])))
proc generateNonce() : string {.gcsafe.} =
let alphabet = map(toSeq(65..90).concat( let alphabet = map(toSeq(65..90).concat(
toSeq(97..122)).concat( toSeq(97..122)).concat(
toSeq(49..57)), (c) => char(c)) toSeq(49..57)), (c) {.gcsafe.} => char(c))
var randBytes : array[50, uint8] var randBytes : array[50, uint8]
discard randomBytes(randBytes) discard randomBytes(randBytes)
@ -55,77 +47,131 @@ proc constructEncodedString(params : Params, sep : string, include_quotes : bool
var encodedPairs : seq[string] = @[] var encodedPairs : seq[string] = @[]
var keyPairs : seq[tuple[key: string, value: string]] = toSeq(params.pairs) var keyPairs : seq[tuple[key: string, value: string]] = toSeq(params.pairs)
keyPairs.sort((a, b) => cmp(a.key.encodeUrl, b.key.encodeUrl)) keyPairs.sort((a, b) => cmp(a.key.realEncodeUrl, b.key.realEncodeUrl))
for pair in keyPairs: for pair in keyPairs:
if include_quotes: if include_quotes:
encodedPairs &= pair[0].encodeUrl & "=" & "\"" & pair[1].encodeUrl & "\"" encodedPairs &= pair[0] & "=" & "\"" & pair[1].realEncodeUrl & "\""
else: else:
encodedPairs &= pair[0].encodeUrl & "=" & pair[1].encodeUrl encodedPairs &= pair[0] & "=" & pair[1].realEncodeUrl
encodedPairs.join(sep) encodedPairs.join(sep)
proc constructParameterString(params : Params) : string = proc constructParameterString(params : Params) : string =
params.constructEncodedString("&", include_quotes=false) result = params.constructEncodedString("&", include_quotes=false)
echo fmt"parameter string = {result}"
proc constructHeaderString(params : Params) : string = proc constructHeaderString(params : Params) : string =
"OAuth " & params.constructEncodedString(", ", include_quotes=true) "OAuth " & params.constructEncodedString(", ", include_quotes=true)
proc sign(reqMethod : string, proc sign(reqMethod : string,
paramString : string, paramString : string,
baseUrl : string, baseUrl : string) : string =
accessToken : string = "") : string =
let sigBaseString : string = reqMethod.toUpperAscii & "&" & baseUrl.encodeUrl & "&" & paramString.encodeUrl
let signingKey : string = getEnv("TWITTER_CONSUMER_SECRET").encodeUrl & "&" & accessToken let sigBaseString : string = reqMethod.toUpperAscii & "&" & baseUrl.realEncodeUrl & "&" & paramString.realEncodeUrl
sha256.hmac(signingKey, sigBaseString).data.encode var signingKey : string
proc requestToken*(requestUrl : string, requestMethod : string, requestBody : string) : Option[OAuthToken] = signingKey = getEnv("TWITTER_CONSUMER_SECRET").realEncodeUrl & "&"
# 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([]) result = sha1.hmac(signingKey, sigBaseString).data.encode
proc signRequest*(requestUrl : string,
requestMethod : string) : Params =
# Return params along with signature that signs request for API request
let oauth_consumer_key = getEnv("TWITTER_CONSUMER_KEY")
let oauth_nonce = generateNonce() let oauth_nonce = generateNonce()
result["oauth_callback"] = getEnv("TWITTER_OAUTH_CALLBACK")
# The twitter documentation uses SHA1, but this works and is future-proof # The twitter documentation uses SHA1, but this works and is future-proof
let oauth_signature_method = "HMAC-SHA256" let oauth_signature_method = "HMAC-SHA1".realEncodeUrl
let oauth_timestamp : string = $trunc(epochTime()).uint64 let oauth_timestamp : string = $trunc(epochTime()).uint64
var params : Params = { result["oauth_nonce"] = oauth_nonce
"oauth_nonce" : oauth_nonce, result["oauth_signature_method"] = oauth_signature_method
"oauth_signature_method" : oauth_signature_method, result["oauth_timestamp"] = oauth_timestamp
"oauth_callback" : callback, result["oauth_consumer_key"] = oauth_consumer_key
"oauth_timestamp" : oauth_timestamp, result["oauth_version"] = "1.0"
"oauth_consumer_key" : consumerKey,
"oauth_version" : "1.0"
}.toTable
let paramString = params.constructParameterString let paramString = result.constructParameterString
let signature = sign(requestMethod, paramString, requestUrl) result["oauth_signature"] = sign(requestMethod, paramString, requestUrl)
params["oauth_signature"] = signature proc getAuthRequestSigned(requestUrl : string) : Params =
signRequest(requestUrl, "POST")
let client = tweetClient(params.constructHeaderString) proc requestToken*(requestUrl : string) : Option[OAuthToken] =
var headers = newHttpHeaders([])
let resp = client.request(requestUrl, httpMethod = HttpPost, headers = headers, body = requestBody) let params = getAuthRequestSigned(requestUrl)
let client = tweetClient(params.constructHeaderString)
let resp = client.request(requestUrl, httpMethod = HttpPost, headers = headers, body = "")
if resp.status != "200 OK": if resp.status != "200 OK":
echo resp.body echo resp.body
return none(OAuthToken) return none(OAuthToken)
let keyPairs : Table[string, string] = toTable( let keyPairs = resp.body.parseQueryString
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")): if not (keyPairs.hasKey("oauth_token") and keyPairs.hasKey("oauth_token_secret")):
return none(OAuthToken) return none(OAuthToken)
some((token: keyPairs["oauth_token"], token_secret: keyPairs["oauth_token_secret"])) some((oauth_token: keyPairs["oauth_token"], oauth_token_secret: keyPairs["oauth_token_secret"]))
proc getTokenRedirect*() : string =
let req = "https://api.twitter.com/oauth/request_token".requestToken
fmt"https://api.twitter.com/oauth/authenticate?oauth_token={req.get.oauth_token}"
proc getAccessToken*(oauth_token : string, oauth_verifier : string) : Option[AccessToken] =
var params : Params
let client = tweetClient(params.constructHeaderString)
let requestUrl = fmt"https://api.twitter.com/oauth/access_token?oauth_token={oauth_token}&oauth_verifier={oauth_verifier}"
let resp = client.request(requestUrl, httpMethod = HttpPost)
if resp.status != "200 OK":
return none(AccessToken)
let keyPairs = resp.body.parseQueryString
some((access_token: keyPairs["oauth_token"],
access_token_secret: keyPairs["oauth_token_secret"],
screen_name: keyPairs["screen_name"],
user_id: keyPairs["user_id"]))
import jwt
proc generateJWT*(token : AccessToken) : string =
# take an access token and return an encrypted JWT
let secret = getEnv("JWT_SECRET")
var encoded = toJWT(%*{
"header" : {
"alg" : "HS256",
"typ" : "JWT"
},
"claims" : {
"access_token" : token.access_token,
"access_token_secret" : token.access_token_secret,
"screen_name" : token.screen_name,
"user_id" : token.user_id
}
})
encoded.sign(secret)
$encoded
proc verify*(token: string): bool =
let secret = getEnv("JWT_SECRET")
try:
let jwtToken = token.toJWT()
result = jwtToken.verify(secret, HS256)
except InvalidToken:
result = false
proc decode*(token: string): Option[AccessToken] =
if not token.verify:
return none(AccessToken)
let claims = token.toJWT().claims
some((access_token: claims["access_token"].node.str,
access_token_secret: claims["access_token_secret"].node.str,
screen_name: claims["screen_name"].node.str,
user_id: claims["user_id"].node.str))

85
src/tweetlogpkg/server.nim

@ -1,7 +1,8 @@
import strutils, options, sugar, sequtils, asyncdispatch, threadpool, db_sqlite, json, strformat, uri, strscans, times import strutils, options, sugar, sequtils, asyncdispatch, threadpool, db_sqlite, json, strformat, uri, strscans, times
import twitter import twitter_api
import templates import templates
import jester import jester
import auth, types
type Author = object type Author = object
name: string name: string
@ -10,6 +11,7 @@ type Author = object
type ThreadRequest = object type ThreadRequest = object
tweetID: string tweetID: string
author: Author author: Author
token: AccessToken
type TwitterThread = ref object of RootObj type TwitterThread = ref object of RootObj
tweetID: string tweetID: string
@ -20,7 +22,7 @@ type TwitterThread = ref object of RootObj
# DateTime format string in ISO8601 format # DateTime format string in ISO8601 format
const dateFmt = "YYYY-MM-dd'T'hh:mm:ss'Z'" const dateFmt = "YYYY-MM-dd'T'hh:mm:ss'Z'"
proc parseTweetUrl(url : string) : Option[ThreadRequest] = proc parseTweetUrl(url : string, token : AccessToken) : Option[ThreadRequest] =
let path = url.parseUri.path let path = url.parseUri.path
var author : string var author : string
var tweetID : int var tweetID : int
@ -28,7 +30,8 @@ proc parseTweetUrl(url : string) : Option[ThreadRequest] =
some( some(
ThreadRequest( ThreadRequest(
tweetID : $tweetID, tweetID : $tweetID,
author: Author(name: author) author: Author(name: author),
token: token
) )
) )
else: else:
@ -123,19 +126,78 @@ proc insertThread(thread : TwitterThread) =
# Routes # Routes
# If using the web app:
# go to login link, log in, get redirected
# jwt generated (of access token and other info) and stored in httponly cookie, sent along with req to api endpoints
# api decodes it using secret
#
# If using api:
# generate your own oauth token and oauth secret
# pass to api
# api generates jwt (of access token and other info) and it is stored client side wherever client wants (filesystem, etc)
# it is sent along with req to api endpoints
# api decodes it using secret
#
# In both cases, expire tokens after n hours
# When re-auth is needed, redirect the user for web app or return response code as appropriate (401 error) and let client refresh
# need a way to get refresh tokens
# should I only refresh when the underlying oauth access token expires?
proc decodeToken(cookies: Table[string, string]) : Option[AccessToken] =
# take cookies, get jwt if it exists, and try to decode it
# this can definitely fail
if cookies.hasKey("twitterjwt"):
let token = cookies["twitterjwt"]
return token.decode
else:
none(AccessToken)
router twitblog: router twitblog:
# TODO make me configurable
get "/tweetlog/auth":
let params = request.params
if not ("oauth_token" in params and "oauth_verifier" in params):
redirect getTokenRedirect()
else:
let oauth_token = params["oauth_token"]
let oauth_verifier = params["oauth_verifier"]
let access_tok = getAccessToken(oauth_token, oauth_verifier)
if access_tok.isSome:
echo "Setting cookie"
# XXX insecure for now
setCookie("twitterjwt", access_tok.get.generateJWT, domain="localhost", sameSite=Lax, path="/")
redirect("http://localhost:3030/")
#resp(200.HttpCode, $(%*{"jwt" : access_tok.get.generateJWT}), contentType="application/json")
else:
resp(500.HttpCode, $(%*{"error" : "Failed to create token"}), contentType="application/json")
get "/": get "/":
# Lists all authors # Lists all authors
let token = request.cookies.decodeToken
if token.isNone:
redirect "/tweetlog/auth"
let authors = allAuthors.toSeq let authors = allAuthors.toSeq
let title = "Authors" let title = "Authors"
resp authors.mainPage resp authors.mainPage
post "/thread": post "/thread":
let token = request.cookies.decodeToken
if token.isNone:
redirect "/tweetlog/auth"
let params = request.params let params = request.params
if not ("tweetURL" in params): if not ("tweetURL" in params):
resp "Invalid" resp "Invalid"
let threadURL = params["tweetURL"].parseTweetUrl let threadURL = params["tweetURL"].parseTweetUrl(token.get)
if threadURL.isSome: if threadURL.isSome:
redirect (fmt"/thread/{threadURL.get.author}/status/{threadURL.get.tweetID}") redirect (fmt"/thread/{threadURL.get.author}/status/{threadURL.get.tweetID}")
@ -156,10 +218,14 @@ router twitblog:
else: else:
# Send it off to the rendering thread for processing # Send it off to the rendering thread for processing
# Let them know to check back later # Let them know to check back later
let token = request.cookies.decodeToken
if token.isNone:
redirect "/tweetlog/auth"
chan.send( chan.send(
ThreadRequest( ThreadRequest(
tweetID: tweetID, tweetID: tweetID,
author: Author(name: author) author: Author(name: author),
token: token.get
) )
) )
resp checkBack() resp checkBack()
@ -171,7 +237,6 @@ router twitblog:
resp author.listThreads(threads) resp author.listThreads(threads)
get "/tweetlog/auth": get "/tweetlog/auth":
echo request
resp "" resp ""
# Entry points # Entry points
@ -179,7 +244,7 @@ router twitblog:
proc startServer* = proc startServer* =
createTweetTables() createTweetTables()
defer: db.close() defer: db.close()
let port = 8080.Port let port = 3030.Port
let settings = newSettings(port=port) let settings = newSettings(port=port)
var jester = initJester(twitblog, settings=settings) var jester = initJester(twitblog, settings=settings)
jester.serve() jester.serve()
@ -189,10 +254,14 @@ proc handleRenders* =
while true: while true:
let t : ThreadRequest = chan.recv() let t : ThreadRequest = chan.recv()
echo t
if threadExists(t.tweetID, t.author.name).isSome: if threadExists(t.tweetID, t.author.name).isSome:
continue continue
let tweets = t.tweetID.renderThread let tweets = t.tweetID.renderThread(t.token)
echo $tweets
if tweets.isSome: if tweets.isSome:
insertThread( insertThread(

105
src/tweetlogpkg/twitter.nim

@ -1,105 +0,0 @@
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
proc parseTwitterTS(ts : string) : DateTime =
ts.parse("ddd MMM dd hh:mm:ss YYYY")
proc listTweets*(user : string) : JsonNode =
# Lists tweets from a given user
# XXX use Tweet type
let client = tweetClient(getBearerToken())
let userIdReq = fmt"/2/users/by?usernames={user}"
var url = fmt"https://api.twitter.com{userIdReq}"
let userId = client.request(url, httpMethod = HttpGet).body.parseJson{"data"}[0]{"id"}.getStr
let tweetsReq = fmt"/2/users/{userId}/tweets"
url = fmt"https://api.twitter.com{tweetsReq}"
return client.request(url, httpMethod = HttpGet).body.parseJson
proc getTweetConvo*(tweetID : string) : JsonNode =
# Gets the conversation info for a given tweet
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}"
let tweetInfo = client.request(url, httpMethod = HttpGet).body.parseJson
tweetInfo
proc getTweet*(tweetID : string) : string =
# Grabs a single tweet
# XXX use Tweet type
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}"
client.request(url, httpMethod = HttpGet).body
proc getHome*(count: int) : string =
# Gets your home timeline
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(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}"
var currentPage : JsonNode
currentPage = client.request(url, httpMethod = HttpGet).body.parseJson
while true:
if currentPage{"meta", "result_count"}.getInt == 0:
break
for tweet in currentPage{"data"}:
yield Tweet(
id: tweet{"id"}.getStr,
in_reply: tweet{"in_reply_to_user_id"}.getStr,
author_id: tweet{"author_id"}.getStr,
text: tweet{"text"}.getStr,
created_at: tweet{"created_at"}.getStr,
conversation_id: tweet{"conversation_id"}.getStr
)
let paginationToken = currentPage{"meta"}{"next_token"}
if paginationToken == nil:
break
reqTarget = fmt"/2/tweets/search/recent?query=conversation_id:{tweetStart}&tweet.fields=in_reply_to_user_id,author_id,created_at,conversation_id&next_token={paginationToken.getStr}"
url = fmt"https://api.twitter.com{reqTarget}"
currentPage = client.request(url, httpMethod = HttpGet).body.parseJson
proc convertWords(tweet : Tweet) : string =
let words = tweet.text.split(" ")
var stripped : seq[string]
for chunk in words:
for word in chunk.splitLines:
if word.len > 3 and word[0..3] == "http":
let parsedUri = word.parseUri
let scheme = parsedUri.scheme
let hostname = parsedUri.hostname
let path = parsedUri.path
if (scheme.len > 0 and hostname.len > 0):
let url = xmltree.escape(fmt"{scheme}://{hostname}{path}")
stripped &= url
elif word.len > 0 and word[0] != '@':
stripped &= word
else:
continue
stripped.join(" ")
proc renderThread*(tweetID : string) : Option[seq[string]] =
let thread = toSeq(getThread(tweetID)).map(convertWords).map(capitalizeAscii)
if thread.len == 0:
return none(seq[string])
some(thread)

129
src/tweetlogpkg/twitter_api.nim

@ -0,0 +1,129 @@
import httpClient, uri, json, os, strformat, sequtils, strutils, options, sugar, types
import tables, options, times, twitter
import auth
from nimcrypto.sysrand import randomBytes
from xmltree import escape
proc parseTwitterTS(ts : string) : DateTime =
ts.parse("ddd MMM dd hh:mm:ss YYYY")
proc listTweets*(user : string, token : AccessToken) : JsonNode =
# Lists tweets from a given user
# XXX use Tweet type
let client = tweetClient("Bearer " & token.access_token)
let userIdReq = fmt"/2/users/by?usernames={user}"
var url = fmt"https://api.twitter.com{userIdReq}"
let userId = client.request(url, httpMethod = HttpGet).body.parseJson{"data"}[0]{"id"}.getStr
let tweetsReq = fmt"/2/users/{userId}/tweets"
url = fmt"https://api.twitter.com{tweetsReq}"
return client.request(url, httpMethod = HttpGet).body.parseJson
proc getTweetConvo*(tweetID : string, token : AccessToken) : JsonNode =
# Gets the conversation info for a given tweet
let client = tweetClient("Bearer " & token.access_token)
let userIdReq = fmt"/2/tweets?ids={tweetID}&tweet.fields=conversation_id,author_id"
var url = fmt"https://api.twitter.com{userIdReq}"
let tweetInfo = client.request(url, httpMethod = HttpGet).body.parseJson
tweetInfo
proc getTweet*(tweetID : string, token : AccessToken) : string =
# Grabs a single tweet
# XXX use Tweet type
let client = tweetClient("Bearer " & token.access_token)
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
proc getHome*(count: int, token : AccessToken) : string =
# Gets your home timeline
let client = tweetClient("Bearer " & token.access_token)
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, token : AccessToken) : Tweet =
var reqParams : Params # params for the actual API request
let consumerKey = "TWITTER_CONSUMER_KEY".getEnv
let secret = "TWITTER_CONSUMER_SECRET".getEnv
var consumerToken = newConsumerToken(consumerKey, secret)
var twitterAPI = newTwitterAPI(consumerToken, token.access_token, token.access_token_secret)
reqParams["status"] = "testing123"
reqParams["include_entities"] = "true"
echo fmt"tweetStart = {tweetStart}"
#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 = "https://api.twitter.com/1.1/statuses/update.json"
var currentPage : string
echo fmt"url = {url}"
# Simply get.
var resp = twitterAPI.get("account/verify_credentials.json")
echo resp.status
# Using proc corresponding twitter REST APIs.
resp = twitterAPI.statusesUpdate("testing 1 2 3")
echo parseJson(resp.body)
#while true:
#if currentPage{"meta", "result_count"}.getInt == 0:
#break
#for tweet in currentPage{"data"}:
#yield Tweet(
#id: tweet{"id"}.getStr,
#in_reply: tweet{"in_reply_to_user_id"}.getStr,
#author_id: tweet{"author_id"}.getStr,
#text: tweet{"text"}.getStr,
#created_at: tweet{"created_at"}.getStr,
#conversation_id: tweet{"conversation_id"}.getStr
#)
#let paginationToken = currentPage{"meta"}{"next_token"}
#if paginationToken == nil:
#break
#echo "Getting next page"
#reqTarget = fmt"/2/tweets/search/recent?query=conversation_id:{tweetStart}&tweet.fields=in_reply_to_user_id,author_id,created_at,conversation_id&next_token={paginationToken.getStr}"
#url = fmt"https://api.twitter.com{reqTarget}"
#currentPage = client.request(url, httpMethod = HttpGet).body.parseJson
proc convertWords(tweet : Tweet) : string =
let words = tweet.text.split(" ")
var stripped : seq[string]
for chunk in words:
for word in chunk.splitLines:
if word.len > 3 and word[0..3] == "http":
let parsedUri = word.parseUri
let scheme = parsedUri.scheme
let hostname = parsedUri.hostname
let path = parsedUri.path
if (scheme.len > 0 and hostname.len > 0):
let url = xmltree.escape(fmt"{scheme}://{hostname}{path}")
stripped &= url
elif word.len > 0 and word[0] != '@':
stripped &= word
else:
continue
stripped.join(" ")
proc renderThread*(tweetID : string, token : AccessToken) : Option[seq[string]] =
let thread = toSeq(getThread(tweetID, token)).map(convertWords).map(capitalizeAscii)
echo $thread
if thread.len == 0:
return none(seq[string])
some(thread)

10
src/tweetlogpkg/types.nim

@ -1,3 +1,13 @@
import tables
type Params* = Table[string, string]
type OAuthToken* = tuple[oauth_token: string, oauth_token_secret: string]
type AccessToken* = tuple[access_token : string,
access_token_secret: string,
screen_name: string,
user_id: string]
type type
Tweet* = ref object of RootObj Tweet* = ref object of RootObj
id*: string id*: string

2
tweetlog.nimble

@ -14,6 +14,8 @@ requires "https://github.com/dom96/jester"
requires "https://github.com/pragmagic/karax" requires "https://github.com/pragmagic/karax"
requires "https://github.com/GULPF/timezones" requires "https://github.com/GULPF/timezones"
requires "https://github.com/cheatfate/nimcrypto" requires "https://github.com/cheatfate/nimcrypto"
requires "https://github.com/yglukhov/nim-jwt"
requires "https://github.com/snus-kin/twitter.nim"
task bookmark, "Builds the minified bookmarklet": task bookmark, "Builds the minified bookmarklet":
"echo -n 'javascript:' > ./bookmarklet.min.js".exec "echo -n 'javascript:' > ./bookmarklet.min.js".exec

Loading…
Cancel
Save