parser.nim 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import xmltree, sequtils, strtabs, strutils, strformat, json
  2. import types, parserutils, formatters
  3. proc parsePopupProfile*(node: XmlNode): Profile =
  4. let profile = node.select(".profile-card")
  5. if profile == nil: return
  6. result = Profile(
  7. fullname: profile.getName(".fullname"),
  8. username: profile.getUsername(".username"),
  9. bio: profile.getBio(".bio"),
  10. userpic: profile.getAvatar(".ProfileCard-avatarImage"),
  11. verified: isVerified(profile),
  12. protected: isProtected(profile),
  13. banner: getBanner(profile)
  14. )
  15. result.getPopupStats(profile)
  16. proc parseIntentProfile*(profile: XmlNode): Profile =
  17. result = Profile(
  18. fullname: profile.getName("a.fn.url.alternate-context"),
  19. username: profile.getUsername(".nickname"),
  20. bio: profile.getBio("p.note"),
  21. userpic: profile.select(".profile.summary").getAvatar("img.photo"),
  22. verified: profile.select("li.verified") != nil,
  23. protected: profile.select("li.protected") != nil,
  24. banner: getBanner(profile)
  25. )
  26. result.getIntentStats(profile)
  27. proc parseTweetProfile*(profile: XmlNode): Profile =
  28. result = Profile(
  29. fullname: profile.attr("data-name").stripText(),
  30. username: profile.attr("data-screen-name"),
  31. userpic: profile.getAvatar(".avatar"),
  32. verified: isVerified(profile)
  33. )
  34. proc parseQuote*(quote: XmlNode): Quote =
  35. result = Quote(
  36. id: quote.attr("data-item-id"),
  37. text: getQuoteText(quote),
  38. reply: parseTweetReply(quote),
  39. hasThread: quote.select(".self-thread-context") != nil,
  40. available: true
  41. )
  42. result.profile = Profile(
  43. fullname: quote.selectText(".QuoteTweet-fullname").stripText(),
  44. username: quote.attr("data-screen-name"),
  45. verified: isVerified(quote)
  46. )
  47. result.getQuoteMedia(quote)
  48. proc parseTweet*(node: XmlNode): Tweet =
  49. let tweet = node.select(".tweet")
  50. if tweet == nil: return Tweet()
  51. result = Tweet(
  52. id: tweet.attr("data-item-id"),
  53. threadId: tweet.attr("data-conversation-id"),
  54. text: getTweetText(tweet),
  55. time: getTimestamp(tweet),
  56. shortTime: getShortTime(tweet),
  57. profile: parseTweetProfile(tweet),
  58. stats: parseTweetStats(tweet),
  59. reply: parseTweetReply(tweet),
  60. hasThread: tweet.select(".content > .self-thread-context") != nil,
  61. pinned: "pinned" in tweet.attr("class"),
  62. available: true
  63. )
  64. result.getTweetMedia(tweet)
  65. result.getTweetCards(tweet)
  66. let by = tweet.selectText(".js-retweet-text > a > b")
  67. if by.len > 0:
  68. result.retweet = some(Retweet(
  69. by: stripText(by),
  70. id: tweet.attr("data-retweet-id")
  71. ))
  72. let quote = tweet.select(".QuoteTweet-innerContainer")
  73. if quote != nil:
  74. result.quote = some(parseQuote(quote))
  75. let tombstone = tweet.select(".Tombstone")
  76. if tombstone != nil:
  77. if "unavailable" in tombstone.innerText():
  78. result.quote = some(Quote())
  79. proc parseThread*(nodes: XmlNode): Thread =
  80. if nodes == nil: return
  81. result = Thread()
  82. for n in nodes.filterIt(it.kind != xnText):
  83. let class = n.attr("class").toLower()
  84. if "tombstone" in class or "unavailable" in class:
  85. result.tweets.add Tweet()
  86. elif "morereplies" in class:
  87. result.more = getMoreReplies(n)
  88. else:
  89. result.tweets.add parseTweet(n)
  90. proc parseConversation*(node: XmlNode): Conversation =
  91. result = Conversation(
  92. tweet: parseTweet(node.select(".permalink-tweet-container")),
  93. before: parseThread(node.select(".in-reply-to .stream-items"))
  94. )
  95. let replies = node.select(".replies-to .stream-items")
  96. if replies == nil: return
  97. for i, reply in replies.filterIt(it.kind != xnText):
  98. let class = reply.attr("class").toLower()
  99. let thread = reply.select(".stream-items")
  100. if i == 0 and "self" in class:
  101. result.after = parseThread(thread)
  102. elif "lone" in class:
  103. result.replies.add parseThread(reply)
  104. else:
  105. result.replies.add parseThread(thread)
  106. proc parseVideo*(node: JsonNode): Video =
  107. let
  108. track = node{"track"}
  109. cType = track["contentType"].to(string)
  110. pType = track["playbackType"].to(string)
  111. case cType
  112. of "media_entity":
  113. result = Video(
  114. playbackType: if "mp4" in pType: mp4 else: m3u8,
  115. contentId: track["contentId"].to(string),
  116. durationMs: track["durationMs"].to(int),
  117. views: track["viewCount"].to(string),
  118. url: track["playbackUrl"].to(string),
  119. available: track{"mediaAvailability"}["status"].to(string) == "available")
  120. of "vmap":
  121. result = Video(
  122. playbackType: vmap,
  123. durationMs: track["durationMs"].to(int),
  124. url: track["vmapUrl"].to(string))
  125. else:
  126. echo "Can't parse video of type ", cType
  127. result.thumb = node["posterImage"].to(string)
  128. proc parsePoll*(node: XmlNode): Poll =
  129. let
  130. choices = node.selectAll(".PollXChoice-choice")
  131. votes = node.selectText(".PollXChoice-footer--total")
  132. result.votes = votes.strip().split(" ")[0]
  133. result.status = node.selectText(".PollXChoice-footer--time")
  134. for choice in choices:
  135. for span in choice.select(".PollXChoice-choice--text").filterIt(it.kind != xnText):
  136. if span.attr("class").len == 0:
  137. result.options.add span.innerText()
  138. elif "progress" in span.attr("class"):
  139. result.values.add parseInt(span.innerText()[0 .. ^2])
  140. var highest = 0
  141. for i, n in result.values:
  142. if n > highest:
  143. highest = n
  144. result.leader = i
  145. proc parsePhotoRail*(node: XmlNode): seq[GalleryPhoto] =
  146. for img in node.selectAll(".tweet-media-img-placeholder"):
  147. result.add GalleryPhoto(
  148. url: img.attr("data-image-url"),
  149. tweetId: img.attr("data-tweet-id"),
  150. color: img.attr("background-color").replace("style: ", "")
  151. )
  152. proc parseCard*(card: var Card; node: XmlNode) =
  153. let cardKind = node.select("head > meta[name*=card_name]").attr("content")
  154. if "summary_large_image" in cardKind:
  155. card.kind = summaryLarge
  156. elif "summary" in cardKind:
  157. card.kind = summary
  158. elif "live_event" in cardKind:
  159. card.kind = liveEvent
  160. elif "player" in cardKind:
  161. card.kind = player
  162. elif "promo_website" in cardKind:
  163. card.kind = promoWebsite
  164. card.title = node.selectText("h2.TwitterCard-title")
  165. card.text = node.selectText("p.tcu-resetMargin")
  166. card.dest = node.selectText("span.SummaryCard-destination")
  167. let image = node.select(".tcu-imageWrapper > img")
  168. if image != nil:
  169. # workaround for issue 11713
  170. card.image = image.attr("data-src").replace("gname", "g&name")
  171. else:
  172. echo card.id
  173. if card.kind == liveEvent:
  174. card.text = card.title
  175. card.title = node.selectText(".TwitterCard-attribution--category")