api.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import httpclient, asyncdispatch, htmlparser, times
  2. import sequtils, strutils, json, xmltree, uri
  3. import types, parser, parserutils, formatters, search
  4. const
  5. lang = "en-US,en;q=0.9"
  6. auth = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"
  7. accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"
  8. jsonAccept = "application/json, text/javascript, */*; q=0.01"
  9. base = parseUri("https://twitter.com/")
  10. apiBase = parseUri("https://api.twitter.com/1.1/")
  11. timelineUrl = "i/profiles/show/$1/timeline/tweets"
  12. timelineSearchUrl = "i/search/timeline"
  13. timelineMediaUrl = "i/profiles/show/$1/media_timeline"
  14. profilePopupUrl = "i/profiles/popup"
  15. profileIntentUrl = "intent/user"
  16. tweetUrl = "status"
  17. videoUrl = "videos/tweet/config/$1.json"
  18. tokenUrl = "guest/activate.json"
  19. cardUrl = "i/cards/tfw/v1/$1"
  20. pollUrl = cardUrl & "?cardname=poll2choice_text_only&lang=en"
  21. var
  22. guestToken = ""
  23. tokenUses = 0
  24. tokenMaxUses = 230
  25. tokenUpdated: Time
  26. tokenLifetime = initDuration(minutes=20)
  27. macro genMediaGet(media: untyped; token=false) =
  28. let
  29. mediaName = capitalizeAscii($media)
  30. multi = ident("get" & mediaName & "s")
  31. convo = ident("getConversation" & mediaName & "s")
  32. single = ident("get" & mediaName)
  33. quote do:
  34. proc `multi`(thread: Thread | Timeline; agent: string; token="") {.async.} =
  35. if thread == nil: return
  36. var `media` = thread.tweets.filterIt(it.`media`.isSome)
  37. when `token`:
  38. var gToken = token
  39. if gToken.len == 0: gToken = await getGuestToken(agent)
  40. await all(`media`.mapIt(`single`(it, token, agent)))
  41. else:
  42. await all(`media`.mapIt(`single`(it, agent)))
  43. proc `convo`(convo: Conversation; agent: string) {.async.} =
  44. var futs: seq[Future[void]]
  45. when `token`:
  46. var token = await getGuestToken(agent)
  47. futs.add `single`(convo.tweet, agent, token)
  48. futs.add `multi`(convo.before, agent, token=token)
  49. futs.add `multi`(convo.after, agent, token=token)
  50. futs.add convo.replies.mapIt(`multi`(it, agent, token=token))
  51. else:
  52. futs.add `single`(convo.tweet, agent)
  53. futs.add `multi`(convo.before, agent)
  54. futs.add `multi`(convo.after, agent)
  55. futs.add convo.replies.mapIt(`multi`(it, agent))
  56. await all(futs)
  57. template newClient() {.dirty.} =
  58. var client = newAsyncHttpClient()
  59. defer: client.close()
  60. client.headers = headers
  61. proc fetchHtml(url: Uri; headers: HttpHeaders; jsonKey = ""): Future[XmlNode] {.async.} =
  62. newClient()
  63. var resp = ""
  64. try:
  65. resp = await client.getContent($url)
  66. except:
  67. return nil
  68. if jsonKey.len > 0:
  69. let json = parseJson(resp)[jsonKey].str
  70. return parseHtml(json)
  71. else:
  72. return parseHtml(resp)
  73. proc fetchJson(url: Uri; headers: HttpHeaders): Future[JsonNode] {.async.} =
  74. newClient()
  75. var resp = ""
  76. try:
  77. resp = await client.getContent($url)
  78. result = parseJson(resp)
  79. except:
  80. return nil
  81. proc getGuestToken(agent: string; force=false): Future[string] {.async.} =
  82. if getTime() - tokenUpdated < tokenLifetime and
  83. not force and tokenUses < tokenMaxUses:
  84. return guestToken
  85. tokenUpdated = getTime()
  86. tokenUses = 0
  87. let headers = newHttpHeaders({
  88. "Accept": jsonAccept,
  89. "Referer": $base,
  90. "User-Agent": agent,
  91. "Authorization": auth
  92. })
  93. newClient()
  94. let
  95. url = apiBase / tokenUrl
  96. json = parseJson(await client.postContent($url))
  97. result = json["guest_token"].to(string)
  98. guestToken = result
  99. proc getVideoFetch*(tweet: Tweet; agent, token: string) {.async.} =
  100. if tweet.video.isNone(): return
  101. let headers = newHttpHeaders({
  102. "Accept": jsonAccept,
  103. "Referer": $(base / getLink(tweet)),
  104. "User-Agent": agent,
  105. "Authorization": auth,
  106. "x-guest-token": token
  107. })
  108. let url = apiBase / (videoUrl % tweet.id)
  109. let json = await fetchJson(url, headers)
  110. if json == nil:
  111. if getTime() - tokenUpdated > initDuration(seconds=1):
  112. tokenUpdated = getTime()
  113. discard await getGuestToken(agent, force=true)
  114. await getVideoFetch(tweet, agent, guestToken)
  115. return
  116. if tweet.card.isNone:
  117. tweet.video = some(parseVideo(json, tweet.id))
  118. else:
  119. get(tweet.card).video = some(parseVideo(json, tweet.id))
  120. tweet.video = none(Video)
  121. tokenUses.inc
  122. proc getVideoVar*(tweet: Tweet): var Option[Video] =
  123. if tweet.card.isSome():
  124. return get(tweet.card).video
  125. else:
  126. return tweet.video
  127. proc getVideo*(tweet: Tweet; agent, token: string; force=false) {.async.} =
  128. withDb:
  129. try:
  130. getVideoVar(tweet) = some(Video.getOne("videoId = ?", tweet.id))
  131. except KeyError:
  132. await getVideoFetch(tweet, agent, token)
  133. var video = getVideoVar(tweet)
  134. if video.isSome():
  135. get(video).insert()
  136. proc getPoll*(tweet: Tweet; agent: string) {.async.} =
  137. if tweet.poll.isNone(): return
  138. let headers = newHttpHeaders({
  139. "Accept": accept,
  140. "Referer": $(base / getLink(tweet)),
  141. "User-Agent": agent,
  142. "Authority": "twitter.com",
  143. "Accept-Language": lang,
  144. })
  145. let url = base / (pollUrl % tweet.id)
  146. let html = await fetchHtml(url, headers)
  147. if html == nil: return
  148. tweet.poll = some(parsePoll(html))
  149. proc getCard*(tweet: Tweet; agent: string) {.async.} =
  150. if tweet.card.isNone(): return
  151. let headers = newHttpHeaders({
  152. "Accept": accept,
  153. "Referer": $(base / getLink(tweet)),
  154. "User-Agent": agent,
  155. "Authority": "twitter.com",
  156. "Accept-Language": lang,
  157. })
  158. let query = get(tweet.card).query.replace("sensitive=true", "sensitive=false")
  159. let html = await fetchHtml(base / query, headers)
  160. if html == nil: return
  161. parseCard(get(tweet.card), html)
  162. genMediaGet(video, token=true)
  163. genMediaGet(poll)
  164. genMediaGet(card)
  165. proc getPhotoRail*(username, agent: string): Future[seq[GalleryPhoto]] {.async.} =
  166. let headers = newHttpHeaders({
  167. "Accept": jsonAccept,
  168. "Referer": $(base / username),
  169. "User-Agent": agent,
  170. "X-Requested-With": "XMLHttpRequest"
  171. })
  172. let params = {
  173. "for_photo_rail": "true",
  174. "oldest_unread_id": "0"
  175. }
  176. let url = base / (timelineMediaUrl % username) ? params
  177. let html = await fetchHtml(url, headers, jsonKey="items_html")
  178. result = parsePhotoRail(html)
  179. proc getProfileFallback(username: string; headers: HttpHeaders): Future[Profile] {.async.} =
  180. let url = base / profileIntentUrl ? {"screen_name": username}
  181. let html = await fetchHtml(url, headers)
  182. if html == nil: return Profile()
  183. result = parseIntentProfile(html)
  184. proc getProfile*(username, agent: string): Future[Profile] {.async.} =
  185. let headers = newHttpHeaders({
  186. "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9",
  187. "Referer": $(base / username),
  188. "User-Agent": agent,
  189. "X-Twitter-Active-User": "yes",
  190. "X-Requested-With": "XMLHttpRequest",
  191. "Accept-Language": lang
  192. })
  193. let
  194. params = {
  195. "screen_name": username,
  196. "wants_hovercard": "true",
  197. "_": $(epochTime().int)
  198. }
  199. url = base / profilePopupUrl ? params
  200. html = await fetchHtml(url, headers, jsonKey="html")
  201. if html == nil: return Profile()
  202. if html.select(".ProfileCard-sensitiveWarningContainer") != nil:
  203. return await getProfileFallback(username, headers)
  204. result = parsePopupProfile(html)
  205. proc getTweet*(username, id, agent: string): Future[Conversation] {.async.} =
  206. let headers = newHttpHeaders({
  207. "Accept": jsonAccept,
  208. "Referer": $base,
  209. "User-Agent": agent,
  210. "X-Twitter-Active-User": "yes",
  211. "X-Requested-With": "XMLHttpRequest",
  212. "Accept-Language": lang,
  213. "pragma": "no-cache",
  214. "x-previous-page-name": "profile"
  215. })
  216. let
  217. url = base / username / tweetUrl / id
  218. html = await fetchHtml(url, headers)
  219. if html == nil: return
  220. result = parseConversation(html)
  221. let
  222. vidsFut = getConversationVideos(result, agent)
  223. pollFut = getConversationPolls(result, agent)
  224. cardFut = getConversationCards(result, agent)
  225. await all(vidsFut, pollFut, cardFut)
  226. proc finishTimeline(json: JsonNode; query: Option[Query]; after, agent: string): Future[Timeline] {.async.} =
  227. if json == nil: return Timeline()
  228. result = Timeline(
  229. hasMore: json["has_more_items"].to(bool),
  230. maxId: json.getOrDefault("max_position").getStr(""),
  231. minId: json.getOrDefault("min_position").getStr("").cleanPos(),
  232. query: query,
  233. beginning: after.len == 0
  234. )
  235. if json["new_latent_count"].to(int) == 0: return
  236. if not json.hasKey("items_html"): return
  237. let
  238. html = parseHtml(json["items_html"].to(string))
  239. thread = parseThread(html)
  240. vidsFut = getVideos(thread, agent)
  241. pollFut = getPolls(thread, agent)
  242. cardFut = getCards(thread, agent)
  243. await all(vidsFut, pollFut, cardFut)
  244. result.tweets = thread.tweets
  245. proc getTimeline*(username, after, agent: string): Future[Timeline] {.async.} =
  246. let headers = newHttpHeaders({
  247. "Accept": jsonAccept,
  248. "Referer": $(base / username),
  249. "User-Agent": agent,
  250. "X-Twitter-Active-User": "yes",
  251. "X-Requested-With": "XMLHttpRequest",
  252. "Accept-Language": lang
  253. })
  254. var params = toSeq({
  255. "include_available_features": "1",
  256. "include_entities": "1",
  257. "include_new_items_bar": "false",
  258. "reset_error_state": "false"
  259. })
  260. if after.len > 0:
  261. params.add {"max_position": after}
  262. let json = await fetchJson(base / (timelineUrl % username) ? params, headers)
  263. result = await finishTimeline(json, none(Query), after, agent)
  264. proc getTimelineSearch*(query: Query; after, agent: string): Future[Timeline] {.async.} =
  265. let queryParam = genQueryParam(query)
  266. let queryEncoded = encodeUrl(queryParam, usePlus=false)
  267. let headers = newHttpHeaders({
  268. "Accept": jsonAccept,
  269. "Referer": $(base / ("search?f=tweets&vertical=default&q=$1&src=typd" % queryEncoded)),
  270. "User-Agent": agent,
  271. "X-Requested-With": "XMLHttpRequest",
  272. "Authority": "twitter.com",
  273. "Accept-Language": lang
  274. })
  275. let params = {
  276. "f": "tweets",
  277. "vertical": "default",
  278. "q": queryParam,
  279. "src": "typd",
  280. "include_available_features": "1",
  281. "include_entities": "1",
  282. "max_position": if after.len > 0: genPos(after) else: "0",
  283. "reset_error_state": "false"
  284. }
  285. let json = await fetchJson(base / timelineSearchUrl ? params, headers)
  286. result = await finishTimeline(json, some(query), after, agent)
  287. proc getProfileAndTimeline*(username, agent, after: string): Future[(Profile, Timeline)] {.async.} =
  288. let headers = newHttpHeaders({
  289. "authority": "twitter.com",
  290. "accept": accept,
  291. "referer": "https://twitter.com/" & username,
  292. "accept-language": lang
  293. })
  294. var url = base / username
  295. if after.len > 0:
  296. url = url ? {"max_position": after}
  297. let
  298. html = await fetchHtml(url, headers)
  299. timeline = parseTimeline(html.select("#timeline > .stream-container"), after)
  300. profile = parseTimelineProfile(html)
  301. vidsFut = getVideos(timeline, agent)
  302. pollFut = getPolls(timeline, agent)
  303. cardFut = getCards(timeline, agent)
  304. await all(vidsFut, pollFut, cardFut)
  305. result = (profile, timeline)
  306. proc getProfileFull*(username: string): Future[Profile] {.async.} =
  307. let headers = newHttpHeaders({
  308. "authority": "twitter.com",
  309. "accept": accept,
  310. "referer": "https://twitter.com/" & username,
  311. "accept-language": lang
  312. })
  313. let html = await fetchHtml(base / username, headers)
  314. if html == nil: return
  315. result = parseTimelineProfile(html)