Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added favorites endpoint and added likes tab to profile pages #825

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions nitter.example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ tokenCount = 10
# always at least `tokenCount` usable tokens. only increase this if you receive
# major bursts all the time and don't have a rate limiting setup via e.g. nginx

#cookieHeader = "ct0=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX; auth_token=XXXXXXXXXXXXXXXXXXXXXXX" # authentication cookie of a logged in account, required for the likes tab
#xCsrfToken = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # required for the likes tab

# Change default preferences here, see src/prefs_impl.nim for a complete list
[Preferences]
theme = "Nitter"
Expand Down
11 changes: 11 additions & 0 deletions src/api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.}
let url = graphListMembers ? {"variables": $variables, "features": gqlFeatures}
result = parseGraphListMembers(await fetchRaw(url, Api.listMembers), after)

proc getFavorites*(id: string; cfg: Config; after=""): Future[Timeline] {.async.} =
if id.len == 0: return
let
ps = genParams({"userId": id}, after)
url = consts.favorites / (id & ".json") ? ps
headers = newHttpHeaders({
"Cookie": cfg.cookieHeader,
"x-csrf-token": cfg.xCsrfToken
})
result = parseTimeline(await fetch(url, Api.favorites, headers), after)

proc getGraphTweetResult*(id: string): Future[Tweet] {.async.} =
if id.len == 0: return
let
Expand Down
15 changes: 9 additions & 6 deletions src/apiutils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ template updateToken() =
reset = parseInt(resp.headers[rlReset])
token.setRateLimit(api, remaining, reset)

template fetchImpl(result, fetchBody) {.dirty.} =
template fetchImpl(result, additional_headers, fetchBody) {.dirty.} =
once:
pool = HttpPool()

Expand All @@ -60,7 +60,10 @@ template fetchImpl(result, fetchBody) {.dirty.} =

try:
var resp: AsyncResponse
pool.use(genHeaders(token)):
var headers = genHeaders(token)
for key, value in additional_headers.pairs():
headers.add(key, value)
pool.use(headers):
template getContent =
resp = await c.get($url)
result = await resp.body
Expand Down Expand Up @@ -94,9 +97,9 @@ template fetchImpl(result, fetchBody) {.dirty.} =
release(token, invalid=true)
raise rateLimitError()

proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} =
proc fetch*(url: Uri; api: Api; additional_headers: HttpHeaders = newHttpHeaders()): Future[JsonNode] {.async.} =
var body: string
fetchImpl body:
fetchImpl(body, additional_headers):
if body.startsWith('{') or body.startsWith('['):
result = parseJson(body)
else:
Expand All @@ -111,8 +114,8 @@ proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} =
release(token, invalid=true)
raise rateLimitError()

proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} =
fetchImpl result:
proc fetchRaw*(url: Uri; api: Api; additional_headers: HttpHeaders = newHttpHeaders()): Future[string] {.async.} =
fetchImpl(result, additional_headers):
if not (result.startsWith('{') or result.startsWith('[')):
echo resp.status, ": ", result, " --- url: ", url
result.setLen(0)
Expand Down
4 changes: 3 additions & 1 deletion src/config.nim
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ proc getConfig*(path: string): (Config, parseCfg.Config) =
enableRss: cfg.get("Config", "enableRSS", true),
enableDebug: cfg.get("Config", "enableDebug", false),
proxy: cfg.get("Config", "proxy", ""),
proxyAuth: cfg.get("Config", "proxyAuth", "")
proxyAuth: cfg.get("Config", "proxyAuth", ""),
cookieHeader: cfg.get("Config", "cookieHeader", ""),
xCsrfToken: cfg.get("Config", "xCsrfToken", "")
)

return (conf, cfg)
3 changes: 3 additions & 0 deletions src/consts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const
activate* = $(api / "1.1/guest/activate.json")

photoRail* = api / "1.1/statuses/media_timeline.json"

timelineApi = api / "2/timeline"
favorites* = timelineApi / "favorites"
userSearch* = api / "1.1/users/search.json"

graphql = api / "graphql"
Expand Down
7 changes: 7 additions & 0 deletions src/query.nim
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ proc getMediaQuery*(name: string): Query =
sep: "OR"
)


proc getFavoritesQuery*(name: string): Query =
Query(
kind: favorites,
fromUser: @[name]
)

proc getReplyQuery*(name: string): Query =
Query(
kind: replies,
Expand Down
5 changes: 3 additions & 2 deletions src/routes/rss.nim
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
names = getNames(name)

if names.len == 1:
profile = await fetchProfile(after, query, skipRail=true, skipPinned=true)
profile = await fetchProfile(after, query, cfg, skipRail=true, skipPinned=true)
else:
var q = query
q.fromUser = names
Expand Down Expand Up @@ -104,14 +104,15 @@ proc createRssRouter*(cfg: Config) =
get "/@name/@tab/rss":
cond cfg.enableRss
cond '.' notin @"name"
cond @"tab" in ["with_replies", "media", "search"]
cond @"tab" in ["with_replies", "media", "favorites", "search"]
let
name = @"name"
tab = @"tab"
query =
case tab
of "with_replies": getReplyQuery(name)
of "media": getMediaQuery(name)
of "favorites": getFavoritesQuery(name)
of "search": initQuery(params(request), name=name)
else: Query(fromUser: @[name])

Expand Down
2 changes: 1 addition & 1 deletion src/routes/search.nim
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ proc createSearchRouter*(cfg: Config) =
let
tweets = await getGraphSearch(query, getCursor())
rss = "/search/rss?" & genQueryUrl(query)
resp renderMain(renderTweetSearch(tweets, prefs, getPath()),
resp renderMain(renderTweetSearch(tweets, cfg, prefs, getPath()),
request, cfg, prefs, title, rss=rss)
else:
resp Http404, showError("Invalid search", cfg)
Expand Down
16 changes: 9 additions & 7 deletions src/routes/timeline.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ proc getQuery*(request: Request; tab, name: string): Query =
case tab
of "with_replies": getReplyQuery(name)
of "media": getMediaQuery(name)
of "favorites": getFavoritesQuery(name)
of "search": initQuery(params(request), name=name)
else: Query(fromUser: @[name])

Expand All @@ -27,7 +28,7 @@ template skipIf[T](cond: bool; default; body: Future[T]): Future[T] =
else:
body

proc fetchProfile*(after: string; query: Query; skipRail=false;
proc fetchProfile*(after: string; query: Query; cfg: Config; skipRail=false;
skipPinned=false): Future[Profile] {.async.} =
let
name = query.fromUser[0]
Expand All @@ -50,6 +51,7 @@ proc fetchProfile*(after: string; query: Query; skipRail=false;
of posts: getGraphUserTweets(userId, TimelineKind.tweets, after)
of replies: getGraphUserTweets(userId, TimelineKind.replies, after)
of media: getGraphUserTweets(userId, TimelineKind.media, after)
of favorites: getFavorites(userId, cfg, after)
else: getGraphSearch(query, after)

rail =
Expand Down Expand Up @@ -84,18 +86,18 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
if query.fromUser.len != 1:
let
timeline = await getGraphSearch(query, after)
html = renderTweetSearch(timeline, prefs, getPath())
html = renderTweetSearch(timeline, cfg, prefs, getPath())
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)

var profile = await fetchProfile(after, query, skipPinned=prefs.hidePins)
var profile = await fetchProfile(after, query, cfg, skipPinned=prefs.hidePins)
template u: untyped = profile.user

if u.suspended:
return showError(getSuspended(u.username), cfg)

if profile.user.id.len == 0: return

let pHtml = renderProfile(profile, prefs, getPath())
let pHtml = renderProfile(profile, cfg, prefs, getPath())
result = renderMain(pHtml, request, cfg, prefs, pageTitle(u), pageDesc(u),
rss=rss, images = @[u.getUserPic("_400x400")],
banner=u.banner)
Expand Down Expand Up @@ -125,7 +127,7 @@ proc createTimelineRouter*(cfg: Config) =
get "/@name/?@tab?/?":
cond '.' notin @"name"
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"]
cond @"tab" in ["with_replies", "media", "search", ""]
cond @"tab" in ["with_replies", "media", "search", "favorites", ""]
let
prefs = cookiePrefs()
after = getCursor()
Expand All @@ -141,9 +143,9 @@ proc createTimelineRouter*(cfg: Config) =
var timeline = await getGraphSearch(query, after)
if timeline.content.len == 0: resp Http404
timeline.beginning = true
resp $renderTweetSearch(timeline, prefs, getPath())
resp $renderTweetSearch(timeline, cfg, prefs, getPath())
else:
var profile = await fetchProfile(after, query, skipRail=true)
var profile = await fetchProfile(after, query, cfg, skipRail=true)
if profile.tweets.content.len == 0: resp Http404
profile.tweets.beginning = true
resp $renderTimelineTweets(profile.tweets, prefs, getPath())
Expand Down
2 changes: 1 addition & 1 deletion src/tokens.nim
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ proc getPoolJson*(): JsonNode =
of Api.listMembers, Api.listBySlug, Api.list, Api.listTweets,
Api.userTweets, Api.userTweetsAndReplies, Api.userMedia,
Api.userRestId, Api.userScreenName,
Api.tweetDetail, Api.tweetResult, Api.search: 500
Api.tweetDetail, Api.tweetResult, Api.search, Api.favorites: 500
of Api.userSearch: 900
reqs = maxReqs - token.apis[api].remaining

Expand Down
6 changes: 5 additions & 1 deletion src/types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type
listTweets
userRestId
userScreenName
favorites
userTweets
userTweetsAndReplies
userMedia
Expand Down Expand Up @@ -106,7 +107,7 @@ type
variants*: seq[VideoVariant]

QueryKind* = enum
posts, replies, media, users, tweets, userList
posts, replies, media, users, tweets, userList, favorites

Query* = object
kind*: QueryKind
Expand Down Expand Up @@ -269,6 +270,9 @@ type
redisMaxConns*: int
redisPassword*: string

cookieHeader*: string
xCsrfToken*: string

Rss* = object
feed*, cursor*: string

Expand Down
4 changes: 2 additions & 2 deletions src/views/profile.nim
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ proc renderProtected(username: string): VNode =
h2: text "This account's tweets are protected."
p: text &"Only confirmed followers have access to @{username}'s tweets."

proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode =
proc renderProfile*(profile: var Profile; cfg: Config; prefs: Prefs; path: string): VNode =
profile.tweets.query.fromUser = @[profile.user.username]

buildHtml(tdiv(class="profile-tabs")):
Expand All @@ -116,4 +116,4 @@ proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode =
if profile.user.protected:
renderProtected(profile.user.username)
else:
renderTweetSearch(profile.tweets, prefs, path, profile.pinned)
renderTweetSearch(profile.tweets, cfg, prefs, path, profile.pinned)
9 changes: 6 additions & 3 deletions src/views/search.nim
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ proc renderSearch*(): VNode =
placeholder="Enter username...", dir="auto")
button(`type`="submit"): icon "search"

proc renderProfileTabs*(query: Query; username: string): VNode =
proc renderProfileTabs*(query: Query; username: string; cfg: Config): VNode =
let link = "/" & username
buildHtml(ul(class="tab")):
li(class=query.getTabClass(posts)):
Expand All @@ -38,6 +38,9 @@ proc renderProfileTabs*(query: Query; username: string): VNode =
a(href=(link & "/with_replies")): text "Tweets & Replies"
li(class=query.getTabClass(media)):
a(href=(link & "/media")): text "Media"
if len(cfg.xCsrfToken) != 0 and len(cfg.cookieHeader) != 0:
li(class=query.getTabClass(favorites)):
a(href=(link & "/favorites")): text "Likes"
li(class=query.getTabClass(tweets)):
a(href=(link & "/search")): text "Search"

Expand Down Expand Up @@ -88,7 +91,7 @@ proc renderSearchPanel*(query: Query): VNode =
span(class="search-title"): text "Near"
genInput("near", "", query.near, "Location...", autofocus=false)

proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string;
proc renderTweetSearch*(results: Result[Tweet]; cfg: Config; prefs: Prefs; path: string;
pinned=none(Tweet)): VNode =
let query = results.query
buildHtml(tdiv(class="timeline-container")):
Expand All @@ -97,7 +100,7 @@ proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string;
text query.fromUser.join(" | ")

if query.fromUser.len > 0:
renderProfileTabs(query, query.fromUser.join(","))
renderProfileTabs(query, query.fromUser.join(","), cfg)

if query.fromUser.len == 0 or query.kind == tweets:
tdiv(class="timeline-header"):
Expand Down