yang991178 / fluent-reader

Modern desktop RSS reader built with Electron, React, and Fluent UI
https://hyliu.me/fluent-reader/
BSD 3-Clause "New" or "Revised" License
7.66k stars 417 forks source link

[Feature Request] Twitter feeds? #454

Open Darthagnon opened 2 years ago

Darthagnon commented 2 years ago

No, it's impossible. If your reader can support adding local RSS and loading article images, it may still be possible. thank you.

chrome_wwSVFbRSz1 Not sure what you mean by "image articles", but FeedBro supports it, loading plaintext and images. They must be using a custom workaround, maybe something like this. Currently, attempts to add a Twitter feed to FluentReader gives an XML parse error (possibly related: #429)

Originally posted by @Darthagnon in https://github.com/yang991178/fluent-reader/issues/23#issuecomment-1224512243

Darthagnon commented 2 years ago

Been doing some more research. Here's the blog article explaining how a dev implemented RSS feeds from twitter back in the day (and here is his Gist.

I've also taken a brief look at the Feedbro source (available, once you install the Feedbro extension, at C:\Users\Username\AppData\Local\Publisher\Browser\User Data\Default\Extensions\mefgmmbdailogpfhfblcnnjfmnpnmdfa\version\scripts-core\feedbro.min.js

Darthagnon commented 2 years ago

Relevant snippet of feedbro.min.js

I don't fully understand it, but seeing an authtoken and a bunch of references to graphQL, I'm wondering if Feedbro is abusing a workaround in Twitter's social network sharing graphQL implementation to extract images and stringified text?

Click me to see the code; I am very very long (~760 lines JS) ```js feedbro.TwPlugin.authToken = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; feedbro.TwPlugin.cachedToken = null; feedbro.TwPlugin.prototype.canParse = function (b) { if (b == undefined || b == "") { return false } b = this.mapUrl(b); var c = b.substring(0, 20); var a = b.substring(20); return c == "https://twitter.com/" && a != "notifications" && a != "home" && a.indexOf("messages") != 0 && a.indexOf("i/bookmarks") != 0 }; feedbro.TwPlugin.prototype.mapUrl = function (a) { if (a && a.substring(0, 26) == "https://mobile.twitter.com") { a = "https://twitter.com/" + a.substring(27) } return a }; feedbro.TwPlugin.prototype.parse = function (d, c, a, e) { var b = this; d.url = this.mapUrl(d.url); this.getCachedGuestToken(function (f) { b.doParse(d, c, a, e) }) }; feedbro.TwPlugin.prototype.getCachedGuestToken = function (a) { if (feedbro.TwPlugin.cachedToken == null || this.isTokenExpired(feedbro.TwPlugin.cachedToken)) { this.getGuestToken(function (b) { if (b && b.token) { feedbro.TwPlugin.cachedToken = b } else { feedbro.TwPlugin.cachedToken = { remaining: 0, init: 0 } } a(b) }) } else { feedbro.TwPlugin.cachedToken.lastUse = new Date().getTime(); a(feedbro.TwPlugin.cachedToken) } }; feedbro.TwPlugin.prototype.getGuestToken = function (a) { fetchTimeout("https://api.twitter.com/1.1/guest/activate.json", 20000, { method: "POST", credentials: "omit", headers: { authorization: "Bearer " + feedbro.TwPlugin.authToken, "accept-language": "en-US,en;q=0.5", accept: "text/html,application/json,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" } }).then(function (b) { return b.json() }).then(function (c) { var b = new Date().getTime(); if (c == null) { c = {} } a({ token: c.guest_token, remaining: 187, init: b, reset: b + (15 * 60 * 1000), lastUse: b }) })["catch"](a) }; feedbro.TwPlugin.prototype.isTokenExpired = function (b) { var d = 2 * 60 * 60 * 1000; var c = 1 * 60 * 60 * 1000; var a = new Date().getTime(); return b == null || b.init < (a - d) || b.remaining < 10 || a > b.reset || b.lastUse < (a - c) }; feedbro.TwPlugin.prototype.doParse = function (f, d, a, g) { var b = this; try { this.getUrl(f.url, a, function (l, j, h, k) { try { if (j && j.globalObjects) { d(f.url, l, b.parseTweets(j, h, k), a, g, true) } else { d(f.url, l, b.parseTimeline(j, h, k), a, g, true) } } catch (i) { d(f.url, l, "", a, g, true) } }) } catch (c) { var e = new feedbro.FetchWrapper(500); a.error = {}; a.error.message = "Internal Server Error"; a.error.code = 500; a.status = 500; d(f.url, e, "", a, g, true) } }; feedbro.TwPlugin.prototype.getUserProfile = function (c, a, e) { var d = c.match(/^https:\/\/twitter\.com\/(\w+)/)[1]; var b = "https://twitter.com/i/api/graphql/7mjxD3-C6BxitPMVQ6w0-Q/UserByScreenName?variables=%7B%22screen_name%22%3A%22" + d + "%22%2C%22withSafetyModeUserFields%22%3Atrue%2C%22withSuperFollowsUserFields%22%3Atrue%7D"; this.loadJson(b, a, function (g, f) { e(f, c) }) }; feedbro.TwPlugin.prototype.getThread = function (c, a, d) { var b = this; this.getUserProfile(c, a, function (f, e) { b.loadThread(e, f, a, d) }) }; feedbro.TwPlugin.prototype.loadThread = function (a, c, i, h) { var f = 0; var d = a.match(/status\/(\d+)/); if (d) { f = d[1] } var e = 0; if (c && c.data && c.data.user) { if (c.data.user.result) { e = c.data.user.result.rest_id } else { e = c.data.user.rest_id } } var b = { focalTweetId: f, with_rux_injections: false, includePromotedContent: true, withCommunity: true, withQuickPromoteEligibilityTweetFields: true, withBirdwatchNotes: false, withSuperFollowsUserFields: true, withDownvotePerspective: false, withReactionsMetadata: false, withReactionsPerspective: false, withSuperFollowsTweetFields: true, withVoice: true, withV2Timeline: false, __fs_dont_mention_me_view_api_enabled: false, __fs_interactive_text_enabled: true, __fs_responsive_web_uc_gql_enabled: false }; var g = "https://twitter.com/i/api/graphql/LJ_TjoWGgNTXCl7gfx4Njw/TweetDetail?variables=" + encodeURIComponent(JSON.stringify(b)); this.loadJson(g, i, function (l, j) { var k; if (c && c.data && c.data.user) { if (c.data.user.result) { k = c.data.user.result.legacy } else { if (c.data.user.legacy) { k = c.data.user.legacy } } } if (k) { h(l, j, a, e != 0 ? (k.name + " (@" + k.screen_name + ") Thread " + f + " | Twitter") : "") } else { h(l, j, a, "Suspended") } }) }; feedbro.TwPlugin.prototype.loadUserTweets = function (c, e, a, g) { var d = 0; if (e && e.data && e.data.user) { if (e.data.user.result) { d = e.data.user.result.rest_id } else { d = e.data.user.rest_id } } var f = { userId: d, count: 40, includePromotedContent: true, withQuickPromoteEligibilityTweetFields: true, withSuperFollowsUserFields: true, withBirdwatchPivots: false, withDownvotePerspective: false, withReactionsMetadata: false, withReactionsPerspective: false, withSuperFollowsTweetFields: true, withVoice: true, withV2Timeline: false, __fs_interactive_text: false, __fs_dont_mention_me_view_api_enabled: false }; var b = "https://twitter.com/i/api/graphql/DhQ8lYnLh5T5K8aVUgHVnQ/UserTweets?variables=" + encodeURIComponent(JSON.stringify(f)); this.loadJson(b, a, function (j, h) { var i; if (e && e.data && e.data.user) { if (e.data.user.result) { i = e.data.user.result.legacy } else { if (e.data.user.legacy) { i = e.data.user.legacy } } } if (i) { g(j, h, c, d != 0 ? (i.name + " (@" + i.screen_name + ") | Twitter") : "") } else { g(j, h, c, "Suspended") } }) }; feedbro.TwPlugin.prototype.getUserTweets = function (c, a, d) { var b = this; this.getUserProfile(c, a, function (f, e) { b.loadUserTweets(e, f, a, d) }) }; feedbro.TwPlugin.prototype.getListTweets = function (d, a, f) { var b = d.match(/^https:\/\/twitter\.com\/i\/lists\/(\d+)/)[1]; var e = { listId: b, count: 20, withSuperFollowsUserFields: true, withBirdwatchPivots: false, withDownvotePerspective: false, withReactionsMetadata: false, withReactionsPerspective: false, withSuperFollowsTweetFields: true, __fs_interactive_text: false, __fs_dont_mention_me_view_api_enabled: false }; var c = "https://twitter.com/i/api/graphql/mwIBwcZV981Bnjb2lPNYfw/ListLatestTweetsTimeline?variables=" + encodeURIComponent(JSON.stringify(e)); this.loadJson(c, a, function (h, g) { f(h, g, d, "List " + b) }) }; feedbro.TwPlugin.prototype.getHashtagTweets = function (g, d, i) { var e = g.indexOf("hashtag/") + 8; var c = g.indexOf("?", e); var h = g.substring(e, c > 0 ? c : g.length); var f = "https://api.twitter.com/2/search/adaptive.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweet=true&q=%23" + h + "&count=20&query_source=hashtag_click&pc=1&spelling_corrections=1&ext=mediaStats%2ChighlightedLabel%2CcameraMoment&include_quote_count=true"; if (g.indexOf("&f=live") != -1) { f = f + "&tweet_search_mode=live" } this.loadJson(f, d, function (b, a) { i(b, a, g, "#" + decodeURIComponent(h) + " - Search") }) }; feedbro.TwPlugin.prototype.getSearchTweets = function (g, d, i) { var e = g.indexOf("q=") + 2; var c = g.indexOf("&", e); var h = g.substring(e, c > 0 ? c : g.length); var f = "https://api.twitter.com/2/search/adaptive.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweet=true&q=" + h + "&count=20&query_source=typed_query&pc=1&spelling_corrections=1&ext=mediaStats%2ChighlightedLabel%2CcameraMoment&include_quote_count=true"; if (g.indexOf("&f=live") != -1) { f = f + "&tweet_search_mode=live" } this.loadJson(f, d, function (b, a) { i(b, a, g, decodeURIComponent(h) + " - Search") }) }; feedbro.TwPlugin.prototype.getExploreTweets = function (c, a, d) { var b = "https://api.twitter.com/2/guide.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweet=true&include_page_configuration=true&count=20&ext=mediaStats%2ChighlightedLabel%2CcameraMoment&include_quote_count=true"; this.loadJson(b, a, function (f, e) { d(f, e, c, "Explore") }) }; feedbro.TwPlugin.prototype.loadJson = function (c, b, d) { var a = new feedbro.FetchWrapper(500); fetchTimeout(c, 20000, { method: "GET", credentials: "omit", headers: { authorization: "Bearer " + feedbro.TwPlugin.authToken, "accept-language": "en-US,en;q=0.5", accept: "*/*", "x-guest-token": feedbro.TwPlugin.cachedToken ? feedbro.TwPlugin.cachedToken.token : "", "x-twitter-client-language": "en", "x-twitter-active-user": "yes", "cache-control": "no-cache" } }).then(function (e) { a.status = e.status + 0; if (e.status == 200) { if (feedbro.TwPlugin.cachedToken) { feedbro.TwPlugin.cachedToken.remaining = parseInt(e.headers.get("x-rate-limit-remaining") || "0", 10); feedbro.TwPlugin.cachedToken.reset = parseInt(e.headers.get("x-rate-limit-reset"), 10) * 1000 } return e.json() } else { throw new Error(e.status + 0) } }).then(function (e) { d(a, e) })["catch"](function (e) { if (e.name == "AbortError") { b.error = {}; b.error.message = "Network error"; b.error.code = 398; b.status = 398; a.status = 0 } d(a, null) }) }; feedbro.TwPlugin.prototype.getUrl = function (c, a, d) { var b = c.substring(20); if (b.indexOf("i/lists/") == 0) { this.getListTweets(c, a, d) } else { if (b.indexOf("hashtag/") == 0) { this.getHashtagTweets(c, a, d) } else { if (b.indexOf("search?") == 0) { this.getSearchTweets(c, a, d) } else { if (b.indexOf("explore") == 0) { this.getExploreTweets(c, a, d) } else { if (b != "notifications" && b.indexOf("messages") != 0 && b.indexOf("i/bookmarks") != 0) { this.getUserTweets(c, a, d) } else { d(undefined) } } } } } }; feedbro.TwPlugin.prototype.parseTweets = function (B, c, C) { var t = '\n\n\n'; if (B != undefined) { var j = /#(\D\S+)/g; var g = /@(\w+)/g; var r = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; var k = []; var b; var q = B.globalObjects.tweets; var n = B.globalObjects.users; var u, a, D, m = Object.keys(q); var y; var w; var p; var e; var s; var o; var f; var x; var A; var l; var h; var v; var z; m.sort(function (E, i) { return new Date(q[i].created_at).getTime() - new Date(q[E].created_at).getTime() }); try { for (u = 0; u < m.length; u++) { a = q[m[u]]; x = a; if (a.source == 0) { continue } D = n[a.user_id_str]; o = D; f = a.id_str; s = a.created_at; if (a.retweeted_status_id_str != undefined) { z = a.retweeted_status_id_str; a = q[z]; D = n[a.user_id_str]; q[z].source = 0 } A = undefined; e = undefined; if (a.is_quote_status) { A = a.full_text; if (q[a.quoted_status_id_str] != undefined) { e = q[a.quoted_status_id_str]; q[a.quoted_status_id_str].source = 0 } } y = undefined; w = undefined; v = this.getContent(a); y = v.card; w = v.media; p = this.getAuthorHeader(D, x.retweeted_status_id_str != undefined ? o : null, a); b = { author: o.name + " (@" + o.screen_name + ")", publishedDate: new Date(s), guid: o.screen_name + "-" + f, link: "https://twitter.com/" + o.screen_name + "/status/" + f, content: a.full_text, title: this._strings.truncate((A || a.full_text).replace(r, ""), 100, "…") }; b.content = "

" + b.content + "

"; b.content = feedbro.Strings.linkify(b.content, true, "twitter-tweet-link"); b.content = b.content.replace(j, this.hashtagMapper); b.content = b.content.replace(g, this.usernameMapper); if (w && !y) { b.content += w } if (y) { b.content += y } if (e) { h = ""; D = n[e.user_id_str]; v = this.getContent(e); l = e.full_text; l = feedbro.Strings.linkify(l, true, "twitter-tweet-link"); l = l.replace(j, this.hashtagMapper); l = l.replace(g, this.usernameMapper); h += ""; b.content += h } b.content = p + b.content; if (A) { b.content += "
" } k.push(b) } } catch (d) {} return feedbro.Strings.toRSS(C + " | Twitter", c, k) } else { t += " No Content Found\n"; t += " " + c.replace(/&/g, "&") + "\n" } t += "\n"; return t }; feedbro.TwPlugin.prototype.getSizeAttribute = function (a) { if (a && a.original_info) { return "width='" + a.original_info.width + "' height='" + a.original_info.height + "' " } else { return "" } }; feedbro.TwPlugin.prototype.getContent = function (l) { var f, d, a, h, g, b, m, c, k, i; if (l.extended_entities && l.extended_entities.media) { d = this.getMedia(l) } if (l.card && l.card.binding_values && l.card.name != "promo_website") { f = this.createCardWithAttributes(l.card.binding_values, l.card.url) } return { card: f, media: d } }; feedbro.TwPlugin.prototype.linkMapper = function (b, a) { return "" + a + "" }; feedbro.TwPlugin.prototype.hashtagMapper = function (a, b) { return "#" + b + "" }; feedbro.TwPlugin.prototype.usernameMapper = function (b, a) { return "" + b + "" }; feedbro.TwPlugin.prototype.parseTimeline = function (b, a, d) { var c = this.parseTwitterTimeline(b, d); if (c.title == null && a != null) { c.title = a.substring(a.lastIndexOf("/") + 1) + " | Twitter" } return feedbro.Strings.toRSS(c.title || "User | Twitter", a || "", c.items) }; feedbro.TwPlugin.prototype.parseTwitterTimeline = function (s, c) { var l = []; var f, k; var e, g, q, d, b, m, a = c, p; if (s.data.user) { if (s.data.user.result.timeline_v2) { f = s.data.user.result.timeline_v2.timeline.instructions } else { f = s.data.user.result.timeline.timeline.instructions } } else { if (s.data.list) { f = s.data.list.tweets_timeline.timeline.instructions } else { if (s.data.threaded_conversation_with_injections) { f = s.data.threaded_conversation_with_injections.instructions } } } for (e = 0; e < f.length; e++) { k = f[e]; if (k.entries) { for (g = 0; g < k.entries.length; g++) { l.push(k.entries[g]) } } else { if (k.entry && k.type == "TimelinePinEntry") { l.push(k.entry) } } } var o = []; var n = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; for (e = 0; e < l.length; e++) { q = l[e]; if (q.content.entryType == "TimelineTimelineItem") { if (q.content.itemContent.tweet_results == null) { continue } b = q.content.itemContent.tweet_results.result; if (b == null || b.tombstone) { continue } var h; if (b.core == null && b.tweet && b.tweet.legacy) { d = b.tweet.core.user_results.result.legacy; h = b.tweet.legacy } else { if (b.core) { d = b.core.user_results.result.legacy; h = b.legacy } else { continue } } if (h.retweeted_status_result) { m = this.getRetweet(h.retweeted_status_result.result, d); p = h.retweeted_status_result.result.legacy.full_text } else { m = this.getAuthorHeader(d, null, h); m += this.getTweetBody(h) + this.getMedia(h) + this.getCard(b) + this.getQuote(b); p = h.full_text } p = this._strings.truncate(p.replace(n, ""), 100, "…"); var r = { publishedDate: new Date(h.created_at), author: d.name + " (@" + d.screen_name + ")", content: m, title: p, link: "https://twitter.com/" + d.screen_name + "/status/" + h.id_str }; o.push(r); if (a == null) { a = d.name + " (@" + d.screen_name + ") | Twitter" } } } return { title: a, items: o } }; feedbro.TwPlugin.prototype.createTweetTextHtml = function (d) { var c = d.full_text; c = c.replace(/#(\D\S+)/g, this.hashtagMapper); c = c.replace(/@(\w+)/g, this.usernameMapper); if (d.entities && d.entities.urls && d.entities.urls.length > 0) { var a, b = d.entities.urls; for (a = 0; a < b.length; a++) { c = c.replace(b[a].url, "" + b[a].display_url + "") } } return c }; feedbro.TwPlugin.prototype.getAuthorHeader = function (b, d, a) { var c = "
"; if (d != null) { c += " " } c += " "; c += " "; c += " "; c += " "; c += "
"; return c }; feedbro.TwPlugin.prototype.getDateTag = function (f, d, h) { var b = new Date(f); if (isNaN(b) || d == null) { return "" } var c = this._months[b.getMonth()] + " " + b.getDate(); var a = b.getHours(), e = b.getMinutes(); var g = (a < 10 ? "0" + a : a) + ":" + (e < 10 ? "0" + e : e) + " " + c + ", " + b.getFullYear(); return "" + c + "" }; feedbro.TwPlugin.prototype.getRetweet = function (a, d) { if (a == null || a.core == null || a.core.user_results == null || a.core.user_results.result == null || a.core.user_results.result.legacy == null || a.legacy == null || a.tombstone || a.legacy.tombstone) { return "
This tweet is not available.
" } var c = a.core.user_results.result.legacy; var b = a.legacy; return this.getAuthorHeader(c, d, b) + this.getTweetBody(b) + this.getMedia(b) + this.getCard(a) + this.getQuote(a) }; feedbro.TwPlugin.prototype.getQuote = function (e) { if (e.quoted_status_result) { var a = e.quoted_status_result; if (a.result.tombstone) { return "
This tweet was deleted by the Tweet author.
" } else { var c = a.result.core.user_results.result.legacy; var b = a.result.legacy; var d = this.createTweetTextHtml(b) + this.getMedia(b); return this.getQuoteHtml(c, d, b.created_at, b.id_str) } } else { return "" } }; feedbro.TwPlugin.prototype.getQuoteHtml = function (a, c, d, e) { var b = ""; b += "
"; b += " "; b += " "; b += "
"; return b }; feedbro.TwPlugin.prototype.getCard = function (b) { if (b.card && !b.quoted_status_result && b.card.name != "promo_website") { var e = b.card; var f, d, c = b.card.legacy.binding_values, a = {}; for (f = 0; f < c.length; f++) { d = c[f]; a[d.key] = d.value } return this.createCardWithAttributes(a, e.legacy.url) } else { return "" } }; feedbro.TwPlugin.prototype.createCardWithAttributes = function (b, f) { var i, d, c, h, g; i = "
"; i += "
"; return i }; feedbro.TwPlugin.prototype.getTweetBody = function (a) { return "

" + this.createTweetTextHtml(a) + "

" }; feedbro.TwPlugin.prototype.getMedia = function (g) { if (g.extended_entities && g.extended_entities.media) { var f = g.extended_entities.media[0]; var e, b, d = ""; if (f.type == "photo") { b = this.getSizeAttribute(f); d = "
" } else { if ((f.type == "video" || f.type == "animated_gif") && f.video_info && f.video_info.variants && f.video_info.variants.length > 0) { var c = f.video_info.variants; var a = c[0]; if (a.bitrate == undefined) { a.bitrate = 0 } for (e = 1; e < c.length; e++) { if (c[e].bitrate > a.bitrate) { a = c[e] } } b = this.getSizeAttribute(f); d = "
"; d += '
'; d += "
" } } return d } else { return "" } }; feedbro.TwPlugin.prototype.testParse = function (b, c) { var a = this; fetch(b).then(function (d) { return d.json() }).then(function (d) { c(a.parseTimeline(d), a.parseTimelineHtml(d)) }) }; feedbro.TwPlugin.prototype.parseTimelineHtml = function (d, a) { var e = this.parseTwitterTimeline(d) ```
rauldipeas commented 1 year ago

I'm getting the Twitter feed through Nitter, with images, videos and so on.