Twitter auto expand show more text + filter tweets + remove short urls

Automatically expand the "show more text" section of tweets when they have more than 280 characters. While we're at it, replace short urls by their actual link, and add a way to filter those annoying repetitive tweets.

  1. // ==UserScript==
  2. // @name Twitter auto expand show more text + filter tweets + remove short urls
  3. // @namespace zezombye.dev
  4. // @version 0.11
  5. // @description Automatically expand the "show more text" section of tweets when they have more than 280 characters. While we're at it, replace short urls by their actual link, and add a way to filter those annoying repetitive tweets.
  6. // @author Zezombye
  7. // @match https://twitter.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  9. // @license MIT
  10. // @require https://cdn.jsdelivr.net/npm/grapheme-splitter@1.0.4/index.min.js
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. //################# START OF CONFIG #################
  19.  
  20. //Define your filters here. If the text of the tweet contains any of these strings, the tweet will be removed from the timeline
  21. const forbiddenText = [
  22. "https://rumble.com/",
  23. "topg.com",
  24. "clownworldstore.com",
  25. "dngcomics",
  26. "tatepledge.com",
  27. "tate confidential ep ",
  28. "skool.com/monetize",
  29. "http://tinyurl.com/48ntfwhh",
  30. "tinyurl.com/334jes22",
  31. "greatonlinegame.com",
  32. "thedankoe.com",
  33. "getairchat.com",
  34. "theartoffocusbook.com",
  35. ].map(x => x.toLowerCase());
  36.  
  37. //Same but for regex
  38. const forbiddenTextRegex = [
  39. /^follow @\w+ for more hilarious commentaries$/i,
  40. /^GM\.?$/,
  41. /You can make money, TODAY.+\s+Begin now: .+university\.com/i,
  42. /\b(israel|palestine|israeli|palestinian)\b/i,
  43. /(^|\W)\$\w+\b/, //cashtags - good for removing crypto shit
  44. ];
  45.  
  46. const removeTextAtStart = /^([\p{Emoji_Presentation}\p{Extended_Pictographic}\p{Emoji_Modifier_Base}\p{Format}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}\uFE0F\u20E3\s]|breaking|flash|alerte info|[:|-])+/iu;
  47.  
  48. //Remove the pinned tweets of these accounts
  49. const accountsWithNoPinnedTweets = [
  50. "clownworld_",
  51. ].map(x => x.toLowerCase());
  52.  
  53. //Remove the tweets of these accounts that only contain images
  54. const accountsWithNoImages = [
  55. "HumansNoContext",
  56. ].map(x => x.toLowerCase());
  57.  
  58. //Remove the tweets of these accounts that do not contain a photo/video (only text)
  59. const accountsWithRequiredMedia = [
  60. "NoContextHumans",
  61. ].map(x => x.toLowerCase());
  62.  
  63. //Remove threads starting by these tweet ids
  64. const forbiddenThreadIds = [
  65. "1681712437085478912", //tate jamaican music
  66. "1710941438526017858", //tatepledge
  67. "1709651947354026010", //tate trw promo quotes
  68. "1753107137444888972", //tate university.com promo quotes
  69. "1738538369557090317", //tate avoiding speaking to famous people
  70. ]
  71.  
  72. const removeTweetsWithOnlyEmojis = true //will not remove tweets with extra info such as quote tweet or media
  73. const removeTweetsWithOnlyMentions = true //same
  74. const hashtagLimit = 15 // remove tweets with more than this amount of hashtags
  75.  
  76. //################# END OF CONFIG #################
  77.  
  78. var splitter = new GraphemeSplitter();
  79. window.splitter = splitter;
  80.  
  81. function shouldRemoveTweet(tweet) {
  82.  
  83. if (!tweet || !tweet.legacy) {
  84. //Tweet husk (when a quote tweet quotes another tweet, for example)
  85. return false;
  86. }
  87.  
  88. //console.log(tweet);
  89.  
  90. if (forbiddenThreadIds.includes(tweet.legacy.conversation_id_str)) {
  91. return true;
  92. }
  93.  
  94.  
  95. if (tweet.legacy.retweeted_status_result) {
  96. //Remove duplicate tweets from those annoying accounts that retweet their own tweets. (I know, it's for the algo...)
  97. //A good account to test with is https://twitter.com/ClownWorld_
  98. if (tweet.core.user_results.result.legacy.screen_name === tweet.legacy.retweeted_status_result.result.core.user_results.result.legacy.screen_name
  99. && new Date(tweet.legacy.created_at) - new Date(tweet.legacy.retweeted_status_result.result.legacy.created_at) < 10 * 24 * 60 * 60 * 1000 //10 days
  100. ) {
  101. return true;
  102. }
  103.  
  104.  
  105. return shouldRemoveTweet(tweet.legacy.retweeted_status_result.result);
  106. }
  107.  
  108. if (tweet.quoted_status_result && shouldRemoveTweet(tweet.quoted_status_result.result)) {
  109. return true;
  110. }
  111.  
  112. var user = tweet.core.user_results.result.legacy.screen_name.toLowerCase();
  113. var text, entities;
  114. if (tweet.note_tweet) {
  115. text = tweet.note_tweet.note_tweet_results.result.text;
  116. entities = tweet.note_tweet.note_tweet_results.result.entity_set;
  117. } else {
  118. text = splitter.splitGraphemes(tweet.legacy.full_text).slice(tweet.legacy.display_text_range[0], tweet.legacy.display_text_range[1]).join("");
  119. entities = tweet.legacy.entities;
  120. }
  121.  
  122. //Replace shorthand urls by their real links
  123. //Go in descending order to not fuck up the indices by earlier replacements
  124. var urls = entities.urls.sort((a,b) => b.indices[0] - a.indices[0])
  125. for (var url of urls) {
  126. text = text.substring(0, url.indices[0]) + url.expanded_url + text.substring(url.indices[1])
  127. }
  128.  
  129. //console.log("Testing if we should remove tweet by '"+user+"' with text: \n"+text);
  130.  
  131. if (removeTweetsWithOnlyEmojis && text.match(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\p{Emoji_Modifier_Base}\p{Format}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}\uFE0F\u20E3\s\p{P}]+$/u) && !tweet.quoted_status_result && !tweet.legacy.entities.media) {
  132. return true;
  133. }
  134. if (removeTweetsWithOnlyMentions && text.match(/^@\w+$/u) && !tweet.quoted_status_result && !tweet.legacy.entities.media) {
  135. return true;
  136. }
  137.  
  138. if (tweet.legacy.entities.hashtags.length > hashtagLimit) {
  139. return true;
  140. }
  141.  
  142. if (forbiddenText.some(x => text.toLowerCase().includes(x))) {
  143. //console.log("Removed tweet");
  144. return true;
  145. }
  146. if (forbiddenTextRegex.some(x => text.match(x))) {
  147. //console.log("Removed tweet");
  148. return true;
  149. }
  150.  
  151. if (accountsWithNoImages.includes(user) && tweet.legacy.entities.media && tweet.legacy.entities.media.every(x => x.expanded_url.includes("/photo/"))) {
  152. return true;
  153. }
  154. if (accountsWithRequiredMedia.includes(user) && !tweet.legacy.entities.media) {
  155. return true;
  156. }
  157.  
  158. return false;
  159. }
  160.  
  161. function fixUser(user, isGraphql) {
  162. if (user.__typename !== "User" && isGraphql) {
  163. console.error("Unhandled user typename '"+user.__typename+"'");
  164. return;
  165. }
  166.  
  167. var userEntities;
  168. if (isGraphql) {
  169. if (!user?.legacy?.entities) {
  170. return;
  171. }
  172. userEntities = user.legacy.entities;
  173. } else {
  174. userEntities = user.entities;
  175. }
  176.  
  177. //Edit user descriptions to remove the shortlinks
  178. if (userEntities.description) {
  179. for (let url of userEntities.description.urls) {
  180. if (url.expanded_url) {
  181. url.url = url.expanded_url;
  182. url.display_url = url.expanded_url.replace(/^https?:\/\/(www\.)?/, "");
  183. }
  184. }
  185. }
  186. if (userEntities.url) {
  187. for (let url of userEntities.url.urls) {
  188. if (url.expanded_url) {
  189. url.url = url.expanded_url;
  190. url.display_url = url.expanded_url.replace(/^https?:\/\/(www\.)?/, "");
  191. }
  192. }
  193. }
  194. }
  195.  
  196. function fixTweet(tweet) {
  197.  
  198. if (!tweet) {
  199. return;
  200. }
  201.  
  202. if (tweet.__typename === "TweetWithVisibilityResults") {
  203. if (tweet.tweetInterstitial && tweet.tweetInterstitial.text.text === "This Post violated the X Rules. However, X has determined that it may be in the public’s interest for the Post to remain accessible. Learn more") {
  204. delete tweet.tweetInterstitial;
  205. }
  206. tweet = tweet.tweet;
  207. }
  208.  
  209. if (tweet.__typename !== "Tweet" && tweet.__typename) {
  210. console.error("Unhandled tweet typename '"+tweet.__typename+"'");
  211. return;
  212. }
  213.  
  214. if (!tweet.legacy) {
  215. //Tweet husk (when a quote tweet quotes another tweet, for example)
  216. return;
  217. }
  218.  
  219. //console.log("Fixing tweet:", tweet);
  220.  
  221.  
  222. fixUser(tweet.core.user_results.result, true);
  223.  
  224.  
  225. if (tweet.birdwatch_pivot) {
  226. //It's pretty neat that you can just delete properties and the markup instantly adapts, ngl
  227. delete tweet.birdwatch_pivot.callToAction;
  228. delete tweet.birdwatch_pivot.footer;
  229. tweet.birdwatch_pivot.title = tweet.birdwatch_pivot.shorttitle;
  230. //Unfortunately, the full URLs of community notes aren't in the tweet itself. It's another API call
  231. }
  232.  
  233. if (tweet.hasOwnProperty("note_tweet")) {
  234. //Thank God for this property or this would simply be impossible.
  235. //For some reason the full text of the tweet is stored here. So put it in where the webapp is fetching the tweet text
  236. //Also put the entities with their indices
  237. tweet.legacy.full_text = tweet.note_tweet.note_tweet_results.result.text;
  238. tweet.legacy.display_text_range = [0, 9999999];
  239. if ("media" in tweet.legacy.entities) {
  240. for (var media of tweet.legacy.entities.media) {
  241. if (media.display_url.startsWith("pic.twitter.com/")) {
  242. media.indices = [1000000, 1000001];
  243. }
  244. }
  245. }
  246. for (let key of ["user_mentions", "urls", "hashtags", "media", "symbols"]) {
  247. if (tweet.note_tweet.note_tweet_results.result.entity_set[key]) {
  248. tweet.legacy.entities[key] = tweet.note_tweet.note_tweet_results.result.entity_set[key];
  249. }
  250. }
  251. }
  252.  
  253. //Good account to test that on: https://twitter.com/AlertesInfos
  254. let displayedTweetText = splitter.splitGraphemes(tweet.legacy.full_text).slice(tweet.legacy.display_text_range[0], tweet.legacy.display_text_range[1]).join("")
  255. let oldLength = splitter.countGraphemes(displayedTweetText)
  256. let oldLengthMedia = [...displayedTweetText].length //media indices are apparently based on this length?
  257. let oldLengthRichText = displayedTweetText.length //apparently rich text indices are based on js calculated length, which counts emojis as 2 chars
  258. //console.log("oldlength", oldLength, "oldLengthRichText", oldLengthRichText)
  259. displayedTweetText = displayedTweetText.replace(removeTextAtStart, "")
  260. let lengthDifference = oldLength - splitter.countGraphemes(displayedTweetText)
  261. let lengthDifferenceMedia = oldLengthMedia - [...displayedTweetText].length
  262. let lengthDifferenceRichText = oldLengthRichText - displayedTweetText.length
  263. //console.log(lengthDifference, lengthDifferenceRichText)
  264. tweet.legacy.full_text = splitter.splitGraphemes(tweet.legacy.full_text).slice(0, tweet.legacy.display_text_range[0]).join("")+displayedTweetText+splitter.splitGraphemes(tweet.legacy.full_text).slice(tweet.legacy.display_text_range[1]).join("")
  265. tweet.legacy.display_text_range[1] -= lengthDifference
  266.  
  267. for (let key of ["user_mentions", "urls", "hashtags", "media", "symbols"]) {
  268. if (tweet.legacy.entities[key]) {
  269. for (let entity of tweet.legacy.entities[key]) {
  270. entity.indices[0] -= lengthDifferenceMedia
  271. entity.indices[1] -= lengthDifferenceMedia
  272. }
  273. }
  274. if (tweet.legacy.extended_entities && tweet.legacy.extended_entities[key]) {
  275. for (let entity of tweet.legacy.extended_entities[key]) {
  276. entity.indices[0] -= lengthDifferenceMedia
  277. entity.indices[1] -= lengthDifferenceMedia
  278. }
  279. }
  280. }
  281.  
  282. if (tweet.hasOwnProperty("note_tweet")) {
  283. //Tweets with more than 250 chars are displayed using the note tweet, so put back the modified full text to the note tweet property
  284. //https://twitter.com/MarioNawfal/status/1733973012050022523
  285. tweet.note_tweet.note_tweet_results.result.text = tweet.legacy.full_text;
  286. for (let key of ["user_mentions", "urls", "hashtags", "media", "symbols"]) {
  287. if (tweet.legacy.entities[key]) {
  288. tweet.note_tweet.note_tweet_results.result.entity_set[key] = tweet.legacy.entities[key];
  289. }
  290. }
  291. if (tweet.note_tweet.note_tweet_results.result.richtext) {
  292. for (let richTextObject of tweet.note_tweet.note_tweet_results.result.richtext.richtext_tags) {
  293. richTextObject.from_index -= lengthDifferenceRichText
  294. richTextObject.to_index -= lengthDifferenceRichText
  295. }
  296. }
  297. }
  298.  
  299.  
  300. //Remove shortlinks for urls
  301. for (let url of tweet.legacy.entities.urls) {
  302. if (url.expanded_url) {
  303. url.display_url = url.expanded_url.replace(/^https?:\/\/(www\.)?/, "");
  304. url.url = url.expanded_url;
  305. }
  306. }
  307.  
  308. if (tweet.legacy.quoted_status_permalink) {
  309. tweet.legacy.quoted_status_permalink.display = tweet.legacy.quoted_status_permalink.expanded.replace(/^https?:\/\//, "")
  310. }
  311.  
  312. if (tweet.quoted_status_result) {
  313. fixTweet(tweet.quoted_status_result.result);
  314. }
  315.  
  316. if (tweet.legacy.retweeted_status_result) {
  317. fixTweet(tweet.legacy.retweeted_status_result.result);
  318. }
  319. }
  320.  
  321. function patchApiResult(apiPath, data) {
  322.  
  323. if (apiPath === "UserByScreenName" || apiPath === "UserByRestId") {
  324. if (data.data.user) {
  325. fixUser(data.data.user.result, true);
  326. }
  327. return data;
  328. }
  329. if (apiPath === "recommendations.json") {
  330. for (var user of data) {
  331. fixUser(user.user, false);
  332. }
  333. return data;
  334. }
  335.  
  336. var timeline;
  337. if (apiPath === "TweetDetail") {
  338. //When viewing a tweet directly.
  339. //https://twitter.com/atensnut/status/1723692342727647277
  340. timeline = data.data.threaded_conversation_with_injections_v2;
  341. } else if (apiPath === "HomeTimeline" || apiPath === "HomeLatestTimeline") {
  342. //"For you" and "Following" respectively, of the twitter homepage
  343. timeline = data.data.home.home_timeline_urt;
  344. } else if (apiPath === "UserTweets" || apiPath === "UserTweetsAndReplies" || apiPath === "UserMedia" || apiPath === "Likes") {
  345. //When viewing a user directly.
  346. //https://twitter.com/elonmusk
  347. //https://twitter.com/elonmusk/with_replies
  348. //https://twitter.com/elonmusk/media
  349. //https://twitter.com/elonmusk/likes
  350. timeline = data.data.user.result.timeline_v2.timeline;
  351. } else if (apiPath === "UserHighlightsTweets") {
  352. //https://twitter.com/elonmusk/highlights
  353. timeline = data.data.user.result.timeline.timeline;
  354. } else if (apiPath === "SearchTimeline") {
  355. //When viewing quoted tweets, or when literally searching tweets.
  356. //https://twitter.com/elonmusk/status/1721042240535973990/quotes
  357. //https://twitter.com/search?q=hormozi&src=typed_query
  358. timeline = data.data.search_by_raw_query.search_timeline.timeline;
  359. } else {
  360. console.error("Unhandled api path '"+apiPath+"'")
  361. return data;
  362. }
  363.  
  364. if (!timeline) {
  365. console.error("No timeline found");
  366. return data;
  367. }
  368.  
  369. for (var instruction of timeline.instructions) {
  370. if (instruction.type === "TimelineClearCache" || instruction.type === "TimelineTerminateTimeline" || instruction.type === "TimelineReplaceEntry") {
  371. //do nothing
  372. } else if (instruction.type === "TimelineShowAlert") {
  373. for (let user of instruction.usersResults) {
  374. fixUser(user.result, true);
  375. }
  376. } else if (instruction.type === "TimelinePinEntry") {
  377. //Sometimes there is an empty pinned entry? Eg https://twitter.com/RyLiberty
  378. if (instruction.entry.content.itemContent.tweet_results.result) {
  379. console.log(instruction.entry.content.itemContent.tweet_results.result);
  380. if (instruction.entry.content.itemContent.tweet_results.result.tweet) {
  381. instruction.entry.content.itemContent.tweet_results.result = instruction.entry.content.itemContent.tweet_results.result.tweet
  382. }
  383. if (accountsWithNoPinnedTweets.includes(instruction.entry.content.itemContent.tweet_results.result.core.user_results.result.legacy.screen_name.toLowerCase())) {
  384. instruction.shouldBeRemoved = true;
  385. } else if (shouldRemoveTweet(instruction.entry.content.itemContent.tweet_results.result)) {
  386. instruction.shouldBeRemoved = true;
  387. } else {
  388. fixTweet(instruction.entry.content.itemContent.tweet_results.result);
  389. }
  390. }
  391.  
  392. } else if (instruction.type === "TimelineAddToModule") {
  393. //Only seen on threads with longer than 30 tweets. Eg: https://twitter.com/Cobratate/status/1653053914411941897
  394. for (let item of instruction.moduleItems) {
  395. if (item.entryId.startsWith(instruction.moduleEntryId+"-tweet-")) {
  396. fixTweet(item.item.itemContent.tweet_results.result);
  397. } else if (item.entryId.startsWith(instruction.moduleEntryId+"-cursor-")) {
  398. //do nothing
  399. } else {
  400. console.error("Unhandled item entry id '"+item.entryId+"'");
  401. }
  402. }
  403.  
  404. } else if (instruction.type === "TimelineAddEntries") {
  405. for (var entry of instruction.entries) {
  406. if (entry.entryId.startsWith("tweet-")) {
  407. if (apiPath !== "TweetDetail" && shouldRemoveTweet(entry.content.itemContent.tweet_results.result)) {
  408. //If TweetDetail, then the tweet is either the tweet itself, or the tweet(s) it is replying to.
  409. //Do not check them for deletion because it would make the tweet have no sense.
  410. entry.shouldBeRemoved = true;
  411. }
  412. if (!entry.shouldBeRemoved) {
  413. fixTweet(entry.content.itemContent.tweet_results.result);
  414. }
  415.  
  416. } else if (entry.entryId.startsWith("conversationthread-") || entry.entryId.startsWith("tweetdetailrelatedtweets-")) {
  417. for (let item of entry.content.items) {
  418. if (item.entryId.startsWith(entry.entryId+"-tweet-")) {
  419. if (shouldRemoveTweet(item.item.itemContent.tweet_results.result)) {
  420. item.shouldBeRemoved = true;
  421. } else {
  422. fixTweet(item.item.itemContent.tweet_results.result)
  423. }
  424. } else if (item.entryId.startsWith(entry.entryId+"-cursor-")) {
  425. //do nothing
  426. } else {
  427. console.error("Unhandled item entry id '"+item.entryId+"'");
  428. }
  429.  
  430. }
  431. entry.content.items = entry.content.items.filter(x => !x.shouldBeRemoved)
  432. if (entry.content.items.length === 0) {
  433. entry.shouldBeRemoved = true
  434. }
  435.  
  436. } else if (entry.entryId.startsWith("profile-conversation-") || entry.entryId.startsWith("home-conversation-")) {
  437. //Only remove tweets in a conversation if it is the last tweet of the conversation. (Else, the tweets after won't make sense.)
  438. let hasTweetBeenKept = false;
  439. for (let i = entry.content.items.length - 1; i >= 0; i--) {
  440. if (!hasTweetBeenKept) {
  441. if (shouldRemoveTweet(entry.content.items[i].item.itemContent.tweet_results.result)) {
  442. entry.content.items[i].shouldBeRemoved = true;
  443. } else {
  444. hasTweetBeenKept = true;
  445. }
  446. }
  447. if (!entry.content.items[i].shouldBeRemoved) {
  448. fixTweet(entry.content.items[i].item.itemContent.tweet_results.result);
  449. }
  450. }
  451. entry.content.items = entry.content.items.filter(x => !x.shouldBeRemoved)
  452. if (entry.content.items.length === 0) {
  453. entry.shouldBeRemoved = true
  454. }
  455.  
  456. } else if (entry.entryId.startsWith("toptabsrpusermodule-")) {
  457. for (let item of entry.content.items) {
  458. fixUser(item.item.itemContent.user_results.result, true);
  459. }
  460.  
  461. } else if (entry.entryId.startsWith("who-to-follow-") || entry.entryId.startsWith("who-to-subscribe-") || entry.entryId.startsWith("promoted-tweet-") || entry.entryId === "messageprompt-premium-plus-upsell-prompt") {
  462. entry.shouldBeRemoved = true;
  463.  
  464. } else if (entry.entryId.startsWith("cursor-") || entry.entryId.startsWith("label-") || entry.entryId.startsWith("relevanceprompt-")) {
  465. //nothing to do
  466. } else {
  467. console.error("Unhandled entry id '"+entry.entryId+"'")
  468. }
  469. }
  470. instruction.entries = instruction.entries.filter(x => !x.shouldBeRemoved);
  471. } else {
  472. console.error("Unhandled instruction type '"+instruction.type+"'");
  473. }
  474. }
  475. timeline.instructions = timeline.instructions.filter(x => !x.shouldBeRemoved);
  476.  
  477. return data;
  478. }
  479.  
  480.  
  481. //It's absolutely crazy that the only viable way of expanding a tweet is to hook the XMLHttpRequest object itself.
  482. //Big thanks to https://stackoverflow.com/a/28513219/4851350 because all other methods did not work.
  483. //Apparently it's only in firefox. If it doesn't work in Chrome, cry about it.
  484.  
  485. /*const OriginalXMLHttpRequest = unsafeWindow.XMLHttpRequest;
  486.  
  487. class XMLHttpRequest extends OriginalXMLHttpRequest {
  488. get responseText() {
  489. // If the request is not done, return the original responseText
  490. if (this.readyState !== 4) {
  491. return super.responseText;
  492. }
  493. console.log(super.responseText);
  494.  
  495. return super.responseText.replaceAll("worse", "owo");
  496. }
  497. }
  498.  
  499. unsafeWindow.XMLHttpRequest = XMLHttpRequest;*/
  500.  
  501. try {
  502. var open_prototype = XMLHttpRequest.prototype.open
  503. XMLHttpRequest.prototype.open = function() {
  504. this.addEventListener('readystatechange', function(event) {
  505. if ( this.readyState === 4 ) {
  506. var urlPath = event.target.responseURL ? (new URL(event.target.responseURL)).pathname : "";
  507. var apiPath = urlPath.split("/").pop()
  508. //console.log(urlPath);
  509. if (urlPath.startsWith("/i/api/") && ["UserTweets", "HomeTimeline", "HomeLatestTimeline", "SearchTimeline", "TweetDetail", "UserByScreenName", "UserByRestId", "UserTweetsAndReplies", "UserMedia", "Likes", "UserHighlightsTweets", "recommendations.json"].includes(apiPath)) {
  510.  
  511. var originalResponseText = event.target.responseText;
  512. console.log(apiPath, JSON.parse(originalResponseText));
  513. originalResponseText = patchApiResult(apiPath, JSON.parse(originalResponseText));
  514. console.log(originalResponseText);
  515. Object.defineProperty(this, 'response', {writable: true});
  516. Object.defineProperty(this, 'responseText', {writable: true});
  517. this.response = this.responseText = JSON.stringify(originalResponseText);
  518. }
  519. }
  520. });
  521. return open_prototype.apply(this, arguments);
  522. };
  523. } catch (e) {
  524. console.error(e);
  525. }
  526. /*var accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
  527.  
  528. Object.defineProperty(unsafeWindow.XMLHttpRequest.prototype, 'responseText', {
  529. get: function() {
  530. var urlPath = this.responseURL ? (new URL(this.responseURL)).pathname : "";
  531. var apiPath = urlPath.split("/").pop()
  532. console.log(urlPath);
  533. if (urlPath.startsWith("/i/api/") && ["UserTweets", "HomeTimeline", "HomeLatestTimeline", "SearchTimeline", "TweetDetail", "UserByScreenName", "UserByRestId", "UserTweetsAndReplies", "UserMedia", "Likes", "UserHighlightsTweets", "recommendations.json"].includes(apiPath)) {
  534. var originalResponseText = accessor.get.call(this);
  535. console.log(apiPath, JSON.parse(originalResponseText));
  536. originalResponseText = patchApiResult(apiPath, JSON.parse(originalResponseText));
  537. console.log(originalResponseText);
  538. return JSON.stringify(originalResponseText);
  539. } else {
  540. return accessor.get.call(this);
  541. }
  542. },
  543. set: function(str) {
  544. console.log('set responseText: %s', str);
  545. return accessor.set.call(this, str);
  546. },
  547. configurable: true
  548. });*/
  549. })();