2020-06-01 00:16:24 +00:00
|
|
|
import json, strutils, times, tables, macros, htmlgen, uri, unicode, options
|
|
|
|
import regex
|
|
|
|
import types, utils, formatters
|
|
|
|
|
|
|
|
const
|
|
|
|
unRegex = re"(^|[^A-z0-9-_./?])@([A-z0-9_]{1,15})"
|
|
|
|
unReplace = "$1<a href=\"/$2\">@$2</a>"
|
|
|
|
|
|
|
|
htRegex = re"(^|[^A-z0-9-_./?])#([A-z0-9_]+)"
|
|
|
|
htReplace = "$1<a href=\"/search?q=%23$2\">#$2</a>"
|
|
|
|
|
2020-06-01 03:03:45 +00:00
|
|
|
let localTimezone = local()
|
|
|
|
|
2020-06-01 00:16:24 +00:00
|
|
|
template `?`*(js: JsonNode): untyped =
|
|
|
|
let j = js
|
|
|
|
if j == nil: return
|
|
|
|
else: j
|
|
|
|
|
|
|
|
template `with`*(ident, value, body): untyped =
|
|
|
|
block:
|
|
|
|
let ident {.inject.} = value
|
|
|
|
if ident != nil:
|
|
|
|
body
|
|
|
|
|
|
|
|
template `with`*(ident; value: JsonNode; body): untyped =
|
|
|
|
block:
|
|
|
|
let ident {.inject.} = value
|
|
|
|
if ident != nil and ident.kind != JNull:
|
|
|
|
body
|
|
|
|
|
|
|
|
proc getCursor*(js: JsonNode): string =
|
|
|
|
js{"content", "operation", "cursor", "value"}.getStr
|
|
|
|
|
|
|
|
proc parseTime(time: string; f: static string; flen: int): Time =
|
|
|
|
if time.len != flen: return
|
2020-06-01 03:03:45 +00:00
|
|
|
parseTime(time, f, localTimezone).utc.toTime
|
2020-06-01 00:16:24 +00:00
|
|
|
|
|
|
|
proc getDateTime*(js: JsonNode): Time =
|
|
|
|
parseTime(js.getStr, "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", 20)
|
|
|
|
|
|
|
|
proc getTime*(js: JsonNode): Time =
|
|
|
|
parseTime(js.getStr, "ddd MMM dd hh:mm:ss \'+0000\' yyyy", 30)
|
|
|
|
|
|
|
|
proc getId*(id: string): string =
|
|
|
|
let start = id.rfind("-")
|
|
|
|
if start < 0: return id
|
|
|
|
id[start + 1 ..< id.len]
|
|
|
|
|
|
|
|
proc getId*(js: JsonNode): int64 =
|
|
|
|
if js == nil: return
|
|
|
|
case js.kind
|
|
|
|
of JString: return parseBiggestInt(js.getStr("0"))
|
|
|
|
of JInt: return js.getBiggestInt()
|
|
|
|
else: return 0
|
|
|
|
|
|
|
|
proc getStrVal*(js: JsonNode; default=""): string =
|
|
|
|
if js == nil: return default
|
|
|
|
js{"string_value"}.getStr(default)
|
|
|
|
|
|
|
|
proc getCardUrl*(js: JsonNode; kind: CardKind): string =
|
|
|
|
result = js{"website_url"}.getStrVal
|
|
|
|
if kind == promoVideoConvo:
|
|
|
|
result = js{"thank_you_url"}.getStrVal(result)
|
|
|
|
|
|
|
|
proc getCardDomain*(js: JsonNode; kind: CardKind): string =
|
|
|
|
result = js{"vanity_url"}.getStrVal(js{"domain"}.getStr)
|
|
|
|
if kind == promoVideoConvo:
|
|
|
|
result = js{"thank_you_vanity_url"}.getStrVal(result)
|
|
|
|
|
|
|
|
proc getCardTitle*(js: JsonNode; kind: CardKind): string =
|
|
|
|
result = js{"title"}.getStrVal
|
|
|
|
if kind == promoVideoConvo:
|
|
|
|
result = js{"thank_you_text"}.getStrVal(result)
|
|
|
|
|
|
|
|
proc getBanner*(js: JsonNode): string =
|
|
|
|
let url = js{"profile_banner_url"}.getStr
|
|
|
|
if url.len > 0:
|
|
|
|
return url & "/1500x500"
|
|
|
|
|
|
|
|
let color = js{"profile_link_color"}.getStr
|
|
|
|
if color.len > 0:
|
|
|
|
return '#' & color
|
|
|
|
|
|
|
|
# use primary color from profile picture color histrogram
|
|
|
|
with p, js{"profile_image_extensions", "mediaColor", "r", "ok", "palette"}:
|
|
|
|
if p.len > 0:
|
|
|
|
let pal = p[0]{"rgb"}.getFields
|
|
|
|
result = "#"
|
|
|
|
result.add toHex(pal["red"].getInt, 2)
|
|
|
|
result.add toHex(pal["green"].getInt, 2)
|
|
|
|
result.add toHex(pal["blue"].getInt, 2)
|
|
|
|
return
|
|
|
|
|
|
|
|
return "#161616"
|
|
|
|
|
|
|
|
proc getTombstone*(js: JsonNode): string =
|
|
|
|
let epitaph = js{"epitaph"}.getStr
|
|
|
|
case epitaph
|
|
|
|
of "Suspended":
|
|
|
|
result = "This tweet is from a suspended account."
|
|
|
|
of "Protected":
|
|
|
|
result = "This account owner limits who can view their tweets."
|
|
|
|
of "Missing":
|
|
|
|
result = "This tweet is unavailable."
|
2020-06-01 06:14:58 +00:00
|
|
|
of "Deactivated":
|
|
|
|
result = "This tweet is from an account that no longer exists."
|
2020-06-01 08:30:57 +00:00
|
|
|
of "Bounced", "BounceDeleted":
|
2020-06-01 06:14:58 +00:00
|
|
|
result = "This tweet violated the Twitter rules."
|
2020-06-01 00:16:24 +00:00
|
|
|
else:
|
|
|
|
result = js{"tombstoneInfo", "richText", "text"}.getStr
|
|
|
|
if epitaph.len > 0 or result.len > 0:
|
|
|
|
echo "Unknown tombstone (", epitaph, "): ", result
|
|
|
|
|
|
|
|
template getSlice(text: string; slice: seq[int]): string =
|
|
|
|
text.runeSubStr(slice[0], slice[1] - slice[0])
|
|
|
|
|
|
|
|
proc getSlice(text: string; js: JsonNode): string =
|
|
|
|
if js == nil or js.kind != JArray or js.len < 2 or
|
|
|
|
js[0].kind != JInt: return text
|
|
|
|
|
|
|
|
let slice = js.to(seq[int])
|
|
|
|
text.getSlice(slice)
|
|
|
|
|
|
|
|
proc expandUrl(text: var string; js: JsonNode; tLen: int; hideTwitter=false) =
|
|
|
|
let u = js{"url"}.getStr
|
|
|
|
if u.len == 0 or u notin text:
|
|
|
|
return
|
|
|
|
|
|
|
|
let
|
|
|
|
url = js{"expanded_url"}.getStr
|
|
|
|
slice = js{"indices"}.to(seq[int])
|
|
|
|
|
|
|
|
if hideTwitter and slice[1] >= tLen and url.isTwitterUrl:
|
|
|
|
text = text.replace(u, "")
|
|
|
|
text.removeSuffix(' ')
|
|
|
|
text.removeSuffix('\n')
|
|
|
|
else:
|
|
|
|
text = text.replace(u, a(shortLink(url), href=url))
|
|
|
|
|
|
|
|
proc expandTag(text: var string; js: JsonNode; prefix: char) =
|
|
|
|
let
|
|
|
|
tag = prefix & js{"text"}.getStr
|
|
|
|
html = a(tag, href=("/search?q=" & encodeUrl(tag)))
|
|
|
|
oldLen = text.len
|
|
|
|
|
|
|
|
text = text.replaceWord(tag, html)
|
|
|
|
|
|
|
|
# for edgecases with emojis or other characters around the tag
|
|
|
|
if text.len == oldLen:
|
|
|
|
text = text.replace(tag, html)
|
|
|
|
|
|
|
|
proc expandMention(text: var string; orig: string; js: JsonNode) =
|
|
|
|
let
|
|
|
|
name = js{"name"}.getStr
|
|
|
|
href = '/' & js{"screen_name"}.getStr
|
|
|
|
uname = orig.getSlice(js{"indices"})
|
|
|
|
text = text.replace(uname, a(uname, href=href, title=name))
|
|
|
|
|
|
|
|
proc expandProfileEntities*(profile: var Profile; js: JsonNode) =
|
|
|
|
let
|
|
|
|
orig = profile.bio
|
|
|
|
ent = ? js{"entities"}
|
|
|
|
|
|
|
|
with urls, ent{"url", "urls"}:
|
|
|
|
profile.website = urls[0]{"expanded_url"}.getStr
|
|
|
|
|
|
|
|
with urls, ent{"description", "urls"}:
|
|
|
|
for u in urls: profile.bio.expandUrl(u, orig.high)
|
|
|
|
|
|
|
|
profile.bio = profile.bio.replace(unRegex, unReplace)
|
|
|
|
.replace(htRegex, htReplace)
|
|
|
|
|
|
|
|
for mention in ? ent{"user_mentions"}:
|
|
|
|
profile.bio.expandMention(orig, mention)
|
|
|
|
|
|
|
|
proc expandTweetEntities*(tweet: Tweet; js: JsonNode) =
|
|
|
|
let
|
|
|
|
orig = tweet.text
|
|
|
|
slice = js{"display_text_range"}.to(seq[int])
|
|
|
|
hasQuote = js{"is_quote_status"}.getBool
|
|
|
|
hasCard = tweet.card.isSome
|
|
|
|
|
|
|
|
tweet.text = tweet.text.getSlice(slice)
|
|
|
|
|
|
|
|
var replyTo = ""
|
|
|
|
if tweet.replyId != 0:
|
|
|
|
with reply, js{"in_reply_to_screen_name"}:
|
|
|
|
tweet.reply.add reply.getStr
|
|
|
|
replyTo = reply.getStr
|
|
|
|
|
|
|
|
let ent = ? js{"entities"}
|
|
|
|
|
|
|
|
with urls, ent{"urls"}:
|
|
|
|
for u in urls:
|
|
|
|
tweet.text.expandUrl(u, slice[1], hasQuote)
|
|
|
|
if hasCard and u{"url"}.getStr == get(tweet.card).url:
|
|
|
|
get(tweet.card).url = u{"expanded_url"}.getStr
|
|
|
|
|
|
|
|
with media, ent{"media"}:
|
|
|
|
for m in media: tweet.text.expandUrl(m, slice[1], hideTwitter=true)
|
|
|
|
|
|
|
|
with hashes, ent{"hashtags"}:
|
|
|
|
for h in hashes: tweet.text.expandTag(h, '#')
|
|
|
|
|
|
|
|
with symbols, ent{"symbols"}:
|
|
|
|
for s in symbols: tweet.text.expandTag(s, '$')
|
|
|
|
|
|
|
|
for mention in ? ent{"user_mentions"}:
|
|
|
|
let
|
|
|
|
name = mention{"screen_name"}.getStr
|
|
|
|
idx = tweet.reply.find(name)
|
|
|
|
|
|
|
|
if mention{"indices"}[0].getInt >= slice[0]:
|
|
|
|
tweet.text.expandMention(orig, mention)
|
|
|
|
if idx > -1 and name != replyTo:
|
|
|
|
tweet.reply.delete idx
|
|
|
|
elif idx == -1 and tweet.replyId != 0:
|
|
|
|
tweet.reply.add name
|