parserutils.nim 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import xmltree, strtabs, strformat, strutils, times, uri
  2. import regex
  3. import types, formatters
  4. from q import nil
  5. from htmlgen import a
  6. const
  7. thumbRegex = re".+:url\('([^']+)'\)"
  8. gifRegex = re".+thumb/([^\.']+)\.[jpng].*"
  9. proc selectAll*(node: XmlNode; selector: string): seq[XmlNode] =
  10. if node == nil: return
  11. q.select(node, selector)
  12. proc select*(node: XmlNode; selector: string): XmlNode =
  13. if node == nil: return
  14. let nodes = node.selectAll(selector)
  15. if nodes.len > 0: nodes[0] else: nil
  16. proc selectAttr*(node: XmlNode; selector: string; attr: string): string =
  17. let res = node.select(selector)
  18. if res == nil: "" else: res.attr(attr)
  19. proc selectText*(node: XmlNode; selector: string): string =
  20. let res = node.select(selector)
  21. result = if res == nil: "" else: res.innerText()
  22. proc getHeader(profile: XmlNode): XmlNode =
  23. result = profile.select(".permalink-header")
  24. if result == nil:
  25. result = profile.select(".stream-item-header")
  26. if result == nil:
  27. result = profile.select(".ProfileCard-userFields")
  28. if result == nil:
  29. result = profile
  30. proc isVerified*(profile: XmlNode): bool =
  31. getHeader(profile).select(".Icon.Icon--verified") != nil
  32. proc isProtected*(profile: XmlNode): bool =
  33. getHeader(profile).select(".Icon.Icon--protected") != nil
  34. proc parseText*(text: XmlNode; skipLink=""): string =
  35. if text == nil: return
  36. for el in text:
  37. case el.kind
  38. of xnText:
  39. result.add el
  40. of xnElement:
  41. if el.attrs == nil:
  42. if el.tag == "strong":
  43. result.add $el
  44. continue
  45. let class = el.attr("class")
  46. if "data-expanded-url" in el.attrs:
  47. let url = el.attr("data-expanded-url")
  48. if url == skipLink: continue
  49. if "u-hidden" in class and result.len > 0:
  50. result.add "\n"
  51. result.add a(shortLink(url), href=url)
  52. elif "ashtag" in class or "hashflag" in class:
  53. let hash = el.innerText()
  54. result.add a(hash, href=("/search?q=" & encodeUrl(hash)))
  55. elif "atreply" in class:
  56. result.add a(el.innerText(), href=el.attr("href"))
  57. elif "Emoji" in class:
  58. result.add el.attr("alt")
  59. else: discard
  60. proc getQuoteText*(tweet: XmlNode): string =
  61. parseText(tweet.select(".QuoteTweet-text"))
  62. proc getTweetText*(tweet: XmlNode): string =
  63. let
  64. quote = tweet.select(".QuoteTweet")
  65. text = tweet.select(".tweet-text")
  66. link = text.selectAttr("a.twitter-timeline-link.u-hidden", "data-expanded-url")
  67. parseText(text, if quote != nil: link else: "")
  68. proc getTime(tweet: XmlNode): XmlNode =
  69. tweet.select(".js-short-timestamp")
  70. proc getTimestamp*(tweet: XmlNode): Time =
  71. let time = getTime(tweet).attr("data-time")
  72. fromUnix(if time.len > 0: parseInt(time) else: 0)
  73. proc getShortTime*(tweet: XmlNode): string =
  74. getTime(tweet).innerText()
  75. proc getDate*(node: XmlNode; selector: string): Time =
  76. let date = node.select(selector)
  77. if date == nil: return
  78. parseTime(date.attr("title"), "h:mm tt - d MMM YYYY", utc())
  79. proc getName*(profile: XmlNode; selector: string): string =
  80. profile.selectText(selector).stripText()
  81. proc getUsername*(profile: XmlNode; selector: string): string =
  82. profile.selectText(selector).strip(chars={'@', ' ', '\n'})
  83. proc getBio*(profile: XmlNode; selector: string; fallback=""): string =
  84. var bio = profile.select(selector)
  85. if bio == nil and fallback.len > 0:
  86. bio = profile.select(fallback)
  87. parseText(bio)
  88. proc getLocation*(profile: XmlNode): string =
  89. let sel = ".ProfileHeaderCard-locationText"
  90. result = profile.selectText(sel).stripText()
  91. let link = profile.selectAttr(sel & " a", "data-place-id")
  92. if link.len > 0:
  93. result &= ":" & link
  94. proc getAvatar*(profile: XmlNode; selector: string): string =
  95. profile.selectAttr(selector, "src").getUserpic()
  96. proc getBanner*(node: XmlNode): string =
  97. let url = node.selectAttr("svg > image", "xlink:href")
  98. if url.len > 0:
  99. result = url.replace("600x200", "1500x500")
  100. else:
  101. result = node.selectAttr(".ProfileCard-bg", "style")
  102. result = result.replace("background-color: ", "")
  103. if result.len == 0:
  104. result = "#161616"
  105. proc getTimelineBanner*(node: XmlNode): string =
  106. let banner = node.select(".ProfileCanopy-headerBg img")
  107. let img = banner.attr("src")
  108. if img.len > 0:
  109. return img
  110. let style = node.select("style").innerText()
  111. var m: RegexMatch
  112. if style.find(re"a:active \{\n +color: (#[A-Z0-9]+)", m):
  113. return style[m.group(0)[0]]
  114. proc getMediaCount*(node: XmlNode): string =
  115. let text = node.selectText(".PhotoRail-headingWithCount")
  116. return text.stripText().split(" ")[0]
  117. proc getProfileStats*(profile: var Profile; node: XmlNode) =
  118. for s in node.selectAll( ".ProfileNav-stat"):
  119. let text = s.attr("title").split(" ")[0]
  120. case s.attr("data-nav")
  121. of "followers": profile.followers = text
  122. of "following": profile.following = text
  123. of "favorites": profile.likes = text
  124. of "tweets": profile.tweets = text
  125. proc getPopupStats*(profile: var Profile; node: XmlNode) =
  126. for s in node.selectAll( ".ProfileCardStats-statLink"):
  127. let text = s.attr("title").split(" ")[0]
  128. case s.attr("href").split("/")[^1]
  129. of "followers": profile.followers = text
  130. of "following": profile.following = text
  131. else: profile.tweets = text
  132. proc getIntentStats*(profile: var Profile; node: XmlNode) =
  133. profile.tweets = "?"
  134. for s in node.selectAll( "dd.count > a"):
  135. let text = s.innerText()
  136. case s.attr("href").split("/")[^1]
  137. of "followers": profile.followers = text
  138. of "following": profile.following = text
  139. proc parseTweetStats*(node: XmlNode): TweetStats =
  140. result = TweetStats(replies: "0", retweets: "0", likes: "0")
  141. for action in node.selectAll(".ProfileTweet-actionCountForAria"):
  142. let text = action.innerText.split()
  143. case text[1][0 .. 2]
  144. of "ret": result.retweets = text[0]
  145. of "rep": result.replies = text[0]
  146. of "lik": result.likes = text[0]
  147. proc parseTweetReply*(node: XmlNode): seq[string] =
  148. let reply = node.select(".ReplyingToContextBelowAuthor")
  149. if reply == nil: return
  150. let selector = if "Quote" in node.attr("class"): "b"
  151. else: "a b"
  152. for username in reply.selectAll(selector):
  153. result.add username.innerText()
  154. proc getGif(player: XmlNode): Gif =
  155. let
  156. thumb = player.attr("style").replace(thumbRegex, "$1")
  157. id = thumb.replace(gifRegex, "$1")
  158. url = &"https://video.twimg.com/tweet_video/{id}.mp4"
  159. Gif(url: url, thumb: thumb)
  160. proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
  161. for photo in node.selectAll(".AdaptiveMedia-photoContainer"):
  162. tweet.photos.add photo.attrs["data-image-url"]
  163. let player = node.select(".PlayableMedia")
  164. if player == nil: return
  165. if "gif" in player.attr("class"):
  166. tweet.gif = some getGif(player.select(".PlayableMedia-player"))
  167. elif "video" in player.attr("class"):
  168. tweet.video = some Video()
  169. proc getQuoteMedia*(quote: var Quote; node: XmlNode) =
  170. if node.select(".QuoteTweet--sensitive") != nil:
  171. quote.sensitive = true
  172. return
  173. let media = node.select(".QuoteMedia")
  174. if media != nil:
  175. quote.thumb = media.selectAttr("img", "src")
  176. let badge = node.select(".AdaptiveMedia-badgeText")
  177. let gifBadge = node.select(".Icon--gifBadge")
  178. if badge != nil:
  179. quote.badge = badge.innerText()
  180. elif gifBadge != nil:
  181. quote.badge = "GIF"
  182. proc getTweetCard*(tweet: Tweet; node: XmlNode) =
  183. if node.attr("data-has-cards") == "false": return
  184. var cardType = node.attr("data-card2-type")
  185. if ":" in cardType:
  186. cardType = cardType.split(":")[^1]
  187. if "poll" in cardType:
  188. tweet.poll = some Poll()
  189. return
  190. if "message_me" in cardType:
  191. return
  192. let cardDiv = node.select(".card2 > .js-macaw-cards-iframe-container")
  193. if cardDiv == nil: return
  194. var card = Card(
  195. id: $tweet.id,
  196. query: cardDiv.attr("data-src")
  197. )
  198. try:
  199. card.kind = parseEnum[CardKind](cardType)
  200. except ValueError:
  201. card.kind = summary
  202. let cardUrl = cardDiv.attr("data-card-url")
  203. for n in node.selectAll(".tweet-text a"):
  204. if n.attr("href") == cardUrl:
  205. card.url = n.attr("data-expanded-url")
  206. tweet.card = some card
  207. proc getMoreReplies*(node: XmlNode): int =
  208. let text = node.innerText().strip()
  209. try:
  210. result = parseInt(text.split(" ")[0])
  211. except:
  212. result = -1