parserutils.nim 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import strutils, times, macros, htmlgen, unicode, options
  2. import regex, packedjson
  3. import types, utils, formatters
  4. const
  5. unRegex = re"(^|[^A-z0-9-_./?])@([A-z0-9_]{1,15})"
  6. unReplace = "$1<a href=\"/$2\">@$2</a>"
  7. htRegex = re"(^|[^\w-_./?])([#$])([\w_]+)"
  8. htReplace = "$1<a href=\"/search?q=%23$3\">$2$3</a>"
  9. template isNull*(js: JsonNode): bool = js.kind == JNull
  10. template notNull*(js: JsonNode): bool = js.kind != JNull
  11. template `?`*(js: JsonNode): untyped =
  12. let j = js
  13. if j.isNull: return
  14. else: j
  15. template `with`*(ident, value, body): untyped =
  16. block:
  17. let ident {.inject.} = value
  18. if ident != nil: body
  19. template `with`*(ident; value: JsonNode; body): untyped =
  20. block:
  21. let ident {.inject.} = value
  22. if value.notNull: body
  23. template getCursor*(js: JsonNode): string =
  24. js{"content", "operation", "cursor", "value"}.getStr
  25. template getError*(js: JsonNode): Error =
  26. if js.kind != JArray or js.len == 0: null
  27. else: Error(js[0]{"code"}.getInt)
  28. template parseTime(time: string; f: static string; flen: int): Time =
  29. if time.len != flen: return
  30. parse(time, f).toTime
  31. proc getDateTime*(js: JsonNode): Time =
  32. parseTime(js.getStr, "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", 20)
  33. proc getTime*(js: JsonNode): Time =
  34. parseTime(js.getStr, "ddd MMM dd hh:mm:ss \'+0000\' yyyy", 30)
  35. proc getId*(id: string): string {.inline.} =
  36. let start = id.rfind("-")
  37. if start < 0: return id
  38. id[start + 1 ..< id.len]
  39. proc getId*(js: JsonNode): int64 {.inline.} =
  40. case js.kind
  41. of JString: return parseBiggestInt(js.getStr("0"))
  42. of JInt: return js.getBiggestInt()
  43. else: return 0
  44. template getStrVal*(js: JsonNode; default=""): string =
  45. js{"string_value"}.getStr(default)
  46. proc getImageStr*(js: JsonNode): string =
  47. result = js.getStr
  48. result.removePrefix(https)
  49. result.removePrefix(twimg)
  50. template getImageVal*(js: JsonNode): string =
  51. js{"image_value", "url"}.getImageStr
  52. proc getCardUrl*(js: JsonNode; kind: CardKind): string =
  53. result = js{"website_url"}.getStrVal
  54. if kind == promoVideoConvo:
  55. result = js{"thank_you_url"}.getStrVal(result)
  56. if result.startsWith("card://"):
  57. result = ""
  58. proc getCardDomain*(js: JsonNode; kind: CardKind): string =
  59. result = js{"vanity_url"}.getStrVal(js{"domain"}.getStr)
  60. if kind == promoVideoConvo:
  61. result = js{"thank_you_vanity_url"}.getStrVal(result)
  62. proc getCardTitle*(js: JsonNode; kind: CardKind): string =
  63. result = js{"title"}.getStrVal
  64. if kind == promoVideoConvo:
  65. result = js{"thank_you_text"}.getStrVal(result)
  66. elif kind == liveEvent:
  67. result = js{"event_category"}.getStrVal
  68. elif kind in {videoDirectMessage, imageDirectMessage}:
  69. result = js{"cta1"}.getStrVal
  70. proc getBanner*(js: JsonNode): string =
  71. let url = js{"profile_banner_url"}.getImageStr
  72. if url.len > 0:
  73. return url & "/1500x500"
  74. let color = js{"profile_link_color"}.getStr
  75. if color.len > 0:
  76. return '#' & color
  77. # use primary color from profile picture color histrogram
  78. with p, js{"profile_image_extensions", "mediaColor", "r", "ok", "palette"}:
  79. if p.len > 0:
  80. let pal = p[0]{"rgb"}
  81. result = "#"
  82. result.add toHex(pal{"red"}.getInt, 2)
  83. result.add toHex(pal{"green"}.getInt, 2)
  84. result.add toHex(pal{"blue"}.getInt, 2)
  85. return
  86. return "#161616"
  87. proc getTombstone*(js: JsonNode): string =
  88. result = js{"tombstoneInfo", "richText", "text"}.getStr
  89. result.removeSuffix(" Learn more")
  90. template getSlice(text: string; slice: seq[int]): string =
  91. text.runeSubStr(slice[0], slice[1] - slice[0])
  92. proc getSlice(text: string; js: JsonNode): string =
  93. if js.kind != JArray or js.len < 2 or js[0].kind != JInt: return text
  94. let slice = @[js{0}.getInt, js{1}.getInt]
  95. text.getSlice(slice)
  96. proc expandUrl(text: var string; js: JsonNode; tLen: int; hideTwitter=false) =
  97. let u = js{"url"}.getStr
  98. if u.len == 0 or u notin text:
  99. return
  100. let
  101. url = js{"expanded_url"}.getStr
  102. slice = js{"indices"}[1].getInt
  103. if hideTwitter and slice >= tLen and url.isTwitterUrl:
  104. text = text.replace(u, "")
  105. text.removeSuffix(' ')
  106. text.removeSuffix('\n')
  107. else:
  108. text = text.replace(u, a(shortLink(url), href=url))
  109. proc expandMention(text: var string; orig: string; js: JsonNode) =
  110. let
  111. name = js{"name"}.getStr
  112. href = '/' & js{"screen_name"}.getStr
  113. uname = orig.getSlice(js{"indices"})
  114. text = text.replace(uname, a(uname, href=href, title=name))
  115. proc expandProfileEntities*(profile: var Profile; js: JsonNode) =
  116. let
  117. orig = profile.bio
  118. ent = ? js{"entities"}
  119. with urls, ent{"url", "urls"}:
  120. profile.website = urls[0]{"expanded_url"}.getStr
  121. with urls, ent{"description", "urls"}:
  122. for u in urls: profile.bio.expandUrl(u, orig.high)
  123. profile.bio = profile.bio.replace(unRegex, unReplace)
  124. .replace(htRegex, htReplace)
  125. for mention in ? ent{"user_mentions"}:
  126. profile.bio.expandMention(orig, mention)
  127. proc expandTweetEntities*(tweet: Tweet; js: JsonNode) =
  128. let
  129. orig = tweet.text
  130. textRange = js{"display_text_range"}
  131. slice = @[textRange{0}.getInt, textRange{1}.getInt]
  132. hasQuote = js{"is_quote_status"}.getBool
  133. hasCard = tweet.card.isSome
  134. tweet.text = tweet.text.getSlice(slice)
  135. var replyTo = ""
  136. if tweet.replyId != 0:
  137. with reply, js{"in_reply_to_screen_name"}:
  138. tweet.reply.add reply.getStr
  139. replyTo = reply.getStr
  140. let ent = ? js{"entities"}
  141. with urls, ent{"urls"}:
  142. for u in urls:
  143. tweet.text.expandUrl(u, slice[1], hasQuote)
  144. if hasCard and u{"url"}.getStr == get(tweet.card).url:
  145. get(tweet.card).url = u{"expanded_url"}.getStr
  146. with media, ent{"media"}:
  147. for m in media: tweet.text.expandUrl(m, slice[1], hideTwitter=true)
  148. if "hashtags" in ent or "symbols" in ent:
  149. tweet.text = tweet.text.replace(htRegex, htReplace)
  150. for mention in ? ent{"user_mentions"}:
  151. let
  152. name = mention{"screen_name"}.getStr
  153. idx = tweet.reply.find(name)
  154. if mention{"indices"}[0].getInt >= slice[0]:
  155. tweet.text.expandMention(orig, mention)
  156. if idx > -1 and name != replyTo:
  157. tweet.reply.delete idx
  158. elif idx == -1 and tweet.replyId != 0:
  159. tweet.reply.add name