parser.nim 7.3 KB

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