diff --git a/src/api.nim b/src/api.nim index d99eb3d..60af68d 100644 --- a/src/api.nim +++ b/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) diff --git a/src/consts.nim b/src/consts.nim index 184f9da..8dd1b14 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -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" diff --git a/src/parser.nim b/src/parser.nim index 5dc96df..f298160 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -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)) diff --git a/src/routes/rss.nim b/src/routes/rss.nim index 8eec399..d378396 100644 --- a/src/routes/rss.nim +++ b/src/routes/rss.nim @@ -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 diff --git a/src/routes/search.nim b/src/routes/search.nim index ed2c397..c270df5 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -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) diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index e62c9e0..ef3d012 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -56,8 +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: @[@[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 @@ -71,9 +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: @[@[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) diff --git a/src/tokens.nim b/src/tokens.nim index 6643de3..531f557 100644 --- a/src/tokens.nim +++ b/src/tokens.nim @@ -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