Fix everything (#927)
* Switch bearer token and endpoints, update parser * Enable user search, disable tweet search * Disable multi-user timelines for now * Fix parsing of pinned tombstone
This commit is contained in:
		
							parent
							
								
									dcf73354ff
								
							
						
					
					
						commit
						0bc3c153d9
					
				
					 20 changed files with 260 additions and 264 deletions
				
			
		
							
								
								
									
										26
									
								
								src/api.nim
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								src/api.nim
									
										
									
									
									
								
							| 
						 | 
					@ -7,20 +7,20 @@ import experimental/parser as newParser
 | 
				
			||||||
proc getGraphUser*(username: string): Future[User] {.async.} =
 | 
					proc getGraphUser*(username: string): Future[User] {.async.} =
 | 
				
			||||||
  if username.len == 0: return
 | 
					  if username.len == 0: return
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
    variables = %*{"screen_name": username}
 | 
					    variables = """{"screen_name": "$1"}""" % username
 | 
				
			||||||
    params = {"variables": $variables, "features": gqlFeatures}
 | 
					    params = {"variables": variables, "features": gqlFeatures}
 | 
				
			||||||
    js = await fetchRaw(graphUser ? params, Api.userScreenName)
 | 
					    js = await fetchRaw(graphUser ? params, Api.userScreenName)
 | 
				
			||||||
  result = parseGraphUser(js)
 | 
					  result = parseGraphUser(js)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc getGraphUserById*(id: string): Future[User] {.async.} =
 | 
					proc getGraphUserById*(id: string): Future[User] {.async.} =
 | 
				
			||||||
  if id.len == 0 or id.any(c => not c.isDigit): return
 | 
					  if id.len == 0 or id.any(c => not c.isDigit): return
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
    variables = %*{"userId": id}
 | 
					    variables = """{"rest_id": "$1"}""" % id
 | 
				
			||||||
    params = {"variables": $variables, "features": gqlFeatures}
 | 
					    params = {"variables": variables, "features": gqlFeatures}
 | 
				
			||||||
    js = await fetchRaw(graphUserById ? params, Api.userRestId)
 | 
					    js = await fetchRaw(graphUserById ? params, Api.userRestId)
 | 
				
			||||||
  result = parseGraphUser(js)
 | 
					  result = parseGraphUser(js)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Timeline] {.async.} =
 | 
					proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profile] {.async.} =
 | 
				
			||||||
  if id.len == 0: return
 | 
					  if id.len == 0: return
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
    cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
 | 
					    cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@ proc getGraphListTweets*(id: string; after=""): Future[Timeline] {.async.} =
 | 
				
			||||||
    variables = listTweetsVariables % [id, cursor]
 | 
					    variables = listTweetsVariables % [id, cursor]
 | 
				
			||||||
    params = {"variables": variables, "features": gqlFeatures}
 | 
					    params = {"variables": variables, "features": gqlFeatures}
 | 
				
			||||||
    js = await fetch(graphListTweets ? params, Api.listTweets)
 | 
					    js = await fetch(graphListTweets ? params, Api.listTweets)
 | 
				
			||||||
  result = parseGraphTimeline(js, "list", after)
 | 
					  result = parseGraphTimeline(js, "list", after).tweets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
 | 
					proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
| 
						 | 
					@ -50,8 +50,8 @@ proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc getGraphList*(id: string): Future[List] {.async.} =
 | 
					proc getGraphList*(id: string): Future[List] {.async.} =
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
    variables = %*{"listId": id}
 | 
					    variables = """{"listId": "$1"}""" % id
 | 
				
			||||||
    params = {"variables": $variables, "features": gqlFeatures}
 | 
					    params = {"variables": variables, "features": gqlFeatures}
 | 
				
			||||||
  result = parseGraphList(await fetch(graphListById ? params, Api.list))
 | 
					  result = parseGraphList(await fetch(graphListById ? params, Api.list))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.} =
 | 
					proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.} =
 | 
				
			||||||
| 
						 | 
					@ -72,7 +72,7 @@ proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.}
 | 
				
			||||||
proc getGraphTweetResult*(id: string): Future[Tweet] {.async.} =
 | 
					proc getGraphTweetResult*(id: string): Future[Tweet] {.async.} =
 | 
				
			||||||
  if id.len == 0: return
 | 
					  if id.len == 0: return
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
    variables = tweetResultVariables % id
 | 
					    variables = """{"rest_id": "$1"}""" % id
 | 
				
			||||||
    params = {"variables": variables, "features": gqlFeatures}
 | 
					    params = {"variables": variables, "features": gqlFeatures}
 | 
				
			||||||
    js = await fetch(graphTweetResult ? params, Api.tweetResult)
 | 
					    js = await fetch(graphTweetResult ? params, Api.tweetResult)
 | 
				
			||||||
  result = parseGraphTweetResult(js)
 | 
					  result = parseGraphTweetResult(js)
 | 
				
			||||||
