mirror of
https://github.com/zedeus/nitter.git
synced 2026-03-04 13:19:57 -05:00
@@ -138,6 +138,13 @@ proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
|
||||
if after.len > 0:
|
||||
result.replies = await getReplies(id, after)
|
||||
|
||||
proc getGraphEditHistory*(id: string): Future[EditHistory] {.async.} =
|
||||
if id.len == 0: return
|
||||
let
|
||||
url = apiReq(graphTweetEditHistory, tweetEditHistoryVars % id)
|
||||
js = await fetch(url)
|
||||
result = parseGraphEditHistory(js, id)
|
||||
|
||||
proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} =
|
||||
let q = genQueryParam(query)
|
||||
if q.len == 0 or q == emptyQuery:
|
||||
|
||||
@@ -19,6 +19,7 @@ const
|
||||
graphTweet* = "Y4Erk_-0hObvLpz0Iw3bzA/ConversationTimeline"
|
||||
graphTweetDetail* = "YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail"
|
||||
graphTweetResult* = "nzme9KiYhfIOrrLrPP_XeQ/TweetResultByIdQuery"
|
||||
graphTweetEditHistory* = "upS9teTSG45aljmP9oTuXA/TweetEditHistory"
|
||||
graphSearchTimeline* = "bshMIjqDk8LTXTq4w91WKw/SearchTimeline"
|
||||
graphListById* = "cIUpT1UjuGgl_oWiY7Snhg/ListByRestId"
|
||||
graphListBySlug* = "K6wihoTiTrzNzSF8y1aeKQ/ListBySlug"
|
||||
@@ -29,35 +30,67 @@ const
|
||||
"android_ad_formats_media_component_render_overlay_enabled": false,
|
||||
"android_graphql_skip_api_media_color_palette": false,
|
||||
"android_professional_link_spotlight_display_enabled": false,
|
||||
"articles_api_enabled": false,
|
||||
"articles_preview_enabled": true,
|
||||
"blue_business_profile_image_shape_enabled": false,
|
||||
"c9s_tweet_anatomy_moderator_badge_enabled": true,
|
||||
"commerce_android_shop_module_enabled": false,
|
||||
"communities_web_enable_tweet_community_results_fetch": true,
|
||||
"creator_subscriptions_quote_tweet_preview_enabled": false,
|
||||
"creator_subscriptions_subscription_count_enabled": false,
|
||||
"creator_subscriptions_tweet_preview_api_enabled": true,
|
||||
"freedom_of_speech_not_reach_fetch_enabled": true,
|
||||
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
|
||||
"grok_android_analyze_trend_fetch_enabled": false,
|
||||
"grok_translations_community_note_auto_translation_is_enabled": false,
|
||||
"grok_translations_community_note_translation_is_enabled": false,
|
||||
"grok_translations_post_auto_translation_is_enabled": false,
|
||||
"grok_translations_timeline_user_bio_auto_translation_is_enabled": false,
|
||||
"hidden_profile_likes_enabled": false,
|
||||
"highlights_tweets_tab_ui_enabled": false,
|
||||
"immersive_video_status_linkable_timestamps": false,
|
||||
"interactive_text_enabled": false,
|
||||
"longform_notetweets_consumption_enabled": true,
|
||||
"longform_notetweets_inline_media_enabled": true,
|
||||
"longform_notetweets_rich_text_read_enabled": true,
|
||||
"longform_notetweets_richtext_consumption_enabled": true,
|
||||
"longform_notetweets_rich_text_read_enabled": true,
|
||||
"mobile_app_spotlight_module_enabled": false,
|
||||
"payments_enabled": false,
|
||||
"post_ctas_fetch_enabled": true,
|
||||
"premium_content_api_read_enabled": false,
|
||||
"profile_label_improvements_pcf_label_in_post_enabled": true,
|
||||
"profile_label_improvements_pcf_label_in_profile_enabled": false,
|
||||
"responsive_web_edit_tweet_api_enabled": true,
|
||||
"responsive_web_enhance_cards_enabled": false,
|
||||
"responsive_web_graphql_exclude_directive_enabled": true,
|
||||
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
|
||||
"responsive_web_graphql_timeline_navigation_enabled": true,
|
||||
"responsive_web_grok_analysis_button_from_backend": true,
|
||||
"responsive_web_grok_analyze_button_fetch_trends_enabled": false,
|
||||
"responsive_web_grok_analyze_post_followups_enabled": true,
|
||||
"responsive_web_grok_annotations_enabled": true,
|
||||
"responsive_web_grok_community_note_auto_translation_is_enabled": false,
|
||||
"responsive_web_grok_image_annotation_enabled": true,
|
||||
"responsive_web_grok_imagine_annotation_enabled": true,
|
||||
"responsive_web_grok_share_attachment_enabled": true,
|
||||
"responsive_web_grok_show_grok_translated_post": false,
|
||||
"responsive_web_jetfuel_frame": true,
|
||||
"responsive_web_media_download_video_enabled": false,
|
||||
"responsive_web_profile_redirect_enabled": false,
|
||||
"responsive_web_text_conversations_enabled": false,
|
||||
"responsive_web_twitter_article_notes_tab_enabled": false,
|
||||
"responsive_web_twitter_article_tweet_consumption_enabled": true,
|
||||
"unified_cards_destination_url_params_enabled": false,
|
||||
"responsive_web_twitter_blue_verified_badge_is_enabled": true,
|
||||
"rweb_lists_timeline_redesign_enabled": true,
|
||||
"rweb_tipjar_consumption_enabled": true,
|
||||
"rweb_video_screen_enabled": false,
|
||||
"rweb_video_timestamps_enabled": false,
|
||||
"spaces_2022_h2_clipping": true,
|
||||
"spaces_2022_h2_spaces_communities": true,
|
||||
"standardized_nudges_misinfo": true,
|
||||
"subscriptions_feature_can_gift_premium": false,
|
||||
"subscriptions_verification_info_enabled": true,
|
||||
"subscriptions_verification_info_is_identity_verified_enabled": false,
|
||||
"subscriptions_verification_info_reason_enabled": true,
|
||||
"subscriptions_verification_info_verified_since_enabled": true,
|
||||
"super_follow_badge_privacy_enabled": false,
|
||||
@@ -68,40 +101,10 @@ const
|
||||
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
|
||||
"tweetypie_unmention_optimization_enabled": false,
|
||||
"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
|
||||
"unified_cards_destination_url_params_enabled": false,
|
||||
"verified_phone_label_enabled": false,
|
||||
"vibe_api_enabled": false,
|
||||
"view_counts_everywhere_api_enabled": true,
|
||||
"premium_content_api_read_enabled": false,
|
||||
"communities_web_enable_tweet_community_results_fetch": true,
|
||||
"responsive_web_jetfuel_frame": true,
|
||||
"responsive_web_grok_analyze_button_fetch_trends_enabled": false,
|
||||
"responsive_web_grok_image_annotation_enabled": true,
|
||||
"responsive_web_grok_imagine_annotation_enabled": true,
|
||||
"rweb_tipjar_consumption_enabled": true,
|
||||
"profile_label_improvements_pcf_label_in_post_enabled": true,
|
||||
"creator_subscriptions_quote_tweet_preview_enabled": false,
|
||||
"c9s_tweet_anatomy_moderator_badge_enabled": true,
|
||||
"responsive_web_grok_analyze_post_followups_enabled": true,
|
||||
"rweb_video_timestamps_enabled": false,
|
||||
"responsive_web_grok_share_attachment_enabled": true,
|
||||
"articles_preview_enabled": true,
|
||||
"immersive_video_status_linkable_timestamps": false,
|
||||
"articles_api_enabled": false,
|
||||
"responsive_web_grok_analysis_button_from_backend": true,
|
||||
"rweb_video_screen_enabled": false,
|
||||
"payments_enabled": false,
|
||||
"responsive_web_profile_redirect_enabled": false,
|
||||
"responsive_web_grok_show_grok_translated_post": false,
|
||||
"responsive_web_grok_community_note_auto_translation_is_enabled": false,
|
||||
"profile_label_improvements_pcf_label_in_profile_enabled": false,
|
||||
"grok_android_analyze_trend_fetch_enabled": false,
|
||||
"grok_translations_community_note_auto_translation_is_enabled": false,
|
||||
"grok_translations_post_auto_translation_is_enabled": false,
|
||||
"grok_translations_community_note_translation_is_enabled": false,
|
||||
"grok_translations_timeline_user_bio_auto_translation_is_enabled": false,
|
||||
"subscriptions_feature_can_gift_premium": false,
|
||||
"responsive_web_twitter_article_notes_tab_enabled": false,
|
||||
"subscriptions_verification_info_is_identity_verified_enabled": false,
|
||||
"hidden_profile_subscriptions_enabled": false
|
||||
}""".replace(" ", "").replace("\n", "")
|
||||
|
||||
@@ -128,6 +131,11 @@ const
|
||||
"withVoice": true
|
||||
}""".replace(" ", "").replace("\n", "")
|
||||
|
||||
tweetEditHistoryVars* = """{
|
||||
"tweetId": "$1",
|
||||
"withQuickPromoteEligibilityTweetFields": true
|
||||
}""".replace(" ", "").replace("\n", "")
|
||||
|
||||
restIdVars* = """{
|
||||
"rest_id": "$1", $2
|
||||
"count": 20
|
||||
|
||||
@@ -165,13 +165,17 @@ proc getDuration*(video: Video): string =
|
||||
else:
|
||||
return &"{min mod 60}:{sec mod 60:02}"
|
||||
|
||||
proc getLink*(id: int64; username="i"; focus=true): string =
|
||||
var username = username
|
||||
if username.len == 0:
|
||||
username = "i"
|
||||
result = &"/{username}/status/{id}"
|
||||
if focus: result &= "#m"
|
||||
|
||||
proc getLink*(tweet: Tweet; focus=true): string =
|
||||
if tweet.id == 0: return
|
||||
var username = tweet.user.username
|
||||
if username.len == 0:
|
||||
username = "i"
|
||||
result = &"/{username}/status/{tweet.id}"
|
||||
if focus: result &= "#m"
|
||||
return getLink(tweet.id, username, focus)
|
||||
|
||||
proc getTwitterLink*(path: string; params: Table[string, string]): string =
|
||||
var
|
||||
|
||||
@@ -435,12 +435,16 @@ proc parseGraphTweet(js: JsonNode): Tweet =
|
||||
else:
|
||||
result.quote = some Tweet(id: js{"legacy", "quoted_status_id_str"}.getId)
|
||||
|
||||
with ids, js{"edit_control", "edit_control_initial", "edit_tweet_ids"}:
|
||||
for id in ids:
|
||||
result.history.add parseBiggestInt(id.getStr)
|
||||
|
||||
proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] =
|
||||
for t in ? js{"content", "items"}:
|
||||
let entryId = t.getEntryId
|
||||
if "tweet-" in entryId and "promoted" notin entryId:
|
||||
let tweet = t.getTweetResult("item")
|
||||
if not tweet.isNull:
|
||||
if tweet.notNull:
|
||||
result.thread.content.add parseGraphTweet(tweet)
|
||||
|
||||
let tweetDisplayType = select(
|
||||
@@ -516,6 +520,29 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
|
||||
)
|
||||
result.replies.bottom = cursorValue.getStr
|
||||
|
||||
proc parseGraphEditHistory*(js: JsonNode; tweetId: string): EditHistory =
|
||||
let instructions = ? js{
|
||||
"data", "tweet_result_by_rest_id", "result",
|
||||
"edit_history_timeline", "timeline", "instructions"
|
||||
}
|
||||
if instructions.len == 0:
|
||||
return
|
||||
|
||||
for i in instructions:
|
||||
if i.getTypeName == "TimelineAddEntries":
|
||||
for e in i{"entries"}:
|
||||
let entryId = e.getEntryId
|
||||
if entryId == "latestTweet":
|
||||
with item, e{"content", "items"}[0]:
|
||||
let tweetResult = item.getTweetResult("item")
|
||||
if tweetResult.notNull:
|
||||
result.latest = parseGraphTweet(tweetResult)
|
||||
elif entryId == "staleTweets":
|
||||
for item in e{"content", "items"}:
|
||||
let tweetResult = item.getTweetResult("item")
|
||||
if tweetResult.notNull:
|
||||
result.history.add parseGraphTweet(tweetResult)
|
||||
|
||||
proc extractTweetsFromEntry*(e: JsonNode): seq[Tweet] =
|
||||
with tweetResult, getTweetResult(e):
|
||||
var tweet = parseGraphTweet(tweetResult)
|
||||
|
||||
@@ -64,9 +64,29 @@ proc createStatusRouter*(cfg: Config) =
|
||||
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
|
||||
images=images, video=video)
|
||||
|
||||
get "/@name/status/@id/history/?":
|
||||
cond '.' notin @"name"
|
||||
let id = @"id"
|
||||
|
||||
if id.len > 19 or id.any(c => not c.isDigit):
|
||||
resp Http404, showError("Invalid tweet ID", cfg)
|
||||
|
||||
let edits = await getGraphEditHistory(id)
|
||||
if edits.latest == nil or edits.latest.id == 0:
|
||||
resp Http404, showError("Tweet history not found", cfg)
|
||||
|
||||
let
|
||||
prefs = requestPrefs()
|
||||
title = "History for " & pageTitle(edits.latest)
|
||||
ogTitle = "Edit History for " & pageTitle(edits.latest.user)
|
||||
desc = edits.latest.text
|
||||
|
||||
let html = renderEditHistory(edits, prefs, getPath())
|
||||
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle)
|
||||
|
||||
get "/@name/@s/@id/@m/?@i?":
|
||||
cond @"s" in ["status", "statuses"]
|
||||
cond @"m" in ["video", "photo", "history"]
|
||||
cond @"m" in ["video", "photo"]
|
||||
redirect("/$1/status/$2" % [@"name", @"id"])
|
||||
|
||||
get "/@name/statuses/@id/?":
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
@include panel(100%, 600px);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
background-color: var(--bg_panel);
|
||||
|
||||
> div:not(:first-child) {
|
||||
border-top: 1px solid var(--border_grey);
|
||||
}
|
||||
.timeline > div:not(:first-child) {
|
||||
border-top: 1px solid var(--border_grey);
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
@@ -159,4 +155,5 @@
|
||||
padding: 0.75em;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background-color: var(--bg_panel);
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
}
|
||||
|
||||
.tweet-published {
|
||||
margin: 0;
|
||||
margin-top: 5px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 3px;
|
||||
color: var(--grey);
|
||||
pointer-events: all;
|
||||
}
|
||||
@@ -242,3 +242,15 @@
|
||||
background-color: var(--bg_hover);
|
||||
}
|
||||
}
|
||||
|
||||
.latest-post-version {
|
||||
border-bottom: 1px solid var(--dark_grey);
|
||||
border-top: 1px solid var(--dark_grey);
|
||||
padding: 01ch 0px;
|
||||
margin: 1ch 0px;
|
||||
color: var(--grey);
|
||||
|
||||
a {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.quote-latest {
|
||||
padding: 0px 8px 6px 8px;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.replying-to {
|
||||
padding: 0px 8px;
|
||||
margin: unset;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@import "_variables";
|
||||
@import "_mixins";
|
||||
|
||||
.conversation {
|
||||
.conversation,
|
||||
.edit-history {
|
||||
@include panel(100%, 600px);
|
||||
|
||||
.show-more {
|
||||
@@ -9,19 +10,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.main-thread {
|
||||
.main-thread,
|
||||
.latest-edit {
|
||||
margin-bottom: 20px;
|
||||
background-color: var(--bg_panel);
|
||||
}
|
||||
|
||||
.reply {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.main-tweet,
|
||||
.replies {
|
||||
.replies,
|
||||
.edit-history > div {
|
||||
body.fixed-nav & {
|
||||
padding-top: 50px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-history-header {
|
||||
padding: 10px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background-color: var(--bg_panel);
|
||||
}
|
||||
|
||||
.tweet-edit {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.main-tweet .tweet-content {
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -32,11 +50,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.reply {
|
||||
background-color: var(--bg_panel);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.thread-line {
|
||||
.timeline-item::before,
|
||||
&.timeline-item::before {
|
||||
|
||||
@@ -222,6 +222,7 @@ type
|
||||
gif*: Option[Gif]
|
||||
video*: Option[Video]
|
||||
photos*: seq[Photo]
|
||||
history*: seq[int64]
|
||||
|
||||
Tweets* = seq[Tweet]
|
||||
|
||||
@@ -242,6 +243,10 @@ type
|
||||
after*: Chain
|
||||
replies*: Result[Chain]
|
||||
|
||||
EditHistory* = object
|
||||
latest*: Tweet
|
||||
history*: Tweets
|
||||
|
||||
Timeline* = Result[Tweets]
|
||||
|
||||
Profile* = object
|
||||
|
||||
@@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
||||
|
||||
buildHtml(head):
|
||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=26")
|
||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=27")
|
||||
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
|
||||
|
||||
if theme.len > 0:
|
||||
|
||||
@@ -78,3 +78,17 @@ proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode
|
||||
renderReplies(conv.replies, prefs, path, conv.tweet)
|
||||
|
||||
renderToTop(focus="#m")
|
||||
|
||||
proc renderEditHistory*(edits: EditHistory; prefs: Prefs; path: string): VNode =
|
||||
buildHtml(tdiv(class="edit-history")):
|
||||
tdiv(class="latest-edit"):
|
||||
tdiv(class="edit-history-header"):
|
||||
text "Latest post"
|
||||
renderTweet(edits.latest, prefs, path)
|
||||
|
||||
tdiv(class="previous-edits"):
|
||||
tdiv(class="edit-history-header"):
|
||||
text "Version history"
|
||||
for tweet in edits.history:
|
||||
tdiv(class="tweet-edit"):
|
||||
renderTweet(tweet, prefs, path)
|
||||
|
||||
@@ -211,6 +211,12 @@ proc renderMediaTags(tags: seq[User]): VNode =
|
||||
if i < tags.high:
|
||||
text ", "
|
||||
|
||||
proc renderLatestPost(username: string; id: int64): VNode =
|
||||
buildHtml(tdiv(class="latest-post-version")):
|
||||
text "There's a new version of this post. "
|
||||
a(href=getLink(id, username)):
|
||||
text "See the latest post"
|
||||
|
||||
proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||
buildHtml(tdiv(class="quote-media-container")):
|
||||
if quote.photos.len > 0:
|
||||
@@ -252,12 +258,16 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||
tdiv(class="quote-text", dir="auto"):
|
||||
verbatim replaceUrls(quote.text, prefs)
|
||||
|
||||
if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
|
||||
renderQuoteMedia(quote, prefs, path)
|
||||
|
||||
if quote.hasThread:
|
||||
a(class="show-thread", href=getLink(quote)):
|
||||
text "Show this thread"
|
||||
|
||||
if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
|
||||
renderQuoteMedia(quote, prefs, path)
|
||||
if quote.history.len > 0 and quote.id != max(quote.history):
|
||||
tdiv(class="quote-latest"):
|
||||
text "There's a new version of this post"
|
||||
|
||||
proc renderLocation*(tweet: Tweet): string =
|
||||
let (place, url) = tweet.getLocation()
|
||||
@@ -336,8 +346,20 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
|
||||
if tweet.quote.isSome:
|
||||
renderQuote(tweet.quote.get(), prefs, path)
|
||||
|
||||
let
|
||||
hasEdits = tweet.history.len > 1
|
||||
isLatest = hasEdits and tweet.id == max(tweet.history)
|
||||
|
||||
if mainTweet:
|
||||
p(class="tweet-published"): text &"{getTime(tweet)}"
|
||||
p(class="tweet-published"):
|
||||
if hasEdits and isLatest:
|
||||
a(href=(getLink(tweet, focus=false) & "/history")):
|
||||
text &"Last edited {getTime(tweet)}"
|
||||
else:
|
||||
text &"{getTime(tweet)}"
|
||||
|
||||
if hasEdits and not isLatest:
|
||||
renderLatestPost(tweet.user.username, max(tweet.history))
|
||||
|
||||
if tweet.mediaTags.len > 0:
|
||||
renderMediaTags(tweet.mediaTags)
|
||||
|
||||
Reference in New Issue
Block a user