parserutils.nim 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import xmltree, times, uri, options, json
  2. import strtabs, strformat, strutils, sequtils
  3. import regex
  4. import types, formatters
  5. from q import nil
  6. from htmlgen import a
  7. const
  8. thumbRegex = re".+:url\('([^']+)'\)"
  9. gifRegex = re".+thumb/([^\.']+)\.[jpng].*"
  10. reColor = re"a:active \{\n +color: (#[A-Z0-9]+)"
  11. proc selectAll*(node: XmlNode; selector: string): seq[XmlNode] =
  12. if node == nil: return
  13. q.select(node, selector)
  14. proc select*(node: XmlNode; selector: string): XmlNode =
  15. if node == nil: return
  16. let nodes = node.selectAll(selector)
  17. if nodes.len > 0: nodes[0] else: nil
  18. proc selectAttr*(node: XmlNode; selector: string; attr: string): string =
  19. let res = node.select(selector)
  20. if res == nil: "" else: res.attr(attr)
  21. proc selectText*(node: XmlNode; selector: string): string =
  22. let res = node.select(selector)
  23. result = if res == nil: "" else: res.innerText()
  24. proc getHeader(profile: XmlNode): XmlNode =
  25. result = profile.select(".permalink-header")
  26. if result == nil:
  27. result = profile.select(".stream-item-header")
  28. if result == nil:
  29. result = profile.select(".ProfileCard-userFields")
  30. if result == nil:
  31. result = profile
  32. proc isVerified*(profile: XmlNode): bool =
  33. getHeader(profile).select(".Icon.Icon--verified") != nil
  34. proc isProtected*(profile: XmlNode): bool =
  35. getHeader(profile).select(".Icon.Icon--protected") != nil
  36. proc parseText*(text: XmlNode; skipLink=""): string =
  37. if text == nil: return
  38. for el in text:
  39. case el.kind
  40. of xnText:
  41. result.add el
  42. of xnElement:
  43. if el.attrs == nil:
  44. if el.tag == "strong":
  45. result.add $el
  46. continue
  47. let class = el.attr("class")
  48. if "data-expanded-url" in el.attrs:
  49. let url = el.attr("data-expanded-url")
  50. if url == skipLink: continue
  51. if "u-hidden" in class and result.len > 0:
  52. result.add "\n"
  53. result.add a(shortLink(url), href=url)
  54. elif "ashtag" in class or "hashflag" in class:
  55. let hash = el.innerText()
  56. result.add a(hash, href=("/search?q=" & encodeUrl(hash)))
  57. elif "atreply" in class:
  58. result.add a(el.innerText(), href=el.attr("href"))
  59. elif "Emoji" in class:
  60. result.add el.attr("alt")
  61. else: discard
  62. proc getQuoteText*(tweet: XmlNode): string =
  63. parseText(tweet.select(".QuoteTweet-text"))
  64. proc getTweetText*(tweet: XmlNode): string =
  65. let
  66. quote = tweet.select(".QuoteTweet")
  67. text = tweet.select(".tweet-text")
  68. link = text.selectAttr("a.twitter-timeline-link.u-hidden", "data-expanded-url")
  69. parseText(text, if quote != nil: link else: "")
  70. proc getTimestamp*(tweet: XmlNode): Time =
  71. let time = tweet.selectAttr(".js-short-timestamp", "data-time")
  72. fromUnix(if time.len > 0: parseBiggestInt(time) else: 0)
  73. proc getShortTime*(tweet: XmlNode): string =
  74. tweet.selectText(".js-short-timestamp")
  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(reColor, 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. result = reply.selectAll(selector).map(innerText)
  153. proc getGif(player: XmlNode): Gif =
  154. let
  155. thumb = player.attr("style").replace(thumbRegex, "$1")
  156. id = thumb.replace(gifRegex, "$1")
  157. url = &"https://video.twimg.com/tweet_video/{id}.mp4"
  158. Gif(url: url, thumb: thumb)
  159. proc getTweetMedia*(tweet: Tweet; node: XmlNode) =
  160. for photo in node.selectAll(".AdaptiveMedia-photoContainer"):
  161. tweet.photos.add photo.attrs["data-image-url"]
  162. let player = node.select(".PlayableMedia")
  163. if player == nil: return
  164. let attrib = player.select(".PlayableMedia-attribution")
  165. if attrib != nil:
  166. tweet.attribution = some Profile(
  167. username: attrib.attr("href").strip(chars={'/'}),
  168. fullname: attrib.selectText(".fullname"),
  169. userpic: attrib.selectAttr(".avatar", "src")
  170. )
  171. if "gif" in player.attr("class"):
  172. tweet.gif = some getGif(player.select(".PlayableMedia-player"))
  173. elif "video" in player.attr("class"):
  174. let
  175. thumb = player.selectAttr(".PlayableMedia-player", "style").split("'")
  176. desc = player.selectText(".PlayableMedia-description")
  177. title = player.selectText(".PlayableMedia-title")
  178. var video = Video(title: title, description: desc)
  179. if thumb.len > 1:
  180. video.thumb = thumb[^2]
  181. tweet.video = some video
  182. proc getQuoteMedia*(quote: var Quote; node: XmlNode) =
  183. if node.select(".QuoteTweet--sensitive") != nil:
  184. quote.sensitive = true
  185. return
  186. let media = node.select(".QuoteMedia")
  187. if media != nil:
  188. quote.thumb = media.selectAttr("img", "src")
  189. let badge = node.select(".AdaptiveMedia-badgeText")
  190. let gifBadge = node.select(".Icon--gifBadge")
  191. if badge != nil:
  192. quote.badge = badge.innerText()
  193. elif gifBadge != nil:
  194. quote.badge = "GIF"
  195. proc getTweetCard*(tweet: Tweet; node: XmlNode) =
  196. if node.attr("data-has-cards") == "false": return
  197. var cardType = node.attr("data-card2-type")
  198. if ":" in cardType:
  199. cardType = cardType.split(":")[^1]
  200. if "poll" in cardType:
  201. tweet.poll = some Poll()
  202. return
  203. if "message_me" in cardType:
  204. return
  205. let cardDiv = node.select(".card2 > .js-macaw-cards-iframe-container")
  206. if cardDiv == nil: return
  207. var card = Card(
  208. id: $tweet.id,
  209. query: cardDiv.attr("data-src")
  210. )
  211. try:
  212. card.kind = parseEnum[CardKind](cardType)
  213. except ValueError:
  214. card.kind = summary
  215. let cardUrl = cardDiv.attr("data-card-url")
  216. for n in node.selectAll(".tweet-text a"):
  217. if n.attr("href") == cardUrl:
  218. card.url = n.attr("data-expanded-url")
  219. tweet.card = some card
  220. proc getMoreReplies*(node: XmlNode): int64 =
  221. let text = node.innerText().strip()
  222. try:
  223. result = parseBiggestInt(text.split(" ")[0])
  224. except:
  225. result = -1
  226. proc getMediaTags*(node: XmlNode): seq[Profile] =
  227. let usernames = node.attr("data-tagged")
  228. if usernames.len == 0: return
  229. let users = parseJson(node.attr("data-reply-to-users-json"))
  230. for user in users:
  231. let un = user["screen_name"].getStr
  232. if un notin usernames: continue
  233. result.add Profile(username: un, fullname: user["name"].getStr)
  234. proc getTweetLocation*(node: XmlNode): string =
  235. let geo = node.select(".js-geo-pivot-link")
  236. if geo == nil: return
  237. result = geo.innerText().stripText()
  238. result &= ":" & geo.attr("data-place-id")