parserutils.nim 7.2 KB

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