Implement Recommendations

This commit is contained in:
jackyzy823 2021-10-01 13:48:50 +02:00
parent 9d117aa15b
commit d95311bf8f
11 changed files with 119 additions and 10 deletions

View file

@ -98,6 +98,12 @@ proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
if after.len > 0:
result.replies = await getReplies(id, after)
proc getRecommendations*(id: string): Future[Recommendations] {.async.} =
let
ps = genParams({"user_id": id})
url = recommendations ? ps
result = parseRecommnedations(await fetch(url, oldApi=true))
proc resolve*(url: string; prefs: Prefs): Future[string] {.async.} =
let client = newAsyncHttpClient(maxRedirects=0)
try:

View file

@ -11,6 +11,7 @@ const
userShow* = api / "1.1/users/show.json"
photoRail* = api / "1.1/statuses/media_timeline.json"
search* = api / "2/search/adaptive.json"
recommendations* = api / "1.1/users/recommendations.json"
timelineApi = api / "2/timeline"
tweet* = timelineApi / "conversation"

View file

@ -449,6 +449,10 @@ proc parseTimeline*(js: JsonNode; after=""): Timeline =
elif "cursor-bottom" in entry:
result.bottom = e.getCursor
proc parseRecommnedations*(js: JsonNode): Recommendations =
for u in js:
result.add parseProfile(u{"user"})
proc parsePhotoRail*(js: JsonNode): PhotoRail =
for tweet in js:
let

View file

@ -74,6 +74,9 @@ proc cache*(data: List) {.async.} =
proc cache*(data: PhotoRail; name: string) {.async.} =
await setex("pr:" & name, baseCacheTime, compress(toFlatty(data)))
proc cache*(data: Recommendations; userId: string) {.async.} =
await setex("rc:" & userId, listCacheTime, compress(toFlatty(data)))
proc cache*(data: Profile) {.async.} =
if data.username.len == 0 or data.id.len == 0: return
let name = toLower(data.username)
@ -99,6 +102,15 @@ proc cacheRss*(query: string; rss: Rss) {.async.} =
discard await r.expire(key, rssCacheTime)
discard await r.flushPipeline()
proc getCachedRecommendations*(userId: string): Future[Recommendations] {.async.} =
if userId.len == 0: return
let recommendations = await get("rc:" & userId)
if recommendations != redisNil:
result = fromFlatty(uncompress(recommendations), Recommendations)
else:
result = await getRecommendations(userId)
await cache(result, userId)
proc getProfileId*(username: string): Future[string] {.async.} =
let name = toLower(username)
pool.withAcquire(r):

View file

@ -20,7 +20,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
if names.len == 1:
(profile, timeline) =
await fetchSingleTimeline(after, query, skipRail=true)
await fetchSingleTimeline(after, query, skipRail=true, skipRecommendations=true)
else:
var q = query
q.fromUser = names

View file

@ -19,8 +19,8 @@ proc getQuery*(request: Request; tab, name: string): Query =
of "search": initQuery(params(request), name=name)
else: Query(fromUser: @[name])
proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
Future[(Profile, Timeline, PhotoRail)] {.async.} =
proc fetchSingleTimeline*(after: string; query: Query; skipRail=false, skipRecommendations=false):
Future[(Profile, Timeline, PhotoRail, Recommendations)] {.async.} =
let name = query.fromUser[0]
var
@ -52,6 +52,13 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
else:
rail = getCachedPhotoRail(name)
var recommendations: Future[Recommendations]
if skipRecommendations:
recommendations = newFuture[Recommendations]()
recommendations.complete(@[])
else:
recommendations = getCachedRecommendations(profileId)
var timeline =
case query.kind
of posts: await getTimeline(profileId, after)
@ -76,7 +83,7 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
if fetched and not found:
await cache(profile)
return (profile, timeline, await rail)
return (profile, timeline, await rail, await recommendations)
proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
rss, after: string): Future[string] {.async.} =
@ -86,12 +93,12 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
html = renderTweetSearch(timeline, prefs, getPath())
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
var (p, t, r) = await fetchSingleTimeline(after, query)
var (p, t, r, rc) = await fetchSingleTimeline(after, query)
if p.suspended: return showError(getSuspended(p.username), cfg)
if p.id.len == 0: return
let pHtml = renderProfile(p, t, r, prefs, getPath())
let pHtml = renderProfile(p, t, r, rc, prefs, getPath())
result = renderMain(pHtml, request, cfg, prefs, pageTitle(p), pageDesc(p),
rss=rss, images = @[p.getUserpic("_400x400")],
banner=p.banner)
@ -139,7 +146,7 @@ proc createTimelineRouter*(cfg: Config) =
timeline.beginning = true
resp $renderTweetSearch(timeline, prefs, getPath())
else:
var (_, timeline, _) = await fetchSingleTimeline(after, query, skipRail=true)
var (_, timeline, _, _) = await fetchSingleTimeline(after, query, skipRail=true, skipRecommendations=true)
if timeline.content.len == 0: resp Http404
timeline.beginning = true
resp $renderTimelineTweets(timeline, prefs, getPath())

