diff --git a/src/parserutils.nim b/src/parserutils.nim index cdd003d..c79eb1e 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -169,13 +169,13 @@ proc getIntentStats*(profile: var Profile; node: XmlNode) = of "following": profile.following = text proc parseTweetStats*(node: XmlNode): TweetStats = - result = TweetStats(replies: "0", retweets: "0", likes: "0") + result = TweetStats() for action in node.selectAll(".ProfileTweet-actionCountForAria"): let text = action.innerText.split() case text[1][0 .. 2] - of "ret": result.retweets = text[0] - of "rep": result.replies = text[0] - of "lik": result.likes = text[0] + of "ret": result.retweets = text[0].parseInt + of "rep": result.replies = text[0].parseInt + of "lik": result.likes = text[0].parseInt proc parseTweetReply*(node: XmlNode): seq[string] = let reply = node.select(".ReplyingToContextBelowAuthor") diff --git a/src/routes/embed.nim b/src/routes/embed.nim index a4c4784..880775d 100644 --- a/src/routes/embed.nim +++ b/src/routes/embed.nim @@ -5,6 +5,7 @@ import jester import router_utils import ".."/[api, types, agents] import ../views/[embed] +export getVideo export embed diff --git a/src/routes/list.nim b/src/routes/list.nim index 5bcdd30..e449a59 100644 --- a/src/routes/list.nim +++ b/src/routes/list.nim @@ -5,6 +5,7 @@ import jester import router_utils import ".."/[query, types, api, agents] import ../views/[general, timeline, list] +export getListTimeline, getListMembers template respList*(list, timeline: typed) = if list.minId.len == 0: diff --git a/src/routes/media.nim b/src/routes/media.nim index 7299862..369d9e0 100644 --- a/src/routes/media.nim +++ b/src/routes/media.nim @@ -13,6 +13,9 @@ const m3u8Regex* = re"""url="(.+.m3u8)"""" proc createMediaRouter*(cfg: Config) = router media: + get "/pic/?": + resp Http404 + get "/pic/@url": cond "http" in @"url" cond "twimg" in @"url" diff --git a/src/routes/resolver.nim b/src/routes/resolver.nim index 0a215a2..deb9b86 100644 --- a/src/routes/resolver.nim +++ b/src/routes/resolver.nim @@ -5,6 +5,7 @@ import jester import router_utils import ".."/[query, types, api, agents] import ../views/general +export resolve template respResolved*(url, kind: string): untyped = let u = url diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index b89908a..1a3ab02 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -87,8 +87,9 @@ proc createTimelineRouter*(cfg: Config) = setProfileCacheTime(cfg.profileCacheTime) router timeline: - get "/@name/?@tab?": + get "/@name/?@tab?/?": cond '.' notin @"name" + cond @"name" notin ["pic", "gif", "video"] cond @"tab" in ["with_replies", "media", "search", ""] let prefs = cookiePrefs() diff --git a/src/types.nim b/src/types.nim index 0be934b..4ea8bf1 100644 --- a/src/types.nim +++ b/src/types.nim @@ -87,6 +87,8 @@ type tweetId*: string color*: string + PhotoRail* = seq[GalleryPhoto] + Poll* = object options*: seq[string] values*: seq[int] @@ -131,9 +133,9 @@ type id*: int64 TweetStats* = object - replies*: string - retweets*: string - likes*: string + replies*: int + retweets*: int + likes*: int Tweet* = ref object id*: int64 diff --git a/src/views/profile.nim b/src/views/profile.nim index 55fe819..aaf86c8 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -9,7 +9,7 @@ proc renderStat(num, class: string; text=""): VNode = buildHtml(li(class=class)): span(class="profile-stat-header"): text capitalizeAscii(t) span(class="profile-stat-num"): - text if num.len == 0: "?" else: num + text if num.len == 0: "?" else: insertSep(num, ',') proc renderProfileCard*(profile: Profile; prefs: Prefs): VNode = buildHtml(tdiv(class="profile-card")): @@ -57,7 +57,7 @@ proc renderProfileCard*(profile: Profile; prefs: Prefs): VNode = renderStat(profile.followers, "followers") renderStat(profile.likes, "likes") -proc renderPhotoRail(profile: Profile; photoRail: seq[GalleryPhoto]): VNode = +proc renderPhotoRail(profile: Profile; photoRail: PhotoRail): VNode = buildHtml(tdiv(class="photo-rail-card")): tdiv(class="photo-rail-header"): a(href=(&"/{profile.username}/media")): @@ -90,7 +90,7 @@ proc renderProtected(username: string): VNode = p: text &"Only confirmed followers have access to @{username}'s tweets." proc renderProfile*(profile: Profile; timeline: Timeline; - photoRail: seq[GalleryPhoto]; prefs: Prefs; path: string): VNode = + photoRail: PhotoRail; prefs: Prefs; path: string): VNode = timeline.query.fromUser = @[profile.username] buildHtml(tdiv(class="profile-tabs")): if not prefs.hideBanner: diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 6b8d2a6..f1061ed 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -110,7 +110,7 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode = proc renderGif(gif: Gif; prefs: Prefs): VNode = buildHtml(tdiv(class="attachments media-gif")): - tdiv(class="gallery-gif", style=style(maxHeight, "unset")): + tdiv(class="gallery-gif", style={maxHeight: "unset"}): tdiv(class="attachment"): let thumb = getPicUrl(gif.thumb) let url = getGifUrl(gif.url) @@ -124,14 +124,16 @@ proc renderGif(gif: Gif; prefs: Prefs): VNode = proc renderPoll(poll: Poll): VNode = buildHtml(tdiv(class="poll")): for i in 0 ..< poll.options.len: - let leader = if poll.leader == i: " leader" else: "" - let perc = $poll.values[i] & "%" + let + leader = if poll.leader == i: " leader" else: "" + perc = poll.values[i] / poll.votes * 100 + percStr = (&"{perc:>3.0f}").strip(chars={'.'}) & '%' tdiv(class=("poll-meter" & leader)): - span(class="poll-choice-bar", style=style(width, perc)) - span(class="poll-choice-value"): text perc + span(class="poll-choice-bar", style={width: percStr}) + span(class="poll-choice-value"): text percStr span(class="poll-choice-option"): text poll.options[i] span(class="poll-info"): - text $poll.votes & " votes • " & poll.status + text insertSep($poll.votes, ',') & " votes • " & poll.status proc renderCardImage(card: Card): VNode = buildHtml(tdiv(class="card-image-container")): @@ -169,11 +171,11 @@ proc renderCard(card: Card; prefs: Prefs; path: string): VNode = proc renderStats(stats: TweetStats; views: string): VNode = buildHtml(tdiv(class="tweet-stats")): - span(class="tweet-stat"): icon "comment", $stats.replies - span(class="tweet-stat"): icon "retweet", $stats.retweets - span(class="tweet-stat"): icon "heart", $stats.likes + span(class="tweet-stat"): icon "comment", insertSep($stats.replies, ',') + span(class="tweet-stat"): icon "retweet", insertSep($stats.retweets, ',') + span(class="tweet-stat"): icon "heart", insertSep($stats.likes, ',') if views.len > 0: - span(class="tweet-stat"): icon "play", views + span(class="tweet-stat"): icon "play", insertSep(views, ',') proc renderReply(tweet: Tweet): VNode = buildHtml(tdiv(class="replying-to")): @@ -279,7 +281,8 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; var views = "" renderHeader(tweet) - if index == 0 and tweet.reply.len > 0: + if index == 0 and tweet.reply.len > 0 and + (tweet.reply.len > 1 or tweet.reply[0] != tweet.profile.username): renderReply(tweet) tdiv(class="tweet-content media-body", dir="auto"): @@ -288,9 +291,6 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; if tweet.attribution.isSome: renderAttribution(tweet.attribution.get()) - if tweet.quote.isSome: - renderQuote(tweet.quote.get(), prefs) - if tweet.card.isSome: renderCard(tweet.card.get(), prefs, path) elif tweet.photos.len > 0: @@ -304,6 +304,9 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; elif tweet.poll.isSome: renderPoll(tweet.poll.get()) + if tweet.quote.isSome: + renderQuote(tweet.quote.get(), prefs) + if mainTweet: p(class="tweet-published"): text getTweetTime(tweet) diff --git a/tests/test_profile.py b/tests/test_profile.py index cf0b4f3..b07d32b 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -72,7 +72,7 @@ class ProfileTest(BaseTestCase): def test_suspended(self): self.open_nitter('test') - self.assert_text(f'User "test" has been suspended') + self.assert_text('User "test" has been suspended') @parameterized.expand(banner_color) def test_banner_color(self, username, color):