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:
Zed 2023-07-10 11:25:34 +02:00 committed by GitHub
parent dcf73354ff
commit 0bc3c153d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 260 additions and 264 deletions

View File

@ -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:

View File

@ -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
}""" }"""

View File

@ -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](

View File

@ -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]

View File

@ -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)

View File

@ -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"

View File

@ -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": ""

View File

@ -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)

View File

@ -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())

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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])

View File

@ -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 = ""

View File

@ -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")):

View File

@ -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()

View File

@ -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):

View File

@ -16,7 +16,12 @@ card = [
['FluentAI/status/1116417904831029248', ['FluentAI/status/1116417904831029248',
'Amazons Alexa isnt just AI — thousands of humans are listening', 'Amazons Alexa isnt 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)

View File

@ -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):

View File

@ -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}')

View File

@ -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')