parserutils.nim 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import xmltree, strtabs, strformat, strutils, times, uri, options
  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 getTimestamp*(tweet: XmlNode): Time =
  69. let time = tweet.selectAttr(".js-short-timestamp", "data-time")
  70. fromUnix(if time.len > 0: parseInt(time) else: 0)
  71. proc getShortTime*(tweet: XmlNode): string =
  72. tweet.selectText(".js-short-timestamp")
  73. proc getDate*(node: XmlNode; selector: string): Time =
  74. let date = node.select(selector)
  75. if date == nil: return
  76. parseTime(date.attr("title"), "h:mm tt - d MMM YYYY", utc())
  77. proc getName*(profile: XmlNode; selector: string): string =
  78. profile.selectText(selector).stripText()
  79. proc getUsername*(profile: XmlNode; selector: string): string =
  80. profile.selectText(selector).strip(chars={'@', ' ', '\n'})
  81. proc getBio*(profile: XmlNode; selector: string; fallback=""): string =
  82. var bio = profile.select(selector)
  83. if bio == nil and fallback.len > 0:
  84. bio = profile.select(fallback)
  85. parseText(bio)
  86. proc getLocation*(profile: XmlNode): string =
  87. let sel = ".ProfileHeaderCard-locationText"
  88. result = profile.selectText(sel).stripText()
  89. let link = profile.selectAttr(sel & " a", "data-place-id")
  90. if link.len > 0:
  91. result &= ":" & link
  92. proc getAvatar*(profile: XmlNode; selector: string): string =
  93. profile.selectAttr(selector, "src").getUserpic()
  94. proc getBanner*(node: XmlNode): string =
  95. let url = node.selectAttr("svg > image", "xlink:href")
  96. if url.len > 0:
  97. result = url.replace("600x200", "1500x500")
  98. else:
  99. result = node.selectAttr(".ProfileCard-bg", "style")
  100. result = result.replace("background-color: ", "")
  101. if result.len == 0:
  102. result = "#161616"
  103. proc getTimelineBanner*(node: XmlNode): string =
  104. let banner = node.select(".ProfileCanopy-headerBg img")
  105. let img = banner.attr("src")
  106. if img.len > 0:
  107. return img
  108. let style = node.select("style").innerText()
  109. var m: RegexMatch
  110. if style.find(re"a:active \{\n +color: (#[A-Z0-9]+)", m):
  111. return style[m.group(0)[0]]
  112. proc getMediaCount*(node: XmlNode): string =
  113. let text = node.selectText(".PhotoRail-headingWithCount")
  114. return text.stripText().split(" ")[0]
  115. proc getProfileStats*(profile: var Profile; node: XmlNode) =
  116. for s in node.selectAll( ".ProfileNav-stat"):
  117. let text = s.attr("title").split(" ")[0]
  118. case s.attr("data-nav")
  119. of "followers": profile.followers = text
  120. of "following": profile.following = text
  121. of "favorites": profile.likes = text
  122. of "tweets": profile.tweets = text
  123. proc getPopupStats*(profile: var Profile; node: XmlNode) =
  124. for s in node.selectAll( ".ProfileCardStats-statLink"):
  125. let text = s.attr("title").split(" ")[0]
  126. case s.attr("href").split("/")[^1]
  127. of "followers": profile.followers = text
  128. of "following": profile.following = text
  129. else: profile.tweets = text
  130. proc getIntentStats*(profile: var Profile; node: XmlNode) =
  131. profile.tweets = "?"
  132. for s in node.selectAll( "dd.count > a"):
  133. let text = s.innerText()
  134. case s.attr("href").split("/")[^1]
  135. of "followers": profile.followers = text
  136. of "following": profile.following = text
  137. proc parseTweetStats*(node: XmlNode): TweetStats =
  138. result = TweetStats(replies: "0", retweets: "0", likes: "0")
  139. for action in node.selectAll(".ProfileTweet-actionCountForAria"):
  140. let text = action.innerText.split()
  141. case text[1][0 .. 2]
  142. of "ret": result.retweets = text[0]
  143. of "rep": result.replies = text[0]
  144. of "lik": result.likes = text[0]
  145. proc parseTweetReply*(node: XmlNode): seq[string] =
  146. let reply = node.select(".ReplyingToContextBelowAuthor")
  147. if reply == nil: return
  148. let selector = if "Quote" in node.attr("class"): "b"
  149. else: "a b"
  150. for username in reply.selectAll(selector):
  151. result.add username.innerText()
  152. proc getGif(player: XmlNode): Gif =
  153. let
  154. thumb = player.attr("style").replace(thumbRegex, "$1")
  155. id = thumb.replace(gifRegex, "$1")
  156. url = &"https://video.twimg.com/tweet_video/{id}.mp4"
  157. Gif(url: url, thumb: thumb)
  158. proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
  159. for photo in node.selectAll(".AdaptiveMedia-photoContainer"):
  160. tweet.photos.add photo.attrs["data-image-url"]
  161. let player = node.select(".PlayableMedia")
  162. if player == nil: return
  163. if "gif" in player.attr("class"):
  164. tweet.gif = some getGif(player.select(".PlayableMedia-player"))
  165. elif "video" in player.attr("class"):
  166. let
  167. thumb = player.selectAttr(".PlayableMedia-player", "style").split("'")
  168. desc = player.selectText(".PlayableMedia-description")
  169. title = player.selectText(".PlayableMedia-title")
  170. var video = Video(title: title, description: desc)
  171. if thumb.len > 1:
  172. video.thumb = thumb[^2]
  173. tweet.video = some video
  174. proc getQuoteMedia*(quote: var Quote; node: XmlNode) =
  175. if node.select(".QuoteTweet--sensitive") != nil:
  176. quote.sensitive = true
  177. return
  178. let media = node.select(".QuoteMedia")
  179. if media != nil:
  180. quote.thumb = media.selectAttr("img", "src")
  181. let badge = node.select(".AdaptiveMedia-badgeText")
  182. let gifBadge = node.select(".Icon--gifBadge")
  183. if badge != nil:
  184. quote.badge = badge.innerText()
  185. elif gifBadge != nil:
  186. quote.badge = "GIF"
  187. proc getTweetCard*(tweet: Tweet; node: XmlNode) =
  188. if node.attr("data-has-cards") == "false": return
  189. var cardType = node.attr("data-card2-type")
  190. if ":" in cardType:
  191. cardType = cardType.split(":")[^1]
  192. if "poll" in cardType:
  193. tweet.poll = some Poll()
  194. return
  195. if "message_me" in cardType:
  196. return
  197. let cardDiv = node.select(".card2 > .js-macaw-cards-iframe-container")
  198. if cardDiv == nil: return
  199. var card = Card(
  200. id: $tweet.id,
  201. query: cardDiv.attr("data-src")
  202. )
  203. try:
  204. card.kind = parseEnum[CardKind](cardType)
  205. except ValueError:
  206. card.kind = summary
  207. let cardUrl = cardDiv.attr("data-card-url")
  208. for n in node.selectAll(".tweet-text a"):
  209. if n.attr("href") == cardUrl:
  210. card.url = n.attr("data-expanded-url")
  211. tweet.card = some card
  212. proc getMoreReplies*(node: XmlNode): int =
  213. let text = node.innerText().strip()
  214. try:
  215. result = parseInt(text.split(" ")[0])
  216. except:
  217. result = -1