View file

@ -3,6 +3,7 @@
@import 'card';
@import 'photo-rail';
@import 'recommendations';
.profile-tabs {
@include panel(auto, 900px);

View file

@ -0,0 +1,59 @@
@import '_variables';
.recommendations {
&-card {
float: left;
background: var(--bg_panel);
border-radius: 0 0 4px 4px;
width: 100%;
margin: 5px 0;
}
&-header {
padding: 5px 12px 0;
}
&-header-mobile {
display: none;
box-sizing: border-box;
padding: 5px 12px 0;
width: 100%;
float: unset;
color: var(--accent);
justify-content: space-between;
}
}
@include create-toggle(recommendations-list, 640px);
#recommendations-list-toggle:checked ~ .recommendations-list {
padding-bottom: 12px;
}
@media(max-width: 600px) {
.recommendations-header {
display: none;
}
.recommendations-header-mobile {
display: flex;
}
.recommendations-list {
max-height: 0;
padding-bottom: 0;
overflow: scroll;
transition: max-height 0.4s;
}
}
@media(max-width: 600px) {
#recommendations-list-toggle:checked ~ .recommendations-list {
max-height: 160px;
}
}
@media(max-width: 450px) {
#recommendations-list-toggle:checked ~ .recommendations-list {
max-height: 160px;
}
}

View file

@ -99,6 +99,8 @@ type
PhotoRail* = seq[GalleryPhoto]
Recommendations* = seq[Profile]
Poll* = object
options*: seq[string]
values*: seq[int]

View file

@ -2,7 +2,7 @@
import strutils, strformat
import karax/[karaxdsl, vdom, vstyles]
import renderutils, search
import renderutils, search, timeline
import ".."/[types, utils, formatters]
proc renderStat(num, class: string; text=""): VNode =
@ -81,6 +81,20 @@ proc renderPhotoRail(profile: Profile; photoRail: PhotoRail): VNode =
style={backgroundColor: col}):
genImg(photo.url & (if "format" in photo.url: "" else: ":thumb"))
proc renderRecommendations(recommendations: Recommendations; prefs: Prefs): VNode =
buildHtml(tdiv(class="recommendations-card")):
tdiv(class="recommendations-header"):
span: text "You might like"
input(id="recommendations-list-toggle", `type`="checkbox")
label(`for`="recommendations-list-toggle", class="recommendations-header-mobile"):
span: text "You might like"
icon "down"
tdiv(class="recommendations-list"):
for i, recommendation in recommendations:
renderUser(recommendation, prefs)
proc renderBanner(profile: Profile): VNode =
buildHtml():
if "#" in profile.banner:
@ -96,7 +110,7 @@ proc renderProtected(username: string): VNode =
p: text &"Only confirmed followers have access to @{username}'s tweets."
proc renderProfile*(profile: Profile; timeline: var Timeline;
photoRail: PhotoRail; prefs: Prefs; path: string): VNode =
photoRail: PhotoRail; recommendations: Recommendations; prefs: Prefs; path: string): VNode =
timeline.query.fromUser = @[profile.username]
buildHtml(tdiv(class="profile-tabs")):
if not prefs.hideBanner:
@ -108,6 +122,9 @@ proc renderProfile*(profile: Profile; timeline: var Timeline;
renderProfileCard(profile, prefs)
if photoRail.len > 0:
renderPhotoRail(profile, photoRail)
if recommendations.len > 0:
renderRecommendations(recommendations, prefs)
if profile.protected:
renderProtected(profile.username)

View file

@ -57,7 +57,7 @@ proc threadFilter(tweets: openArray[Tweet]; threads: openArray[int64]; it: Tweet
elif t.replyId == result[0].id:
result.add t
proc renderUser(user: Profile; prefs: Prefs): VNode =
proc renderUser*(user: Profile; prefs: Prefs): VNode =
buildHtml(tdiv(class="timeline-item")):
a(class="tweet-link", href=("/" & user.username))
tdiv(class="tweet-body profile-result"):