parserutils.nim 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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): string =
  67. profile.selectText(selector).stripText()
  68. proc getAvatar*(profile: XmlNode; selector: string): string =
  69. profile.selectAttr(selector, "src").getUserpic()
  70. proc getBanner*(node: XmlNode): string =
  71. let url = node.selectAttr("svg > image", "xlink:href")
  72. if url.len > 0:
  73. result = url.replace("600x200", "1500x500")
  74. else:
  75. result = node.selectAttr(".ProfileCard-bg", "style")
  76. result = result.replace("background-color: ", "")
  77. if result.len == 0:
  78. result = "#161616"
  79. proc getTimelineBanner*(node: XmlNode): string =
  80. let banner = node.select(".ProfileCanopy-headerBg img")
  81. let img = banner.attr("src")
  82. if img.len > 0:
  83. return img
  84. let style = node.select("style").innerText()
  85. var m: RegexMatch
  86. if style.find(re"a:active \{\n +color: (#[A-Z0-9]+)", m):
  87. return style[m.group(0)[0]]
  88. proc getMediaCount*(node: XmlNode): string =
  89. let text = node.selectText(".PhotoRail-headingWithCount")
  90. return text.stripText().split(" ")[0]
  91. proc getProfileStats*(profile: var Profile; node: XmlNode) =
  92. for s in node.selectAll( ".ProfileNav-stat"):
  93. let text = s.attr("title").split(" ")[0]
  94. case s.attr("data-nav")
  95. of "followers": profile.followers = text
  96. of "following": profile.following = text
  97. of "favorites": profile.likes = text
  98. of "tweets": profile.tweets = text
  99. proc getPopupStats*(profile: var Profile; node: XmlNode) =
  100. for s in node.selectAll( ".ProfileCardStats-statLink"):
  101. let text = s.attr("title").split(" ")[0]
  102. case s.attr("href").split("/")[^1]
  103. of "followers": profile.followers = text
  104. of "following": profile.following = text
  105. else: profile.tweets = text
  106. proc getIntentStats*(profile: var Profile; node: XmlNode) =
  107. profile.tweets = "?"
  108. for s in node.selectAll( "dd.count > a"):
  109. let text = s.innerText()
  110. case s.attr("href").split("/")[^1]
  111. of "followers": profile.followers = text
  112. of "following": profile.following = text
  113. proc parseTweetStats*(node: XmlNode): TweetStats =
  114. result = TweetStats(replies: "0", retweets: "0", likes: "0")
  115. for action in node.selectAll(".ProfileTweet-actionCountForAria"):
  116. let text = action.innerText.split()
  117. case text[1][0 .. 2]
  118. of "ret": result.retweets = text[0]
  119. of "rep": result.replies = text[0]
  120. of "lik": result.likes = text[0]
  121. proc parseTweetReply*(node: XmlNode): seq[string] =
  122. let reply = node.select(".ReplyingToContextBelowAuthor")
  123. if reply == nil: return
  124. let selector = if "Quote" in node.attr("class"): "b"
  125. else: "a b"
  126. for username in reply.selectAll(selector):
  127. result.add username.innerText()
  128. proc getGif(player: XmlNode): Gif =
  129. let
  130. thumb = player.attr("style").replace(thumbRegex, "$1")
  131. id = thumb.replace(gifRegex, "$1")
  132. url = &"https://video.twimg.com/tweet_video/{id}.mp4"
  133. Gif(url: url, thumb: thumb)
  134. proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
  135. for photo in node.selectAll(".AdaptiveMedia-photoContainer"):
  136. tweet.photos.add photo.attrs["data-image-url"]
  137. let player = node.select(".PlayableMedia")
  138. if player == nil: return
  139. if "gif" in player.attr("class"):
  140. tweet.gif = some(getGif(player.select(".PlayableMedia-player")))
  141. elif "video" in player.attr("class"):
  142. tweet.video = some(Video())
  143. proc getQuoteMedia*(quote: var Quote; node: XmlNode) =
  144. if node.select(".QuoteTweet--sensitive") != nil:
  145. quote.sensitive = true
  146. return
  147. let media = node.select(".QuoteMedia")
  148. if media != nil:
  149. quote.thumb = media.selectAttr("img", "src")
  150. let badge = node.select(".AdaptiveMedia-badgeText")
  151. let gifBadge = node.select(".Icon--gifBadge")
  152. if badge != nil:
  153. quote.badge = badge.innerText()
  154. elif gifBadge != nil:
  155. quote.badge = "GIF"
  156. proc getTweetCard*(tweet: Tweet; node: XmlNode) =
  157. if node.attr("data-has-cards") == "false": return
  158. var cardType = node.attr("data-card2-type")
  159. if ":" in cardType:
  160. cardType = cardType.split(":")[^1]
  161. if "poll" in cardType:
  162. tweet.poll = some(Poll())
  163. return
  164. let cardDiv = node.select(".card2 > .js-macaw-cards-iframe-container")
  165. if cardDiv == nil: return
  166. var card = Card(
  167. id: tweet.id,
  168. query: cardDiv.attr("data-src")
  169. )
  170. try:
  171. card.kind = parseEnum[CardKind](cardType)
  172. except ValueError:
  173. card.kind = summary
  174. let cardUrl = cardDiv.attr("data-card-url")
  175. for n in node.selectAll(".tweet-text a"):
  176. if n.attr("href") == cardUrl:
  177. card.url = n.attr("data-expanded-url")
  178. tweet.card = some(card)
  179. proc getMoreReplies*(node: XmlNode): int =
  180. let text = node.innerText().strip()
  181. try:
  182. result = parseInt(text.split(" ")[0])
  183. except:
  184. result = -1