| 
						 | 
					@ -95,10 +95,10 @@ proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
 | 
				
			||||||
  if after.len > 0:
 | 
					  if after.len > 0:
 | 
				
			||||||
    result.replies = await getReplies(id, after)
 | 
					    result.replies = await getReplies(id, after)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc getGraphSearch*(query: Query; after=""): Future[Result[Tweet]] {.async.} =
 | 
					proc getGraphSearch*(query: Query; after=""): Future[Profile] {.async.} =
 | 
				
			||||||
  let q = genQueryParam(query)
 | 
					  let q = genQueryParam(query)
 | 
				
			||||||
  if q.len == 0 or q == emptyQuery:
 | 
					  if q.len == 0 or q == emptyQuery:
 | 
				
			||||||
    return Result[Tweet](query: query, beginning: true)
 | 
					    return Profile(tweets: Timeline(query: query, beginning: true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var
 | 
					  var
 | 
				
			||||||
    variables = %*{
 | 
					    variables = %*{
 | 
				
			||||||
| 
						 | 
					@ -112,8 +112,8 @@ proc getGraphSearch*(query: Query; after=""): Future[Result[Tweet]] {.async.} =
 | 
				
			||||||
  if after.len > 0:
 | 
					  if after.len > 0:
 | 
				
			||||||
    variables["cursor"] = % after
 | 
					    variables["cursor"] = % after
 | 
				
			||||||
  let url = graphSearchTimeline ? {"variables": $variables, "features": gqlFeatures}
 | 
					  let url = graphSearchTimeline ? {"variables": $variables, "features": gqlFeatures}
 | 
				
			||||||
  result = parseGraphSearch(await fetch(url, Api.search), after)
 | 
					  result = Profile(tweets: parseGraphSearch(await fetch(url, Api.search), after))
 | 
				
			||||||
  result.query = query
 | 
					  result.tweets.query = query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc getUserSearch*(query: Query; page="1"): Future[Result[User]] {.async.} =
 | 
					proc getUserSearch*(query: Query; page="1"): Future[Result[User]] {.async.} =
 | 
				
			||||||
  if query.text.len == 0:
 | 
					  if query.text.len == 0:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
import uri, sequtils, strutils
 | 
					import uri, sequtils, strutils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const
 | 
					const
 | 
				
			||||||
  auth* = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
 | 
					  auth* = "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  api = parseUri("https://api.twitter.com")
 | 
					  api = parseUri("https://api.twitter.com")
 | 
				
			||||||
  activate* = $(api / "1.1/guest/activate.json")
 | 
					  activate* = $(api / "1.1/guest/activate.json")
 | 
				
			||||||
| 
						 | 
					@ -11,18 +11,18 @@ const
 | 
				
			||||||
  userSearch* = api / "1.1/users/search.json"
 | 
					  userSearch* = api / "1.1/users/search.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  graphql = api / "graphql"
 | 
					  graphql = api / "graphql"
 | 
				
			||||||
  graphUser* = graphql / "pVrmNaXcxPjisIvKtLDMEA/UserByScreenName"
 | 
					  graphUser* = graphql / "u7wQyGi6oExe8_TRWGMq4Q/UserResultByScreenNameQuery"
 | 
				
			||||||
  graphUserById* = graphql / "1YAM811Q8Ry4XyPpJclURQ/UserByRestId"
 | 
					  graphUserById* = graphql / "oPppcargziU1uDQHAUmH-A/UserResultByIdQuery"
 | 
				
			||||||
  graphUserTweets* = graphql / "WzJjibAcDa-oCjCcLOotcg/UserTweets"
 | 
					  graphUserTweets* = graphql / "3JNH4e9dq1BifLxAa3UMWg/UserWithProfileTweetsQueryV2"
 | 
				
			||||||
  graphUserTweetsAndReplies* = graphql / "fn9oRltM1N4thkh5CVusPg/UserTweetsAndReplies"
 | 
					  graphUserTweetsAndReplies* = graphql / "8IS8MaO-2EN6GZZZb8jF0g/UserWithProfileTweetsAndRepliesQueryV2"
 | 
				
			||||||
  graphUserMedia* = graphql / "qQoeS7szGavsi8-ehD2AWg/UserMedia"
 | 
					  graphUserMedia* = graphql / "PDfFf8hGeJvUCiTyWtw4wQ/MediaTimelineV2"
 | 
				
			||||||
  graphTweet* = graphql / "miKSMGb2R1SewIJv2-ablQ/TweetDetail"
 | 
					  graphTweet* = graphql / "83h5UyHZ9wEKBVzALX8R_g/ConversationTimelineV2"
 | 
				
			||||||
  graphTweetResult* = graphql / "0kc0a_7TTr3dvweZlMslsQ/TweetResultByRestId"
 | 
					  graphTweetResult* = graphql / "sITyJdhRPpvpEjg4waUmTA/TweetResultByIdQuery"
 | 
				
			||||||
  graphSearchTimeline* = graphql / "gkjsKepM6gl_HmFWoWKfgg/SearchTimeline"
 | 
					  graphSearchTimeline* = graphql / "gkjsKepM6gl_HmFWoWKfgg/SearchTimeline"
 | 
				
			||||||
  graphListById* = graphql / "iTpgCtbdxrsJfyx0cFjHqg/ListByRestId"
 | 
					  graphListById* = graphql / "iTpgCtbdxrsJfyx0cFjHqg/ListByRestId"
 | 
				
			||||||
  graphListBySlug* = graphql / "-kmqNvm5Y-cVrfvBy6docg/ListBySlug"
 | 
					  graphListBySlug* = graphql / "-kmqNvm5Y-cVrfvBy6docg/ListBySlug"
 | 
				
			||||||
  graphListMembers* = graphql / "P4NpVZDqUD_7MEM84L-8nw/ListMembers"
 | 
					  graphListMembers* = graphql / "P4NpVZDqUD_7MEM84L-8nw/ListMembers"
 | 
				
			||||||
  graphListTweets* = graphql / "jZntL0oVJSdjhmPcdbw_eA/ListLatestTweetsTimeline"
 | 
					  graphListTweets* = graphql / "BbGLL1ZfMibdFNWlk7a0Pw/ListTimeline"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  timelineParams* = {
 | 
					  timelineParams* = {
 | 
				
			||||||
    "include_profile_interstitial_type": "0",
 | 
					    "include_profile_interstitial_type": "0",
 | 
				
			||||||
| 
						 | 
					@ -49,10 +49,13 @@ const
 | 
				
			||||||
  }.toSeq
 | 
					  }.toSeq
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  gqlFeatures* = """{
 | 
					  gqlFeatures* = """{
 | 
				
			||||||
 | 
					  "android_graphql_skip_api_media_color_palette": false,
 | 
				
			||||||
  "blue_business_profile_image_shape_enabled": false,
 | 
					  "blue_business_profile_image_shape_enabled": false,
 | 
				
			||||||
 | 
					  "creator_subscriptions_subscription_count_enabled": false,
 | 
				
			||||||
  "creator_subscriptions_tweet_preview_api_enabled": true,
 | 
					  "creator_subscriptions_tweet_preview_api_enabled": true,
 | 
				
			||||||
  "freedom_of_speech_not_reach_fetch_enabled": false,
 | 
					  "freedom_of_speech_not_reach_fetch_enabled": false,
 | 
				
			||||||
  "graphql_is_translatable_rweb_tweet_is_translatable_enabled": false,
 | 
					  "graphql_is_translatable_rweb_tweet_is_translatable_enabled": false,
 | 
				
			||||||
 | 
					  "hidden_profile_likes_enabled": false,
 | 
				
			||||||
  "highlights_tweets_tab_ui_enabled": false,
 | 
					  "highlights_tweets_tab_ui_enabled": false,
 | 
				
			||||||
  "interactive_text_enabled": false,
 | 
					  "interactive_text_enabled": false,
 | 
				
			||||||
  "longform_notetweets_consumption_enabled": true,
 | 
					  "longform_notetweets_consumption_enabled": true,
 | 
				
			||||||
| 
						 | 
					@ -64,15 +67,25 @@ const
 | 
				
			||||||
  "responsive_web_graphql_exclude_directive_enabled": true,
 | 
					  "responsive_web_graphql_exclude_directive_enabled": true,
 | 
				
			||||||
  "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
 | 
					  "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
 | 
				
			||||||
  "responsive_web_graphql_timeline_navigation_enabled": false,
 | 
					  "responsive_web_graphql_timeline_navigation_enabled": false,
 | 
				
			||||||
 | 
					  "responsive_web_media_download_video_enabled": false,
 | 
				
			||||||
  "responsive_web_text_conversations_enabled": false,
 | 
					  "responsive_web_text_conversations_enabled": false,
 | 
				
			||||||
 | 
					  "responsive_web_twitter_article_tweet_consumption_enabled": false,
 | 
				
			||||||
  "responsive_web_twitter_blue_verified_badge_is_enabled": true,
 | 
					  "responsive_web_twitter_blue_verified_badge_is_enabled": true,
 | 
				
			||||||
  "rweb_lists_timeline_redesign_enabled": true,
 | 
					  "rweb_lists_timeline_redesign_enabled": true,
 | 
				
			||||||
  "spaces_2022_h2_clipping": true,
 | 
					  "spaces_2022_h2_clipping": true,
 | 
				
			||||||
  "spaces_2022_h2_spaces_communities": true,
 | 
					  "spaces_2022_h2_spaces_communities": true,
 | 
				
			||||||
  "standardized_nudges_misinfo": false,
 | 
					  "standardized_nudges_misinfo": false,
 | 
				
			||||||
 | 
					  "subscriptions_verification_info_enabled": true,
 | 
				
			||||||
 | 
					  "subscriptions_verification_info_reason_enabled": true,
 | 
				
			||||||
 | 
					  "subscriptions_verification_info_verified_since_enabled": true,
 | 
				
			||||||
 | 
					  "super_follow_badge_privacy_enabled": false,
 | 
				
			||||||
 | 
					  "super_follow_exclusive_tweet_notifications_enabled": false,
 | 
				
			||||||
 | 
					  "super_follow_tweet_api_enabled": false,
 | 
				
			||||||
 | 
					  "super_follow_user_api_enabled": false,
 | 
				
			||||||
  "tweet_awards_web_tipping_enabled": false,
 | 
					  "tweet_awards_web_tipping_enabled": false,
 | 
				
			||||||
  "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": false,
 | 
					  "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": false,
 | 
				
			||||||
  "tweetypie_unmention_optimization_enabled": false,
 | 
					  "tweetypie_unmention_optimization_enabled": false,
 | 
				
			||||||
 | 
					  "unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
 | 
				
			||||||
  "verified_phone_label_enabled": false,
 | 
					  "verified_phone_label_enabled": false,
 | 
				
			||||||
  "vibe_api_enabled": false,
 | 
					  "vibe_api_enabled": false,
 | 
				
			||||||
  "view_counts_everywhere_api_enabled": false
 | 
					  "view_counts_everywhere_api_enabled": false
 | 
				
			||||||
| 
						 | 
					@ -81,41 +94,15 @@ const
 | 
				
			||||||
  tweetVariables* = """{
 | 
					  tweetVariables* = """{
 | 
				
			||||||
  "focalTweetId": "$1",
 | 
					  "focalTweetId": "$1",
 | 
				
			||||||
  $2
 | 
					  $2
 | 
				
			||||||
  "withBirdwatchNotes": false,
 | 
					  "includeHasBirdwatchNotes": false
 | 
				
			||||||
  "includePromotedContent": false,
 | 
					 | 
				
			||||||
  "withDownvotePerspective": false,
 | 
					 | 
				
			||||||
  "withReactionsMetadata": false,
 | 
					 | 
				
			||||||
  "withReactionsPerspective": false,
 | 
					 | 
				
			||||||
  "withVoice": false
 | 
					 | 
				
			||||||
}"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  tweetResultVariables* = """{
 | 
					 | 
				
			||||||
  "tweetId": "$1",
 | 
					 | 
				
			||||||
  "includePromotedContent": false,
 | 
					 | 
				
			||||||
  "withDownvotePerspective": false,
 | 
					 | 
				
			||||||
  "withReactionsMetadata": false,
 | 
					 | 
				
			||||||
  "withReactionsPerspective": false,
 | 
					 | 
				
			||||||
  "withVoice": false,
 | 
					 | 
				
			||||||
  "withCommunity": false
 | 
					 | 
				
			||||||
}"""
 | 
					}"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  userTweetsVariables* = """{
 | 
					  userTweetsVariables* = """{
 | 
				
			||||||
  "userId": "$1", $2
 | 
					  "rest_id": "$1", $2
 | 
				
			||||||
  "count": 20,
 | 
					  "count": 20
 | 
				
			||||||
  "includePromotedContent": false,
 | 
					 | 
				
			||||||
  "withDownvotePerspective": false,
 | 
					 | 
				
			||||||
  "withReactionsMetadata": false,
 | 
					 | 
				
			||||||
  "withReactionsPerspective": false,
 | 
					 | 
				
			||||||
  "withVoice": false,
 | 
					 | 
				
			||||||
  "withV2Timeline": true
 | 
					 | 
				
			||||||
}"""
 | 
					}"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  listTweetsVariables* = """{
 | 
					  listTweetsVariables* = """{
 | 
				
			||||||
  "listId": "$1", $2
 | 
					  "rest_id": "$1", $2
 | 
				
			||||||
  "count": 20,
 | 
					  "count": 20
 | 
				
			||||||
  "includePromotedContent": false,
 | 
					 | 
				
			||||||
  "withDownvotePerspective": false,
 | 
					 | 
				
			||||||
  "withReactionsMetadata": false,
 | 
					 | 
				
			||||||
  "withReactionsPerspective": false,
 | 
					 | 
				
			||||||
  "withVoice": false
 | 
					 | 
				
			||||||
}"""
 | 
					}"""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,12 +9,12 @@ proc parseGraphUser*(json: string): User =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let raw = json.fromJson(GraphUser)
 | 
					  let raw = json.fromJson(GraphUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if raw.data.user.result.reason.get("") == "Suspended":
 | 
					  if raw.data.userResult.result.unavailableReason.get("") == "Suspended":
 | 
				
			||||||
    return User(suspended: true)
 | 
					    return User(suspended: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  result = toUser raw.data.user.result.legacy
 | 
					  result = toUser raw.data.userResult.result.legacy
 | 
				
			||||||
  result.id = raw.data.user.result.restId
 | 
					  result.id = raw.data.userResult.result.restId
 | 
				
			||||||
  result.verified = result.verified or raw.data.user.result.isBlueVerified
 | 
					  result.verified = result.verified or raw.data.userResult.result.isBlueVerified
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc parseGraphListMembers*(json, cursor: string): Result[User] =
 | 
					proc parseGraphListMembers*(json, cursor: string): Result[User] =
 | 
				
			||||||
  result = Result[User](
 | 
					  result = Result[User](
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ import user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type
 | 
					type
 | 
				
			||||||
  GraphUser* = object
 | 
					  GraphUser* = object
 | 
				
			||||||
    data*: tuple[user: UserData]
 | 
					    data*: tuple[userResult: UserData]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  UserData* = object
 | 
					  UserData* = object
 | 
				
			||||||
    result*: UserResult
 | 
					    result*: UserResult
 | 
				
			||||||
| 
						 | 
					@ -12,4 +12,4 @@ type
 | 
				
			||||||
    legacy*: RawUser
 | 
					    legacy*: RawUser
 | 
				
			||||||
    restId*: string
 | 
					    restId*: string
 | 
				
			||||||
    isBlueVerified*: bool
 | 
					    isBlueVerified*: bool
 | 
				
			||||||
    reason*: Option[string]
 | 
					    unavailableReason*: Option[string]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,7 +29,7 @@ proc parseUser(js: JsonNode; id=""): User =
 | 
				
			||||||
  result.expandUserEntities(js)
 | 
					  result.expandUserEntities(js)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc parseGraphUser(js: JsonNode): User =
 | 
					proc parseGraphUser(js: JsonNode): User =
 | 
				
			||||||
  let user = ? js{"user_results", "result"}
 | 
					  let user = ? js{"user_result", "result"}
 | 
				
			||||||
  result = parseUser(user{"legacy"})
 | 
					  result = parseUser(user{"legacy"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if "is_blue_verified" in user:
 | 
					  if "is_blue_verified" in user:
 | 
				
			||||||
| 
						 | 
					@ -262,6 +262,11 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet =
 | 
				
			||||||
        result.gif = some(parseGif(m))
 | 
					        result.gif = some(parseGif(m))
 | 
				
			||||||
      else: discard
 | 
					      else: discard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      with url, m{"url"}:
 | 
				
			||||||
 | 
					        if result.text.endsWith(url.getStr):
 | 
				
			||||||
 | 
					          result.text.removeSuffix(url.getStr)
 | 
				
			||||||
 | 
					          result.text = result.text.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  with jsWithheld, js{"withheld_in_countries"}:
 | 
					  with jsWithheld, js{"withheld_in_countries"}:
 | 
				
			||||||
    let withheldInCountries: seq[string] =
 | 
					    let withheldInCountries: seq[string] =
 | 
				
			||||||
      if jsWithheld.kind != JArray: @[]
 | 
					      if jsWithheld.kind != JArray: @[]
 | 
				
			||||||
| 
						 | 
					@ -294,16 +299,6 @@ proc finalizeTweet(global: GlobalObjects; id: string): Tweet =
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
      result.retweet = some Tweet()
 | 
					      result.retweet = some Tweet()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc parsePin(js: JsonNode; global: GlobalObjects): Tweet =
 | 
					 | 
				
			||||||
  let pin = js{"pinEntry", "entry", "entryId"}.getStr
 | 
					 | 
				
			||||||
  if pin.len == 0: return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let id = pin.getId
 | 
					 | 
				
			||||||
  if id notin global.tweets: return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  global.tweets[id].pinned = true
 | 
					 | 
				
			||||||
  return finalizeTweet(global, id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
proc parseGlobalObjects(js: JsonNode): GlobalObjects =
 | 
					proc parseGlobalObjects(js: JsonNode): GlobalObjects =
 | 
				
			||||||
  result = GlobalObjects()
 | 
					  result = GlobalObjects()
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
| 
						 | 
					@ -314,7 +309,7 @@ proc parseGlobalObjects(js: JsonNode): GlobalObjects =
 | 
				
			||||||
    result.users[k] = parseUser(v, k)
 | 
					    result.users[k] = parseUser(v, k)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for k, v in tweets:
 | 
					  for k, v in tweets:
 | 
				
			||||||
    var tweet = parseTweet(v, v{"card"})
 | 
					    var tweet = parseTweet(v, v{"tweet_card"})
 | 
				
			||||||
    if tweet.user.id in result.users:
 | 
					    if tweet.user.id in result.users:
 | 
				
			||||||
      tweet.user = result.users[tweet.user.id]
 | 
					      tweet.user = result.users[tweet.user.id]
 | 
				
			||||||
    result.tweets[k] = tweet
 | 
					    result.tweets[k] = tweet
 | 
				
			||||||
| 
						 | 
					@ -324,11 +319,6 @@ proc parseInstructions[T](res: var Result[T]; global: GlobalObjects; js: JsonNod
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for i in js:
 | 
					  for i in js:
 | 
				
			||||||
    when T is Tweet:
 | 
					 | 
				
			||||||
      if res.beginning and i{"pinEntry"}.notNull:
 | 
					 | 
				
			||||||
        with pin, parsePin(i, global):
 | 
					 | 
				
			||||||
          res.content.add pin
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with r, i{"replaceEntry", "entry"}:
 | 
					    with r, i{"replaceEntry", "entry"}:
 | 
				
			||||||
      if "top" in r{"entryId"}.getStr:
 | 
					      if "top" in r{"entryId"}.getStr:
 | 
				
			||||||
        res.top = r.getCursor
 | 
					        res.top = r.getCursor
 | 
				
			||||||
| 
						 | 
					@ -369,7 +359,7 @@ proc parseTimeline*(js: JsonNode; after=""): Timeline =
 | 
				
			||||||
proc parsePhotoRail*(js: JsonNode): PhotoRail =
 | 
					proc parsePhotoRail*(js: JsonNode): PhotoRail =
 | 
				
			||||||
  for tweet in js:
 | 
					  for tweet in js:
 | 
				
			||||||
    let
 | 
					    let
 | 
				
			||||||
      t = parseTweet(tweet, js{"card"})
 | 
					      t = parseTweet(tweet, js{"tweet_card"})
 | 
				
			||||||
      url = if t.photos.len > 0: t.photos[0]
 | 
					      url = if t.photos.len > 0: t.photos[0]
 | 
				
			||||||
            elif t.video.isSome: get(t.video).thumb
 | 
					            elif t.video.isSome: get(t.video).thumb
 | 
				
			||||||
            elif t.gif.isSome: get(t.gif).thumb
 | 
					            elif t.gif.isSome: get(t.gif).thumb
 | 
				
			||||||
| 
						 | 
					@ -387,13 +377,17 @@ proc parseGraphTweet(js: JsonNode): Tweet =
 | 
				
			||||||
  of "TweetUnavailable":
 | 
					  of "TweetUnavailable":
 | 
				
			||||||
    return Tweet()
 | 
					    return Tweet()
 | 
				
			||||||
  of "TweetTombstone":
 | 
					  of "TweetTombstone":
 | 
				
			||||||
    return Tweet(text: js{"tombstone", "text"}.getTombstone)
 | 
					    with text, js{"tombstone", "richText"}:
 | 
				
			||||||
 | 
					      return Tweet(text: text.getTombstone)
 | 
				
			||||||
 | 
					    with text, js{"tombstone", "text"}:
 | 
				
			||||||
 | 
					      return Tweet(text: text.getTombstone)
 | 
				
			||||||
 | 
					    return Tweet()
 | 
				
			||||||
  of "TweetPreviewDisplay":
 | 
					  of "TweetPreviewDisplay":
 | 
				
			||||||
    return Tweet(text: "You're unable to view this Tweet because it's only available to the Subscribers of the account owner.")
 | 
					    return Tweet(text: "You're unable to view this Tweet because it's only available to the Subscribers of the account owner.")
 | 
				
			||||||
  of "TweetWithVisibilityResults":
 | 
					  of "TweetWithVisibilityResults":
 | 
				
			||||||
    return parseGraphTweet(js{"tweet"})
 | 
					    return parseGraphTweet(js{"tweet"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var jsCard = copy(js{"card", "legacy"})
 | 
					  var jsCard = copy(js{"tweet_card", "legacy"})
 | 
				
			||||||
  if jsCard.kind != JNull:
 | 
					  if jsCard.kind != JNull:
 | 
				
			||||||
    var values = newJObject()
 | 
					    var values = newJObject()
 | 
				
			||||||
    for val in jsCard["binding_values"]:
 | 
					    for val in jsCard["binding_values"]:
 | 
				
			||||||
| 
						 | 
					@ -401,6 +395,7 @@ proc parseGraphTweet(js: JsonNode): Tweet =
 | 
				
			||||||
    jsCard["binding_values"] = values
 | 
					    jsCard["binding_values"] = values
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  result = parseTweet(js{"legacy"}, jsCard)
 | 
					  result = parseTweet(js{"legacy"}, jsCard)
 | 
				
			||||||
 | 
					  result.id = js{"rest_id"}.getId
 | 
				
			||||||
  result.user = parseGraphUser(js{"core"})
 | 
					  result.user = parseGraphUser(js{"core"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  with noteTweet, js{"note_tweet", "note_tweet_results", "result"}:
 | 
					  with noteTweet, js{"note_tweet", "note_tweet_results", "result"}:
 | 
				
			||||||
| 
						 | 
					@ -414,32 +409,31 @@ proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] =
 | 
				
			||||||
  for t in js{"content", "items"}:
 | 
					  for t in js{"content", "items"}:
 | 
				
			||||||
    let entryId = t{"entryId"}.getStr
 | 
					    let entryId = t{"entryId"}.getStr
 | 
				
			||||||
    if "cursor-showmore" in entryId:
 | 
					    if "cursor-showmore" in entryId:
 | 
				
			||||||
      let cursor = t{"item", "itemContent", "value"}
 | 
					      let cursor = t{"item", "content", "value"}
 | 
				
			||||||
      result.thread.cursor = cursor.getStr
 | 
					      result.thread.cursor = cursor.getStr
 | 
				
			||||||
      result.thread.hasMore = true
 | 
					      result.thread.hasMore = true
 | 
				
			||||||
    elif "tweet" in entryId:
 | 
					    elif "tweet" in entryId:
 | 
				
			||||||
      let tweet = parseGraphTweet(t{"item", "itemContent", "tweet_results", "result"})
 | 
					      let tweet = parseGraphTweet(t{"item", "content", "tweetResult", "result"})
 | 
				
			||||||
      result.thread.content.add tweet
 | 
					      result.thread.content.add tweet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if t{"item", "itemContent", "tweetDisplayType"}.getStr == "SelfThread":
 | 
					      if t{"item", "content", "tweetDisplayType"}.getStr == "SelfThread":
 | 
				
			||||||
        result.self = true
 | 
					        result.self = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc parseGraphTweetResult*(js: JsonNode): Tweet =
 | 
					proc parseGraphTweetResult*(js: JsonNode): Tweet =
 | 
				
			||||||
  with tweet, js{"data", "tweetResult", "result"}:
 | 
					  with tweet, js{"data", "tweet_result", "result"}:
 | 
				
			||||||
    result = parseGraphTweet(tweet)
 | 
					    result = parseGraphTweet(tweet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
 | 
					proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
 | 
				
			||||||
  result = Conversation(replies: Result[Chain](beginning: true))
 | 
					  result = Conversation(replies: Result[Chain](beginning: true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let instructions = ? js{"data", "threaded_conversation_with_injections", "instructions"}
 | 
					  let instructions = ? js{"data", "timeline_response", "instructions"}
 | 
				
			||||||
  if instructions.len == 0:
 | 
					  if instructions.len == 0:
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for e in instructions[0]{"entries"}:
 | 
					  for e in instructions[0]{"entries"}:
 | 
				
			||||||
    let entryId = e{"entryId"}.getStr
 | 
					    let entryId = e{"entryId"}.getStr
 | 
				
			||||||
    # echo entryId
 | 
					 | 
				
			||||||
    if entryId.startsWith("tweet"):
 | 
					    if entryId.startsWith("tweet"):
 | 
				
			||||||
      with tweetResult, e{"content", "itemContent", "tweet_results", "result"}:
 | 
					      with tweetResult, e{"content", "content", "tweetResult", "result"}:
 | 
				
			||||||
        let tweet = parseGraphTweet(tweetResult)
 | 
					        let tweet = parseGraphTweet(tweetResult)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not tweet.available:
 | 
					        if not tweet.available:
 | 
				
			||||||
| 
						 | 
					@ -454,7 +448,7 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
 | 
				
			||||||
      let tweet = Tweet(
 | 
					      let tweet = Tweet(
 | 
				
			||||||
        id: parseBiggestInt(id),
 | 
					        id: parseBiggestInt(id),
 | 
				
			||||||
        available: false,
 | 
					        available: false,
 | 
				
			||||||
        text: e{"content", "itemContent", "tombstoneInfo", "richText"}.getTombstone
 | 
					        text: e{"content", "content", "tombstoneInfo", "richText"}.getTombstone
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if id == tweetId:
 | 
					      if id == tweetId:
 | 
				
			||||||
| 
						 | 
					@ -468,34 +462,42 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
 | 
				
			||||||
      else:
 | 
					      else:
 | 
				
			||||||
        result.replies.content.add thread
 | 
					        result.replies.content.add thread
 | 
				
			||||||
    elif entryId.startsWith("cursor-bottom"):
 | 
					    elif entryId.startsWith("cursor-bottom"):
 | 
				
			||||||
      result.replies.bottom = e{"content", "itemContent", "value"}.getStr
 | 
					      result.replies.bottom = e{"content", "content", "value"}.getStr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Timeline =
 | 
					proc parseGraphTimeline*(js: JsonNode; root: string; after=""): Profile =
 | 
				
			||||||
  result = Timeline(beginning: after.len == 0)
 | 
					  result = Profile(tweets: Timeline(beginning: after.len == 0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let instructions =
 | 
					  let instructions =
 | 
				
			||||||
    if root == "list": ? js{"data", "list", "tweets_timeline", "timeline", "instructions"}
 | 
					    if root == "list": ? js{"data", "list", "timeline_response", "timeline", "instructions"}
 | 
				
			||||||
    else: ? js{"data", "user", "result", "timeline_v2", "timeline", "instructions"}
 | 
					    else: ? js{"data", "user_result", "result", "timeline_response", "timeline", "instructions"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if instructions.len == 0:
 | 
					  if instructions.len == 0:
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for i in instructions:
 | 
					  for i in instructions:
 | 
				
			||||||
    if i{"type"}.getStr == "TimelineAddEntries":
 | 
					    if i{"__typename"}.getStr == "TimelineAddEntries":
 | 
				
			||||||
      for e in i{"entries"}:
 | 
					      for e in i{"entries"}:
 | 
				
			||||||
        let entryId = e{"entryId"}.getStr
 | 
					        let entryId = e{"entryId"}.getStr
 | 
				
			||||||
        if entryId.startsWith("tweet"):
 | 
					        if entryId.startsWith("tweet"):
 | 
				
			||||||
          with tweetResult, e{"content", "itemContent", "tweet_results", "result"}:
 | 
					          with tweetResult, e{"content", "content", "tweetResult", "result"}:
 | 
				
			||||||
            let tweet = parseGraphTweet(tweetResult)
 | 
					            let tweet = parseGraphTweet(tweetResult)
 | 
				
			||||||
            if not tweet.available:
 | 
					            if not tweet.available:
 | 
				
			||||||
              tweet.id = parseBiggestInt(entryId.getId())
 | 
					              tweet.id = parseBiggestInt(entryId.getId())
 | 
				
			||||||
            result.content.add tweet
 | 
					            result.tweets.content.add tweet
 | 
				
			||||||
        elif entryId.startsWith("profile-conversation") or entryId.startsWith("homeConversation"):
 | 
					        elif "-conversation-" in entryId or entryId.startsWith("homeConversation"):
 | 
				
			||||||
          let (thread, self) = parseGraphThread(e)
 | 
					          let (thread, self) = parseGraphThread(e)
 | 
				
			||||||
          for tweet in thread.content:
 | 
					          result.tweets.content.add thread
 | 
				
			||||||
            result.content.add tweet
 | 
					 | 
				
			||||||
        elif entryId.startsWith("cursor-bottom"):
 | 
					        elif entryId.startsWith("cursor-bottom"):
 | 
				
			||||||
          result.bottom = e{"content", "value"}.getStr
 | 
					          result.tweets.bottom = e{"content", "value"}.getStr
 | 
				
			||||||
 | 
					    if after.len == 0 and i{"__typename"}.getStr == "TimelinePinEntry":
 | 
				
			||||||
 | 
					      with tweetResult, i{"entry", "content", "content", "tweetResult", "result"}:
 | 
				
			||||||
 | 
					        let tweet = parseGraphTweet(tweetResult)
 | 
				
			||||||
 | 
					        tweet.pinned = true
 | 
				
			||||||
 | 
					        if not tweet.available and tweet.tombstone.len == 0:
 | 
				
			||||||
 | 
					          let entryId = i{"entry", "entryId"}.getEntryId
 | 
				
			||||||
 | 
					          if entryId.len > 0:
 | 
				
			||||||
 | 
					            tweet.id = parseBiggestInt(entryId)
 | 
				
			||||||
 | 
					        result.pinned = some tweet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc parseGraphSearch*(js: JsonNode; after=""): Timeline =
 | 
					proc parseGraphSearch*(js: JsonNode; after=""): Timeline =
 | 
				
			||||||
  result = Timeline(beginning: after.len == 0)
 | 
					  result = Timeline(beginning: after.len == 0)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,22 +10,22 @@ export api, embed, vdom, tweet, general, router_utils
 | 
				
			||||||
proc createEmbedRouter*(cfg: Config) =
 | 
					proc createEmbedRouter*(cfg: Config) =
 | 
				
			||||||
  router embed:
 | 
					  router embed:
 | 
				
			||||||
    get "/i/videos/tweet/@id":
 | 
					    get "/i/videos/tweet/@id":
 | 
				
			||||||
      let convo = await getTweet(@"id")
 | 
					      let tweet = await getGraphTweetResult(@"id")
 | 
				
			||||||
      if convo == nil or convo.tweet == nil or convo.tweet.video.isNone:
 | 
					      if tweet == nil or tweet.video.isNone:
 | 
				
			||||||
        resp Http404
 | 
					        resp Http404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      resp renderVideoEmbed(convo.tweet, cfg, request)
 | 
					      resp renderVideoEmbed(tweet, cfg, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get "/@user/status/@id/embed":
 | 
					    get "/@user/status/@id/embed":
 | 
				
			||||||
      let
 | 
					      let
 | 
				
			||||||
        convo = await getTweet(@"id")
 | 
					        tweet = await getGraphTweetResult(@"id")
 | 
				
			||||||
        prefs = cookiePrefs()
 | 
					        prefs = cookiePrefs()
 | 
				
			||||||
        path = getPath()
 | 
					        path = getPath()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if convo == nil or convo.tweet == nil:
 | 
					      if tweet == nil:
 | 
				
			||||||
        resp Http404
 | 
					        resp Http404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      resp renderTweetEmbed(convo.tweet, path, prefs, cfg, request)
 | 
					      resp renderTweetEmbed(tweet, path, prefs, cfg, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get "/embed/Tweet.html":
 | 
					    get "/embed/Tweet.html":
 | 
				
			||||||
      let id = @"id"
 | 
					      let id = @"id"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,15 +27,13 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
 | 
				
			||||||
  else:
 | 
					  else:
 | 
				
			||||||
    var q = query
 | 
					    var q = query
 | 
				
			||||||
    q.fromUser = names
 | 
					    q.fromUser = names
 | 
				
			||||||
    profile = Profile(
 | 
					    profile = await getGraphSearch(q, after)
 | 
				
			||||||
      tweets: await getGraphSearch(q, after),
 | 
					 | 
				
			||||||
    # this is kinda dumb
 | 
					    # this is kinda dumb
 | 
				
			||||||
      user: User(
 | 
					    profile.user = User(
 | 
				
			||||||
      username: name,
 | 
					      username: name,
 | 
				
			||||||
      fullname: names.join(" | "),
 | 
					      fullname: names.join(" | "),
 | 
				
			||||||
      userpic: "https://abs.twimg.com/sticky/default_profile_images/default_profile.png"
 | 
					      userpic: "https://abs.twimg.com/sticky/default_profile_images/default_profile.png"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if profile.user.suspended:
 | 
					  if profile.user.suspended:
 | 
				
			||||||
    return Rss(feed: profile.user.username, cursor: "suspended")
 | 
					    return Rss(feed: profile.user.username, cursor: "suspended")
 | 
				
			||||||
| 
						 | 
					@ -61,29 +59,29 @@ template respRss*(rss, page) =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc createRssRouter*(cfg: Config) =
 | 
					proc createRssRouter*(cfg: Config) =
 | 
				
			||||||
  router rss:
 | 
					  router rss:
 | 
				
			||||||
    get "/search/rss":
 | 
					    # get "/search/rss":
 | 
				
			||||||
      cond cfg.enableRss
 | 
					    #   cond cfg.enableRss
 | 
				
			||||||
      if @"q".len > 200:
 | 
					    #   if @"q".len > 200:
 | 
				
			||||||
        resp Http400, showError("Search input too long.", cfg)
 | 
					    #     resp Http400, showError("Search input too long.", cfg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let query = initQuery(params(request))
 | 
					    #   let query = initQuery(params(request))
 | 
				
			||||||
      if query.kind != tweets:
 | 
					    #   if query.kind != tweets:
 | 
				
			||||||
        resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg)
 | 
					    #     resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let
 | 
					    #   let
 | 
				
			||||||
        cursor = getCursor()
 | 
					    #     cursor = getCursor()
 | 
				
			||||||
        key = redisKey("search", $hash(genQueryUrl(query)), cursor)
 | 
					    #     key = redisKey("search", $hash(genQueryUrl(query)), cursor)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var rss = await getCachedRss(key)
 | 
					    #   var rss = await getCachedRss(key)
 | 
				
			||||||
      if rss.cursor.len > 0:
 | 
					    #   if rss.cursor.len > 0:
 | 
				
			||||||
        respRss(rss, "Search")
 | 
					    #     respRss(rss, "Search")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let tweets = await getGraphSearch(query, cursor)
 | 
					    #   let tweets = await getGraphSearch(query, cursor)
 | 
				
			||||||
      rss.cursor = tweets.bottom
 | 
					    #   rss.cursor = tweets.bottom
 | 
				
			||||||
      rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg)
 | 
					    #   rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await cacheRss(key, rss)
 | 
					    #   await cacheRss(key, rss)
 | 
				
			||||||
      respRss(rss, "Search")
 | 
					    #   respRss(rss, "Search")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get "/@name/rss":
 | 
					    get "/@name/rss":
 | 
				
			||||||
      cond cfg.enableRss
 | 
					      cond cfg.enableRss
 | 
				
			||||||
| 
						 | 
					@ -112,7 +110,7 @@ proc createRssRouter*(cfg: Config) =
 | 
				
			||||||
          case tab
 | 
					          case tab
 | 
				
			||||||
          of "with_replies": getReplyQuery(name)
 | 
					          of "with_replies": getReplyQuery(name)
 | 
				
			||||||
          of "media": getMediaQuery(name)
 | 
					          of "media": getMediaQuery(name)
 | 
				
			||||||
          of "search": initQuery(params(request), name=name)
 | 
					          # of "search": initQuery(params(request), name=name)
 | 
				
			||||||
          else: Query(fromUser: @[name])
 | 
					          else: Query(fromUser: @[name])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let searchKey = if tab != "search": ""
 | 
					      let searchKey = if tab != "search": ""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,11 +34,15 @@ proc createSearchRouter*(cfg: Config) =
 | 
				
			||||||
          users = Result[User](beginning: true, query: query)
 | 
					          users = Result[User](beginning: true, query: query)
 | 
				
			||||||
        resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title)
 | 
					        resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title)
 | 
				
			||||||
      of tweets:
 | 
					      of tweets:
 | 
				
			||||||
        let
 | 
					        # let
 | 
				
			||||||
          tweets = await getGraphSearch(query, getCursor())
 | 
					        #   tweets = await getGraphSearch(query, getCursor())
 | 
				
			||||||
          rss = "/search/rss?" & genQueryUrl(query)
 | 
					        #   rss = "/search/rss?" & genQueryUrl(query)
 | 
				
			||||||
        resp renderMain(renderTweetSearch(tweets, prefs, getPath()),
 | 
					        # resp renderMain(renderTweetSearch(tweets, prefs, getPath()),
 | 
				
			||||||
                        request, cfg, prefs, title, rss=rss)
 | 
					        #                 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)
 | 
				
			||||||
      else:
 | 
					      else:
 | 
				
			||||||
        resp Http404, showError("Invalid search", cfg)
 | 
					        resp Http404, showError("Invalid search", cfg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,34 +45,24 @@ proc fetchProfile*(after: string; query: Query; skipRail=false;
 | 
				
			||||||
    after.setLen 0
 | 
					    after.setLen 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let
 | 
					  let
 | 
				
			||||||
    timeline =
 | 
					 | 
				
			||||||
      case query.kind
 | 
					 | 
				
			||||||
      of posts: getGraphUserTweets(userId, TimelineKind.tweets, after)
 | 
					 | 
				
			||||||
      of replies: getGraphUserTweets(userId, TimelineKind.replies, after)
 | 
					 | 
				
			||||||
      of media: getGraphUserTweets(userId, TimelineKind.media, after)
 | 
					 | 
				
			||||||
      else: getGraphSearch(query, after)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    rail =
 | 
					    rail =
 | 
				
			||||||
      skipIf(skipRail or query.kind == media, @[]):
 | 
					      skipIf(skipRail or query.kind == media, @[]):
 | 
				
			||||||
        getCachedPhotoRail(name)
 | 
					        getCachedPhotoRail(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user = await getCachedUser(name)
 | 
					    user = getCachedUser(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var pinned: Option[Tweet]
 | 
					  result =
 | 
				
			||||||
  if not skipPinned and user.pinnedTweet > 0 and
 | 
					    case query.kind
 | 
				
			||||||
     after.len == 0 and query.kind in {posts, replies}:
 | 
					    of posts: await getGraphUserTweets(userId, TimelineKind.tweets, after)
 | 
				
			||||||
    let tweet = await getCachedTweet(user.pinnedTweet)
 | 
					    of replies: await getGraphUserTweets(userId, TimelineKind.replies, after)
 | 
				
			||||||
    if not tweet.isNil:
 | 
					    of media: await getGraphUserTweets(userId, TimelineKind.media, after)
 | 
				
			||||||
      tweet.pinned = true
 | 
					    else: Profile(tweets: Timeline(beginning: true, content: @[Chain(content:
 | 
				
			||||||
      tweet.user = user
 | 
					      @[Tweet(tombstone: "Tweet search is unavailable for now")]
 | 
				
			||||||
      pinned = some tweet
 | 
					    )]))
 | 
				
			||||||
 | 
					    # else: await getGraphSearch(query, after)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  result = Profile(
 | 
					  result.user = await user
 | 
				
			||||||
    user: user,
 | 
					  result.photoRail = await rail
 | 
				
			||||||
    pinned: pinned,
 | 
					 | 
				
			||||||
    tweets: await timeline,
 | 
					 | 
				
			||||||
    photoRail: await rail
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if result.user.protected or result.user.suspended:
 | 
					  if result.user.protected or result.user.suspended:
 | 
				
			||||||
    return
 | 
					    return
 | 
				
			||||||
| 
						 | 
					@ -83,8 +73,11 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
 | 
				
			||||||
                   rss, after: string): Future[string] {.async.} =
 | 
					                   rss, after: string): Future[string] {.async.} =
 | 
				
			||||||
  if query.fromUser.len != 1:
 | 
					  if query.fromUser.len != 1:
 | 
				
			||||||
    let
 | 
					    let
 | 
				
			||||||
      timeline = await getGraphSearch(query, after)
 | 
					      # timeline = await getGraphSearch(query, after)
 | 
				
			||||||
      html = renderTweetSearch(timeline, prefs, getPath())
 | 
					      timeline = Profile(tweets: Timeline(beginning: true, content: @[Chain(content:
 | 
				
			||||||
 | 
					        @[Tweet(tombstone: "This features is unavailable for now")]
 | 
				
			||||||
 | 
					      )]))
 | 
				
			||||||
 | 
					      html = renderTweetSearch(timeline.tweets, prefs, getPath())
 | 
				
			||||||
    return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
 | 
					    return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var profile = await fetchProfile(after, query, skipPinned=prefs.hidePins)
 | 
					  var profile = await fetchProfile(after, query, skipPinned=prefs.hidePins)
 | 
				
			||||||
| 
						 | 
					@ -138,7 +131,7 @@ proc createTimelineRouter*(cfg: Config) =
 | 
				
			||||||
      # used for the infinite scroll feature
 | 
					      # used for the infinite scroll feature
 | 
				
			||||||
      if @"scroll".len > 0:
 | 
					      if @"scroll".len > 0:
 | 
				
			||||||
        if query.fromUser.len != 1:
 | 
					        if query.fromUser.len != 1:
 | 
				
			||||||
          var timeline = await getGraphSearch(query, after)
 | 
					          var timeline = (await getGraphSearch(query, after)).tweets
 | 
				
			||||||
          if timeline.content.len == 0: resp Http404
 | 
					          if timeline.content.len == 0: resp Http404
 | 
				
			||||||
          timeline.beginning = true
 | 
					          timeline.beginning = true
 | 
				
			||||||
          resp $renderTweetSearch(timeline, prefs, getPath())
 | 
					          resp $renderTweetSearch(timeline, prefs, getPath())
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -110,3 +110,29 @@
 | 
				
			||||||
    margin-left: 58px;
 | 
					    margin-left: 58px;
 | 
				
			||||||
    padding: 7px 0;
 | 
					    padding: 7px 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.timeline-item.thread.more-replies-thread {
 | 
				
			||||||
 | 
					    padding: 0 0.75em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::before {
 | 
				
			||||||
 | 
					        top: 40px;
 | 
				
			||||||
 | 
					        margin-bottom: 31px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .more-replies {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        padding-top: unset !important;
 | 
				
			||||||
 | 
					        margin-top: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &::before {
 | 
				
			||||||
 | 
					            display: inline-block;
 | 
				
			||||||
 | 
					            position: relative;
 | 
				
			||||||
 | 
					            top: -1px;
 | 
				
			||||||
 | 
					            line-height: 0.4em;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .more-replies-text {
 | 
				
			||||||
 | 
					            display: inline;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,11 +41,10 @@ proc getPoolJson*(): JsonNode =
 | 
				
			||||||
      let
 | 
					      let
 | 
				
			||||||
        maxReqs =
 | 
					        maxReqs =
 | 
				
			||||||
          case api
 | 
					          case api
 | 
				
			||||||
          of Api.timeline: 187
 | 
					          of Api.timeline: 180
 | 
				
			||||||
          of Api.listMembers, Api.listBySlug, Api.list, Api.listTweets,
 | 
					          of Api.userTweets, Api.userTweetsAndReplies, Api.userRestId,
 | 
				
			||||||
             Api.userTweets, Api.userTweetsAndReplies, Api.userMedia,
 | 
					             Api.userScreenName, Api.tweetDetail, Api.tweetResult, Api.search: 500
 | 
				
			||||||
             Api.userRestId, Api.userScreenName,
 | 
					          of Api.list, Api.listTweets, Api.listMembers, Api.listBySlug, Api.userMedia: 500
 | 
				
			||||||
             Api.tweetDetail, Api.tweetResult, Api.search: 500
 | 
					 | 
				
			||||||
          of Api.userSearch: 900
 | 
					          of Api.userSearch: 900
 | 
				
			||||||
        reqs = maxReqs - token.apis[api].remaining
 | 
					        reqs = maxReqs - token.apis[api].remaining
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -222,7 +222,7 @@ type
 | 
				
			||||||
    after*: Chain
 | 
					    after*: Chain
 | 
				
			||||||
    replies*: Result[Chain]
 | 
					    replies*: Result[Chain]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Timeline* = Result[Tweet]
 | 
					  Timeline* = Result[Chain]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Profile* = object
 | 
					  Profile* = object
 | 
				
			||||||
    user*: User
 | 
					    user*: User
 | 
				
			||||||
| 
						 | 
					@ -274,3 +274,6 @@ type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc contains*(thread: Chain; tweet: Tweet): bool =
 | 
					proc contains*(thread: Chain; tweet: Tweet): bool =
 | 
				
			||||||
  thread.content.anyIt(it.id == tweet.id)
 | 
					  thread.content.anyIt(it.id == tweet.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					proc add*(timeline: var seq[Chain]; tweet: Tweet) =
 | 
				
			||||||
 | 
					  timeline.add Chain(content: @[tweet])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,10 +56,14 @@ Twitter feed for: ${desc}. Generated by ${cfg.hostname}
 | 
				
			||||||
#end if
 | 
					#end if
 | 
				
			||||||
#end proc
 | 
					#end proc
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#proc renderRssTweets(tweets: seq[Tweet]; cfg: Config): string =
 | 
					#proc renderRssTweets(tweets: seq[Chain]; cfg: Config; userId=""): string =
 | 
				
			||||||
#let urlPrefix = getUrlPrefix(cfg)
 | 
					#let urlPrefix = getUrlPrefix(cfg)
 | 
				
			||||||
#var links: seq[string]
 | 
					#var links: seq[string]
 | 
				
			||||||
#for t in tweets:
 | 
					#for c in tweets:
 | 
				
			||||||
 | 
					#  for t in c.content:
 | 
				
			||||||
 | 
					#    if userId.len > 0 and t.user.id != userId: continue
 | 
				
			||||||
 | 
					#    end if
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
#    let retweet = if t.retweet.isSome: t.user.username else: ""
 | 
					#    let retweet = if t.retweet.isSome: t.user.username else: ""
 | 
				
			||||||
#    let tweet = if retweet.len > 0: t.retweet.get else: t
 | 
					#    let tweet = if retweet.len > 0: t.retweet.get else: t
 | 
				
			||||||
#    let link = getLink(tweet)
 | 
					#    let link = getLink(tweet)
 | 
				
			||||||
| 
						 | 
					@ -75,6 +79,7 @@ Twitter feed for: ${desc}. Generated by ${cfg.hostname}
 | 
				
			||||||
        <link>${urlPrefix & link}</link>
 | 
					        <link>${urlPrefix & link}</link>
 | 
				
			||||||
      </item>
 | 
					      </item>
 | 
				
			||||||
#  end for
 | 
					#  end for
 | 
				
			||||||
 | 
					#end for
 | 
				
			||||||
#end proc
 | 
					#end proc
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#proc renderTimelineRss*(profile: Profile; cfg: Config; multi=false): string =
 | 
					#proc renderTimelineRss*(profile: Profile; cfg: Config; multi=false): string =
 | 
				
			||||||
| 
						 | 
					@ -102,13 +107,13 @@ Twitter feed for: ${desc}. Generated by ${cfg.hostname}
 | 
				
			||||||
      <height>128</height>
 | 
					      <height>128</height>
 | 
				
			||||||
    </image>
 | 
					    </image>
 | 
				
			||||||
#if profile.tweets.content.len > 0:
 | 
					#if profile.tweets.content.len > 0:
 | 
				
			||||||
${renderRssTweets(profile.tweets.content, cfg)}
 | 
					${renderRssTweets(profile.tweets.content, cfg, userId=profile.user.id)}
 | 
				
			||||||
#end if
 | 
					#end if
 | 
				
			||||||
  </channel>
 | 
					  </channel>
 | 
				
			||||||
</rss>
 | 
					</rss>
 | 
				
			||||||
#end proc
 | 
					#end proc
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#proc renderListRss*(tweets: seq[Tweet]; list: List; cfg: Config): string =
 | 
					#proc renderListRss*(tweets: seq[Chain]; list: List; cfg: Config): string =
 | 
				
			||||||
#let link = &"{getUrlPrefix(cfg)}/i/lists/{list.id}"
 | 
					#let link = &"{getUrlPrefix(cfg)}/i/lists/{list.id}"
 | 
				
			||||||
#result = ""
 | 
					#result = ""
 | 
				
			||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
| 
						 | 
					@ -125,7 +130,7 @@ ${renderRssTweets(tweets, cfg)}
 | 
				
			||||||
</rss>
 | 
					</rss>
 | 
				
			||||||
#end proc
 | 
					#end proc
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#proc renderSearchRss*(tweets: seq[Tweet]; name, param: string; cfg: Config): string =
 | 
					#proc renderSearchRss*(tweets: seq[Chain]; name, param: string; cfg: Config): string =
 | 
				
			||||||
#let link = &"{getUrlPrefix(cfg)}/search"
 | 
					#let link = &"{getUrlPrefix(cfg)}/search"
 | 
				
			||||||
#let escName = xmltree.escape(name)
 | 
					#let escName = xmltree.escape(name)
 | 
				
			||||||
#result = ""
 | 
					#result = ""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,7 +88,7 @@ proc renderSearchPanel*(query: Query): VNode =
 | 
				
			||||||
          span(class="search-title"): text "Near"
 | 
					          span(class="search-title"): text "Near"
 | 
				
			||||||
          genInput("near", "", query.near, "Location...", autofocus=false)
 | 
					          genInput("near", "", query.near, "Location...", autofocus=false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string;
 | 
					proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string;
 | 
				
			||||||
                        pinned=none(Tweet)): VNode =
 | 
					                        pinned=none(Tweet)): VNode =
 | 
				
			||||||
  let query = results.query
 | 
					  let query = results.query
 | 
				
			||||||
  buildHtml(tdiv(class="timeline-container")):
 | 
					  buildHtml(tdiv(class="timeline-container")):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
					# SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
import strutils, strformat, sequtils, algorithm, uri, options
 | 
					import strutils, strformat, algorithm, uri, options
 | 
				
			||||||
import karax/[karaxdsl, vdom]
 | 
					import karax/[karaxdsl, vdom]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ".."/[types, query, formatters]
 | 
					import ".."/[types, query, formatters]
 | 
				
			||||||
| 
						 | 
					@ -43,20 +43,18 @@ proc renderThread(thread: seq[Tweet]; prefs: Prefs; path: string): VNode =
 | 
				
			||||||
  buildHtml(tdiv(class="thread-line")):
 | 
					  buildHtml(tdiv(class="thread-line")):
 | 
				
			||||||
    let sortedThread = thread.sortedByIt(it.id)
 | 
					    let sortedThread = thread.sortedByIt(it.id)
 | 
				
			||||||
    for i, tweet in sortedThread:
 | 
					    for i, tweet in sortedThread:
 | 
				
			||||||
 | 
					      # thread has a gap, display "more replies" link
 | 
				
			||||||
 | 
					      if i > 0 and tweet.replyId != sortedThread[i - 1].id:
 | 
				
			||||||
 | 
					        tdiv(class="timeline-item thread more-replies-thread"):
 | 
				
			||||||
 | 
					          tdiv(class="more-replies"):
 | 
				
			||||||
 | 
					            a(class="more-replies-text", href=getLink(tweet)):
 | 
				
			||||||
 | 
					              text "more replies"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let show = i == thread.high and sortedThread[0].id != tweet.threadId
 | 
					      let show = i == thread.high and sortedThread[0].id != tweet.threadId
 | 
				
			||||||
      let header = if tweet.pinned or tweet.retweet.isSome: "with-header " else: ""
 | 
					      let header = if tweet.pinned or tweet.retweet.isSome: "with-header " else: ""
 | 
				
			||||||
      renderTweet(tweet, prefs, path, class=(header & "thread"),
 | 
					      renderTweet(tweet, prefs, path, class=(header & "thread"),
 | 
				
			||||||
                  index=i, last=(i == thread.high), showThread=show)
 | 
					                  index=i, last=(i == thread.high), showThread=show)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc threadFilter(tweets: openArray[Tweet]; threads: openArray[int64]; it: Tweet): seq[Tweet] =
 | 
					 | 
				
			||||||
  result = @[it]
 | 
					 | 
				
			||||||
  if it.retweet.isSome or it.replyId in threads: return
 | 
					 | 
				
			||||||
  for t in tweets:
 | 
					 | 
				
			||||||
    if t.id == result[0].replyId:
 | 
					 | 
				
			||||||
      result.insert t
 | 
					 | 
				
			||||||
    elif t.replyId == result[0].id:
 | 
					 | 
				
			||||||
      result.add t
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
proc renderUser(user: User; prefs: Prefs): VNode =
 | 
					proc renderUser(user: User; prefs: Prefs): VNode =
 | 
				
			||||||
  buildHtml(tdiv(class="timeline-item")):
 | 
					  buildHtml(tdiv(class="timeline-item")):
 | 
				
			||||||
    a(class="tweet-link", href=("/" & user.username))
 | 
					    a(class="tweet-link", href=("/" & user.username))
 | 
				
			||||||
| 
						 | 
					@ -89,7 +87,7 @@ proc renderTimelineUsers*(results: Result[User]; prefs: Prefs; path=""): VNode =
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
      renderNoMore()
 | 
					      renderNoMore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc renderTimelineTweets*(results: Result[Tweet]; prefs: Prefs; path: string;
 | 
					proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string;
 | 
				
			||||||
                           pinned=none(Tweet)): VNode =
 | 
					                           pinned=none(Tweet)): VNode =
 | 
				
			||||||
  buildHtml(tdiv(class="timeline")):
 | 
					  buildHtml(tdiv(class="timeline")):
 | 
				
			||||||
    if not results.beginning:
 | 
					    if not results.beginning:
 | 
				
			||||||
| 
						 | 
					@ -105,26 +103,26 @@ proc renderTimelineTweets*(results: Result[Tweet]; prefs: Prefs; path: string;
 | 
				
			||||||
      else:
 | 
					      else:
 | 
				
			||||||
        renderNoneFound()
 | 
					        renderNoneFound()
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
      var
 | 
					      var retweets: seq[int64]
 | 
				
			||||||
        threads: seq[int64]
 | 
					 | 
				
			||||||
        retweets: seq[int64]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for tweet in results.content:
 | 
					      for thread in results.content:
 | 
				
			||||||
        let rt = if tweet.retweet.isSome: get(tweet.retweet).id else: 0
 | 
					        if thread.content.len == 1:
 | 
				
			||||||
 | 
					          let
 | 
				
			||||||
 | 
					            tweet = thread.content[0]
 | 
				
			||||||
 | 
					            retweetId = if tweet.retweet.isSome: get(tweet.retweet).id else: 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if tweet.id in threads or rt in retweets or tweet.id in retweets or
 | 
					          if retweetId in retweets or tweet.id in retweets or
 | 
				
			||||||
           tweet.pinned and prefs.hidePins: continue
 | 
					             tweet.pinned and prefs.hidePins:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let thread = results.content.threadFilter(threads, tweet)
 | 
					 | 
				
			||||||
        if thread.len < 2:
 | 
					 | 
				
			||||||
          var hasThread = tweet.hasThread
 | 
					          var hasThread = tweet.hasThread
 | 
				
			||||||
          if rt != 0:
 | 
					          if retweetId != 0 and tweet.retweet.isSome:
 | 
				
			||||||
            retweets &= rt
 | 
					            retweets &= retweetId
 | 
				
			||||||
            hasThread = get(tweet.retweet).hasThread
 | 
					            hasThread = get(tweet.retweet).hasThread
 | 
				
			||||||
          renderTweet(tweet, prefs, path, showThread=hasThread)
 | 
					          renderTweet(tweet, prefs, path, showThread=hasThread)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
          renderThread(thread, prefs, path)
 | 
					          renderThread(thread.content, prefs, path)
 | 
				
			||||||
          threads &= thread.mapIt(it.id)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if results.bottom.len > 0:
 | 
				
			||||||
        renderMore(results.query, results.bottom)
 | 
					        renderMore(results.query, results.bottom)
 | 
				
			||||||
      renderToTop()
 | 
					      renderToTop()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,15 +14,14 @@ proc renderMiniAvatar(user: User; prefs: Prefs): VNode =
 | 
				
			||||||
  buildHtml():
 | 
					  buildHtml():
 | 
				
			||||||
    img(class=(prefs.getAvatarClass & " mini"), src=url)
 | 
					    img(class=(prefs.getAvatarClass & " mini"), src=url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
proc renderHeader(tweet: Tweet; retweet: string; prefs: Prefs): VNode =
 | 
					proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs): VNode =
 | 
				
			||||||
  buildHtml(tdiv):
 | 
					  buildHtml(tdiv):
 | 
				
			||||||
    if retweet.len > 0:
 | 
					    if pinned:
 | 
				
			||||||
      tdiv(class="retweet-header"):
 | 
					 | 
				
			||||||
        span: icon "retweet", retweet & " retweeted"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if tweet.pinned:
 | 
					 | 
				
			||||||
      tdiv(class="pinned"):
 | 
					      tdiv(class="pinned"):
 | 
				
			||||||
        span: icon "pin", "Pinned Tweet"
 | 
					        span: icon "pin", "Pinned Tweet"
 | 
				
			||||||
 | 
					    elif retweet.len > 0:
 | 
				
			||||||
 | 
					      tdiv(class="retweet-header"):
 | 
				
			||||||
 | 
					        span: icon "retweet", retweet & " retweeted"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tdiv(class="tweet-header"):
 | 
					    tdiv(class="tweet-header"):
 | 
				
			||||||
      a(class="tweet-avatar", href=("/" & tweet.user.username)):
 | 
					      a(class="tweet-avatar", href=("/" & tweet.user.username)):
 | 
				
			||||||
| 
						 | 
					@ -290,7 +289,10 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
 | 
				
			||||||
      if tweet.quote.isSome:
 | 
					      if tweet.quote.isSome:
 | 
				
			||||||
        renderQuote(tweet.quote.get(), prefs, path)
 | 
					        renderQuote(tweet.quote.get(), prefs, path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let fullTweet = tweet
 | 
					  let
 | 
				
			||||||
 | 
					    fullTweet = tweet
 | 
				
			||||||
 | 
					    pinned = tweet.pinned
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var retweet: string
 | 
					  var retweet: string
 | 
				
			||||||
  var tweet = fullTweet
 | 
					  var tweet = fullTweet
 | 
				
			||||||
  if tweet.retweet.isSome:
 | 
					  if tweet.retweet.isSome:
 | 
				
			||||||
| 
						 | 
					@ -303,7 +305,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tdiv(class="tweet-body"):
 | 
					    tdiv(class="tweet-body"):
 | 
				
			||||||
      var views = ""
 | 
					      var views = ""
 | 
				
			||||||
      renderHeader(tweet, retweet, prefs)
 | 
					      renderHeader(tweet, retweet, pinned, prefs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if not afterTweet and index == 0 and tweet.reply.len > 0 and
 | 
					      if not afterTweet and index == 0 and tweet.reply.len > 0 and
 | 
				
			||||||
         (tweet.reply.len > 1 or tweet.reply[0] != tweet.user.username):
 | 
					         (tweet.reply.len > 1 or tweet.reply[0] != tweet.user.username):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,12 @@ card = [
 | 
				
			||||||
    ['FluentAI/status/1116417904831029248',
 | 
					    ['FluentAI/status/1116417904831029248',
 | 
				
			||||||
     'Amazon’s Alexa isn’t just AI — thousands of humans are listening',
 | 
					     'Amazon’s Alexa isn’t just AI — thousands of humans are listening',
 | 
				
			||||||
     'One of the only ways to improve Alexa is to have human beings check it for errors',
 | 
					     'One of the only ways to improve Alexa is to have human beings check it for errors',
 | 
				
			||||||
     'theverge.com', True]
 | 
					     'theverge.com', True],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ['nim_lang/status/1082989146040340480',
 | 
				
			||||||
 | 
					     'Nim in 2018: A short recap',
 | 
				
			||||||
 | 
					     'There were several big news in the Nim world in 2018 – two new major releases, partnership with Status, and much more. But let us go chronologically.',
 | 
				
			||||||
 | 
					     'nim-lang.org', True]
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
no_thumb = [
 | 
					no_thumb = [
 | 
				
			||||||
| 
						 | 
					@ -33,12 +38,7 @@ no_thumb = [
 | 
				
			||||||
    ['voidtarget/status/1133028231672582145',
 | 
					    ['voidtarget/status/1133028231672582145',
 | 
				
			||||||
     'sinkingsugar/nimqt-example',
 | 
					     'sinkingsugar/nimqt-example',
 | 
				
			||||||
     'A sample of a Qt app written using mostly nim. Contribute to sinkingsugar/nimqt-example development by creating an account on GitHub.',
 | 
					     'A sample of a Qt app written using mostly nim. Contribute to sinkingsugar/nimqt-example development by creating an account on GitHub.',
 | 
				
			||||||
     'github.com'],
 | 
					     'github.com']
 | 
				
			||||||
 | 
					 | 
				
			||||||
    ['nim_lang/status/1082989146040340480',
 | 
					 | 
				
			||||||
     'Nim in 2018: A short recap',
 | 
					 | 
				
			||||||
     'Posted by u/miran1 - 36 votes and 46 comments',
 | 
					 | 
				
			||||||
     'reddit.com']
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
playable = [
 | 
					playable = [
 | 
				
			||||||
| 
						 | 
					@ -53,17 +53,6 @@ playable = [
 | 
				
			||||||
     'youtube.com']
 | 
					     'youtube.com']
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# promo = [
 | 
					 | 
				
			||||||
    # ['BangOlufsen/status/1145698701517754368',
 | 
					 | 
				
			||||||
    #  'Upgrade your journey', '',
 | 
					 | 
				
			||||||
    #  'www.bang-olufsen.com'],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # ['BangOlufsen/status/1154934429900406784',
 | 
					 | 
				
			||||||
    #  'Learn more about Beosound Shape', '',
 | 
					 | 
				
			||||||
    #  'www.bang-olufsen.com']
 | 
					 | 
				
			||||||
# ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CardTest(BaseTestCase):
 | 
					class CardTest(BaseTestCase):
 | 
				
			||||||
    @parameterized.expand(card)
 | 
					    @parameterized.expand(card)
 | 
				
			||||||
    def test_card(self, tweet, title, description, destination, large):
 | 
					    def test_card(self, tweet, title, description, destination, large):
 | 
				
			||||||
| 
						 | 
					@ -98,13 +87,3 @@ class CardTest(BaseTestCase):
 | 
				
			||||||
        self.assert_element_visible('.card-overlay')
 | 
					        self.assert_element_visible('.card-overlay')
 | 
				
			||||||
        if len(description) > 0:
 | 
					        if len(description) > 0:
 | 
				
			||||||
            self.assert_text(description, c.description)
 | 
					            self.assert_text(description, c.description)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # @parameterized.expand(promo)
 | 
					 | 
				
			||||||
    # def test_card_promo(self, tweet, title, description, destination):
 | 
					 | 
				
			||||||
    #     self.open_nitter(tweet)
 | 
					 | 
				
			||||||
    #     c = Card(Conversation.main + " ")
 | 
					 | 
				
			||||||
    #     self.assert_text(title, c.title)
 | 
					 | 
				
			||||||
    #     self.assert_text(destination, c.destination)
 | 
					 | 
				
			||||||
    #     self.assert_element_visible('.video-overlay')
 | 
					 | 
				
			||||||
    #     if len(description) > 0:
 | 
					 | 
				
			||||||
    #         self.assert_text(description, c.description)
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,8 +66,8 @@ class ProfileTest(BaseTestCase):
 | 
				
			||||||
        self.assert_text(f'User "{username}" not found')
 | 
					        self.assert_text(f'User "{username}" not found')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_suspended(self):
 | 
					    def test_suspended(self):
 | 
				
			||||||
        self.open_nitter('user')
 | 
					        self.open_nitter('suspendme')
 | 
				
			||||||
        self.assert_text('User "user" has been suspended')
 | 
					        self.assert_text('User "suspendme" has been suspended')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @parameterized.expand(banner_image)
 | 
					    @parameterized.expand(banner_image)
 | 
				
			||||||
    def test_banner_image(self, username, url):
 | 
					    def test_banner_image(self, username, url):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,8 +2,8 @@ from base import BaseTestCase
 | 
				
			||||||
from parameterized import parameterized
 | 
					from parameterized import parameterized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchTest(BaseTestCase):
 | 
					#class SearchTest(BaseTestCase):
 | 
				
			||||||
    @parameterized.expand([['@mobile_test'], ['@mobile_test_2']])
 | 
					    #@parameterized.expand([['@mobile_test'], ['@mobile_test_2']])
 | 
				
			||||||
    def test_username_search(self, username):
 | 
					    #def test_username_search(self, username):
 | 
				
			||||||
        self.search_username(username)
 | 
					        #self.search_username(username)
 | 
				
			||||||
        self.assert_text(f'{username}')
 | 
					        #self.assert_text(f'{username}')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -74,9 +74,9 @@ retweet = [
 | 
				
			||||||
    [3, 'mobile_test_8', 'mobile test 8', 'jack', '@jack', 'twttr']
 | 
					    [3, 'mobile_test_8', 'mobile test 8', 'jack', '@jack', 'twttr']
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
reply = [
 | 
					# reply = [
 | 
				
			||||||
    ['mobile_test/with_replies', 15]
 | 
					#     ['mobile_test/with_replies', 15]
 | 
				
			||||||
]
 | 
					# ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TweetTest(BaseTestCase):
 | 
					class TweetTest(BaseTestCase):
 | 
				
			||||||
| 
						 | 
					@ -137,8 +137,8 @@ class TweetTest(BaseTestCase):
 | 
				
			||||||
        self.open_nitter(tweet)
 | 
					        self.open_nitter(tweet)
 | 
				
			||||||
        self.assert_text('Tweet not found', '.error-panel')
 | 
					        self.assert_text('Tweet not found', '.error-panel')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @parameterized.expand(reply)
 | 
					    # @parameterized.expand(reply)
 | 
				
			||||||
    def test_thread(self, tweet, num):
 | 
					    # def test_thread(self, tweet, num):
 | 
				
			||||||
        self.open_nitter(tweet)
 | 
					    #     self.open_nitter(tweet)
 | 
				
			||||||
        thread = self.find_element(f'.timeline > div:nth-child({num})')
 | 
					    #     thread = self.find_element(f'.timeline > div:nth-child({num})')
 | 
				
			||||||
        self.assertIn(thread.get_attribute('class'), 'thread-line')
 | 
					    #     self.assertIn(thread.get_attribute('class'), 'thread-line')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue