Merge branch 'master' of https://github.com/zedeus/nitter
このコミットが含まれているのは:
コミット
a253798b94
16
src/api.nim
16
src/api.nim
|
@ -115,6 +115,22 @@ proc getGraphSearch*(query: Query; after=""): Future[Profile] {.async.} =
|
|||
result = Profile(tweets: parseGraphSearch(await fetch(url, Api.search), after))
|
||||
result.tweets.query = query
|
||||
|
||||
proc getTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} =
|
||||
let q = genQueryParam(query)
|
||||
if q.len == 0 or q == emptyQuery:
|
||||
return Timeline(query: query, beginning: true)
|
||||
|
||||
let url = tweetSearch ? genParams({
|
||||
"q": q,
|
||||
"tweet_search_mode": "live",
|
||||
"max_id": after
|
||||
})
|
||||
|
||||
result = parseTweetSearch(await fetch(url, Api.search))
|
||||
result.query = query
|
||||
if after.len == 0:
|
||||
result.beginning = true
|
||||
|
||||
proc getUserSearch*(query: Query; page="1"): Future[Result[User]] {.async.} =
|
||||
if query.text.len == 0:
|
||||
return Result[User](query: query, beginning: true)
|
||||
|
|
|
@ -9,6 +9,7 @@ const
|
|||
|
||||
photoRail* = api / "1.1/statuses/media_timeline.json"
|
||||
userSearch* = api / "1.1/users/search.json"
|
||||
tweetSearch* = api / "1.1/search/tweets.json"
|
||||
|
||||
graphql = api / "graphql"
|
||||
graphUser* = graphql / "u7wQyGi6oExe8_TRWGMq4Q/UserResultByScreenNameQuery"
|
||||
|
|
|
@ -87,7 +87,7 @@ routes:
|
|||
|
||||
error BadClientError:
|
||||
echo error.exc.name, ": ", error.exc.msg
|
||||
resp Http500, showError("Network error occured, please try again.", cfg)
|
||||
resp Http500, showError("Network error occurred, please try again.", cfg)
|
||||
|
||||
error RateLimitError:
|
||||
const link = a("another instance", href = instancesUrl)
|
||||
|
|
|
@ -82,12 +82,16 @@ proc parseVideo(js: JsonNode): Video =
|
|||
result = Video(
|
||||
thumb: js{"media_url_https"}.getImageStr,
|
||||
views: js{"ext", "mediaStats", "r", "ok", "viewCount"}.getStr($js{"mediaStats", "viewCount"}.getInt),
|
||||
available: js{"ext_media_availability", "status"}.getStr.toLowerAscii == "available",
|
||||
available: true,
|
||||
title: js{"ext_alt_text"}.getStr,
|
||||
durationMs: js{"video_info", "duration_millis"}.getInt
|
||||
# playbackType: mp4
|
||||
)
|
||||
|
||||
with status, js{"ext_media_availability", "status"}:
|
||||
if status.getStr.len > 0 and status.getStr.toLowerAscii != "available":
|
||||
result.available = false
|
||||
|
||||
with title, js{"additional_media_info", "title"}:
|
||||
result.title = title.getStr
|
||||
|
||||
|
@ -219,7 +223,9 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet =
|
|||
if result.hasThread and result.threadId == 0:
|
||||
result.threadId = js{"self_thread", "id_str"}.getId
|
||||
|
||||
if js{"is_quote_status"}.getBool:
|
||||
if "retweeted_status" in js:
|
||||
result.retweet = some Tweet()
|
||||
elif js{"is_quote_status"}.getBool:
|
||||
result.quote = some Tweet(id: js{"quoted_status_id_str"}.getId)
|
||||
|
||||
# legacy
|
||||
|
@ -281,6 +287,30 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet =
|
|||
result.text.removeSuffix(" Learn more.")
|
||||
result.available = false
|
||||
|
||||
proc parseLegacyTweet(js: JsonNode): Tweet =
|
||||
result = parseTweet(js, js{"card"})
|
||||
if not result.isNil and result.available:
|
||||
result.user = parseUser(js{"user"})
|
||||
|
||||
if result.quote.isSome:
|
||||
result.quote = some parseLegacyTweet(js{"quoted_status"})
|
||||
|
||||
proc parseTweetSearch*(js: JsonNode): Timeline =
|
||||
if js.kind == JNull or "statuses" notin js:
|
||||
return Timeline(beginning: true)
|
||||
|
||||
for tweet in js{"statuses"}:
|
||||
let parsed = parseLegacyTweet(tweet)
|
||||
|
||||
if parsed.retweet.isSome:
|
||||
parsed.retweet = some parseLegacyTweet(tweet{"retweeted_status"})
|
||||
|
||||
result.content.add @[parsed]
|
||||
|
||||
let cursor = js{"search_metadata", "next_results"}.getStr
|
||||
if cursor.len > 0 and "max_id" in cursor:
|
||||
result.bottom = cursor[cursor.find("=") + 1 .. cursor.find("&q=")]
|
||||
|
||||
proc finalizeTweet(global: GlobalObjects; id: string): Tweet =
|
||||
let intId = if id.len > 0: parseBiggestInt(id) else: 0
|
||||
result = global.tweets.getOrDefault(id, Tweet(id: intId))
|
||||
|
@ -357,6 +387,10 @@ proc parseTimeline*(js: JsonNode; after=""): Timeline =
|
|||
result.top = cursor{"value"}.getStr
|
||||
|
||||
proc parsePhotoRail*(js: JsonNode): PhotoRail =
|
||||
with error, js{"error"}:
|
||||
if error.getStr == "Not authorized.":
|
||||
return
|
||||
|
||||
for tweet in js:
|
||||
let
|
||||
t = parseTweet(tweet, js{"tweet_card"})
|
||||
|
@ -486,7 +520,7 @@ proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile =
|
|||
result.tweets.content.add tweet
|
||||
elif "-conversation-" in entryId or entryId.startsWith("homeConversation"):
|
||||
let (thread, self) = parseGraphThread(e)
|
||||
result.tweets.content.add thread
|
||||
result.tweets.content.add thread.content
|
||||
elif entryId.startsWith("cursor-bottom"):
|
||||
result.tweets.bottom = e{"content", "value"}.getStr
|
||||
if after.len == 0 and i{"__typename"}.getStr == "TimelinePinEntry":
|
||||
|
|
|
@ -27,7 +27,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
|
|||
else:
|
||||
var q = query
|
||||
q.fromUser = names
|
||||
profile = await getGraphSearch(q, after)
|
||||
profile.tweets = await getTweetSearch(q, after)
|
||||
# this is kinda dumb
|
||||
profile.user = User(
|
||||
username: name,
|
||||
|
@ -59,29 +59,29 @@ template respRss*(rss, page) =
|
|||
|
||||
proc createRssRouter*(cfg: Config) =
|
||||
router rss:
|
||||
# get "/search/rss":
|
||||
# cond cfg.enableRss
|
||||
# if @"q".len > 200:
|
||||
# resp Http400, showError("Search input too long.", cfg)
|
||||
get "/search/rss":
|
||||
cond cfg.enableRss
|
||||
if @"q".len > 200:
|
||||
resp Http400, showError("Search input too long.", cfg)
|
||||
|
||||
# let query = initQuery(params(request))
|
||||
# if query.kind != tweets:
|
||||
# resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg)
|
||||
let query = initQuery(params(request))
|
||||
if query.kind != tweets:
|
||||
resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg)
|
||||
|
||||
# let
|
||||
# cursor = getCursor()
|
||||
# key = redisKey("search", $hash(genQueryUrl(query)), cursor)
|
||||
let
|
||||
cursor = getCursor()
|
||||
key = redisKey("search", $hash(genQueryUrl(query)), cursor)
|
||||
|
||||
# var rss = await getCachedRss(key)
|
||||
# if rss.cursor.len > 0:
|
||||
# respRss(rss, "Search")
|
||||
var rss = await getCachedRss(key)
|
||||
if rss.cursor.len > 0:
|
||||
respRss(rss, "Search")
|
||||
|
||||
# let tweets = await getGraphSearch(query, cursor)
|
||||
# rss.cursor = tweets.bottom
|
||||
# rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg)
|
||||
let tweets = await getTweetSearch(query, cursor)
|
||||
rss.cursor = tweets.bottom
|
||||
rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg)
|
||||
|
||||
# await cacheRss(key, rss)
|
||||
# respRss(rss, "Search")
|
||||
await cacheRss(key, rss)
|
||||
respRss(rss, "Search")
|
||||
|
||||
get "/@name/rss":
|
||||
cond cfg.enableRss
|
||||
|
|
|
@ -34,15 +34,11 @@ proc createSearchRouter*(cfg: Config) =
|
|||
users = Result[User](beginning: true, query: query)
|
||||
resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title)
|
||||
of tweets:
|
||||
# let
|
||||
# tweets = await getGraphSearch(query, getCursor())
|
||||
# rss = "/search/rss?" & genQueryUrl(query)
|
||||
# resp renderMain(renderTweetSearch(tweets, prefs, getPath()),
|
||||
# request, cfg, prefs, title, rss=rss)
|
||||
var fakeTimeline = Timeline(beginning: true)
|
||||
fakeTimeline.content.add Tweet(tombstone: "Tweet search is unavailable for now")
|
||||
|
||||
resp renderMain(renderTweetSearch(fakeTimeline, prefs, getPath()), request, cfg, prefs, title)
|
||||
let
|
||||
tweets = await getTweetSearch(query, getCursor())
|
||||
rss = "/search/rss?" & genQueryUrl(query)
|
||||
resp renderMain(renderTweetSearch(tweets, prefs, getPath()),
|
||||
request, cfg, prefs, title, rss=rss)
|
||||
else:
|
||||
resp Http404, showError("Invalid search", cfg)
|
||||
|
||||
|
|
|
@ -56,10 +56,7 @@ proc fetchProfile*(after: string; query: Query; skipRail=false;
|
|||
of posts: await getGraphUserTweets(userId, TimelineKind.tweets, after)
|
||||
of replies: await getGraphUserTweets(userId, TimelineKind.replies, after)
|
||||
of media: await getGraphUserTweets(userId, TimelineKind.media, after)
|
||||
else: Profile(tweets: Timeline(beginning: true, content: @[Chain(content:
|
||||
@[Tweet(tombstone: "Tweet search is unavailable for now")]
|
||||
)]))
|
||||
# else: await getGraphSearch(query, after)
|
||||
else: Profile(tweets: await getTweetSearch(query, after))
|
||||
|
||||
result.user = await user
|
||||
result.photoRail = await rail
|
||||
|
@ -73,11 +70,8 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
|||
rss, after: string): Future[string] {.async.} =
|
||||
if query.fromUser.len != 1:
|
||||
let
|
||||
# timeline = await getGraphSearch(query, after)
|
||||
timeline = Profile(tweets: Timeline(beginning: true, content: @[Chain(content:
|
||||
@[Tweet(tombstone: "This features is unavailable for now")]
|
||||
)]))
|
||||
html = renderTweetSearch(timeline.tweets, prefs, getPath())
|
||||
timeline = await getTweetSearch(query, after)
|
||||
html = renderTweetSearch(timeline, prefs, getPath())
|
||||
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
|
||||
|
||||
var profile = await fetchProfile(after, query, skipPinned=prefs.hidePins)
|
||||
|
|
|
@ -41,9 +41,9 @@ proc getPoolJson*(): JsonNode =
|
|||
let
|
||||
maxReqs =
|
||||
case api
|
||||
of Api.timeline: 180
|
||||
of Api.timeline, Api.search: 180
|
||||
of Api.userTweets, Api.userTweetsAndReplies, Api.userRestId,
|
||||
Api.userScreenName, Api.tweetDetail, Api.tweetResult, Api.search: 500
|
||||
Api.userScreenName, Api.tweetDetail, Api.tweetResult: 500
|
||||
of Api.list, Api.listTweets, Api.listMembers, Api.listBySlug, Api.userMedia: 500
|
||||
of Api.userSearch: 900
|
||||
reqs = maxReqs - token.apis[api].remaining
|
||||
|
|
|
@ -205,6 +205,8 @@ type
|
|||
video*: Option[Video]
|
||||
photos*: seq[string]
|
||||
|
||||
Tweets* = seq[Tweet]
|
||||
|
||||
Result*[T] = object
|
||||
content*: seq[T]
|
||||
top*, bottom*: string
|
||||
|
@ -212,7 +214,7 @@ type
|
|||
query*: Query
|
||||
|
||||
Chain* = object
|
||||
content*: seq[Tweet]
|
||||
content*: Tweets
|
||||
hasMore*: bool
|
||||
cursor*: string
|
||||
|
||||
|
@ -222,7 +224,7 @@ type
|
|||
after*: Chain
|
||||
replies*: Result[Chain]
|
||||
|
||||
Timeline* = Result[Chain]
|
||||
Timeline* = Result[Tweets]
|
||||
|
||||
Profile* = object
|
||||
user*: User
|
||||
|
@ -287,5 +289,5 @@ type
|
|||
proc contains*(thread: Chain; tweet: Tweet): bool =
|
||||
thread.content.anyIt(it.id == tweet.id)
|
||||
|
||||
proc add*(timeline: var seq[Chain]; tweet: Tweet) =
|
||||
timeline.add Chain(content: @[tweet])
|
||||
proc add*(timeline: var seq[Tweets]; tweet: Tweet) =
|
||||
timeline.add @[tweet]
|
||||
|
|
|
@ -11,7 +11,7 @@ const doctype = "<!DOCTYPE html>\n"
|
|||
proc renderVideoEmbed*(tweet: Tweet; cfg: Config; req: Request): string =
|
||||
let thumb = get(tweet.video).thumb
|
||||
let vidUrl = getVideoEmbed(cfg, tweet.id)
|
||||
let prefs = Prefs(hlsPlayback: true)
|
||||
let prefs = Prefs(hlsPlayback: true, mp4Playback: true)
|
||||
let node = buildHtml(html(lang="en")):
|
||||
renderHead(prefs, cfg, req, video=vidUrl, images=(@[thumb]))
|
||||
|
||||
|
|
|
@ -56,16 +56,16 @@ Twitter feed for: ${desc}. Generated by ${cfg.hostname}
|
|||
#end if
|
||||
#end proc
|
||||
#
|
||||
#proc renderRssTweets(tweets: seq[Chain]; cfg: Config; userId=""): string =
|
||||
#proc renderRssTweets(tweets: seq[Tweets]; cfg: Config; userId=""): string =
|
||||
#let urlPrefix = getUrlPrefix(cfg)
|
||||
#var links: seq[string]
|
||||
#for c in tweets:
|
||||
# for t in c.content:
|
||||
# if userId.len > 0 and t.user.id != userId: continue
|
||||
#for thread in tweets:
|
||||
# for tweet in thread:
|
||||
# if userId.len > 0 and tweet.user.id != userId: continue
|
||||
# end if
|
||||
#
|
||||
# let retweet = if t.retweet.isSome: t.user.username else: ""
|
||||
# let tweet = if retweet.len > 0: t.retweet.get else: t
|
||||
# let retweet = if tweet.retweet.isSome: tweet.user.username else: ""
|
||||
# let tweet = if retweet.len > 0: tweet.retweet.get else: tweet
|
||||
# let link = getLink(tweet)
|
||||
# if link in links: continue
|
||||
# end if
|
||||
|
@ -113,7 +113,7 @@ ${renderRssTweets(profile.tweets.content, cfg, userId=profile.user.id)}
|
|||
</rss>
|
||||
#end proc
|
||||
#
|
||||
#proc renderListRss*(tweets: seq[Chain]; list: List; cfg: Config): string =
|
||||
#proc renderListRss*(tweets: seq[Tweets]; list: List; cfg: Config): string =
|
||||
#let link = &"{getUrlPrefix(cfg)}/i/lists/{list.id}"
|
||||
#result = ""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
@ -130,7 +130,7 @@ ${renderRssTweets(tweets, cfg)}
|
|||
</rss>
|
||||
#end proc
|
||||
#
|
||||
#proc renderSearchRss*(tweets: seq[Chain]; name, param: string; cfg: Config): string =
|
||||
#proc renderSearchRss*(tweets: seq[Tweets]; name, param: string; cfg: Config): string =
|
||||
#let link = &"{getUrlPrefix(cfg)}/search"
|
||||
#let escName = xmltree.escape(name)
|
||||
#result = ""
|
||||
|
|
|
@ -39,7 +39,7 @@ proc renderNoneFound(): VNode =
|
|||
h2(class="timeline-none"):
|
||||
text "何も見つけられませんでした"
|
||||
|
||||
proc renderThread(thread: seq[Tweet]; prefs: Prefs; path: string): VNode =
|
||||
proc renderThread(thread: Tweets; prefs: Prefs; path: string): VNode =
|
||||
buildHtml(tdiv(class="thread-line")):
|
||||
let sortedThread = thread.sortedByIt(it.id)
|
||||
for i, tweet in sortedThread:
|
||||
|
@ -106,9 +106,9 @@ proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string;
|
|||
var retweets: seq[int64]
|
||||
|
||||
for thread in results.content:
|
||||
if thread.content.len == 1:
|
||||
if thread.len == 1:
|
||||
let
|
||||
tweet = thread.content[0]
|
||||
tweet = thread[0]
|
||||
retweetId = if tweet.retweet.isSome: get(tweet.retweet).id else: 0
|
||||
|
||||
if retweetId in retweets or tweet.id in retweets or
|
||||
|
@ -121,7 +121,7 @@ proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string;
|
|||
hasThread = get(tweet.retweet).hasThread
|
||||
renderTweet(tweet, prefs, path, showThread=hasThread)
|
||||
else:
|
||||
renderThread(thread.content, prefs, path)
|
||||
renderThread(thread, prefs, path)
|
||||
|
||||
if results.bottom.len > 0:
|
||||
renderMore(results.query, results.bottom)
|
||||
|
|
新しいイシューから参照