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.
当前为
// ==UserScript==
// @name Twitter auto expand show more text + filter timeline + remove short urls
// @namespace zezombye.dev
// @version 0.2
// @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.
// @author Zezombye
// @match https://twitter.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
//Define your filters here. If the text of the tweet contains any of these strings, the tweet will be removed from the timeline
const forbiddenText = ["https://rumble.com/", "topg.com", "clownworldstore.com"].map(x => x.toLowerCase());
//Same but for regex
const forbiddenTextRegex = [/^follow @\w+ for more hilarious commentaries$/i];
//Self explanatory
const accountsWithNoPinnedTweets = ["clownworld_"].map(x => x.toLowerCase());
function shouldRemoveTweet(tweet) {
if (!tweet.legacy) {
//Tweet husk (when a quote tweet quotes another tweet, for example)
return false;
}
//console.log(tweet);
if (tweet.legacy.retweeted_status_result) {
//Remove duplicate tweets from those annoying accounts that retweet their own tweets. (I know, it's for the algo...)
//A good account to test with is https://twitter.com/ClownWorld_
if (tweet.core.user_results.result.legacy.screen_name === tweet.legacy.retweeted_status_result.result.core.user_results.result.legacy.screen_name
&& new Date(tweet.legacy.created_at) - new Date(tweet.legacy.retweeted_status_result.result.legacy.created_at) < 10 * 24 * 60 * 60 * 1000 //10 days
) {
return true;
}
return shouldRemoveTweet(tweet.legacy.retweeted_status_result.result);
}
if (tweet.quoted_status_result && shouldRemoveTweet(tweet.quoted_status_result.result)) {
return true;
}
var user = tweet.core.user_results.result.legacy.screen_name;
var text, entities;
if (tweet.note_tweet) {
text = tweet.note_tweet.note_tweet_results.result.text;
entities = tweet.note_tweet.note_tweet_results.result.entity_set;
} else {
text = tweet.legacy.full_text;
entities = tweet.legacy.entities;
}
//Replace shorthand urls by their real links
//Go in descending order to not fuck up the indices by earlier replacements
var urls = entities.urls.sort((a,b) => b.indices[0] - a.indices[0])
for (var url of urls) {
text = text.substring(0, url.indices[0]) + url.expanded_url + text.substring(url.indices[1])
}
//console.log("Testing if we should remove tweet by '"+user+"' with text: \n"+text);
if (forbiddenText.some(x => text.toLowerCase().includes(x))) {
//console.log("Removed tweet");
return true;
}
if (forbiddenTextRegex.some(x => text.match(x))) {
//console.log("Removed tweet");
return true;
}
return false;
}
function fixTweetGraph(obj) {
if (obj.__typename === "Tweet" && obj.core || obj.__typename === "TweetWithVisibilityResults" && obj.tweet) {
if (obj.__typename === "TweetWithVisibilityResults") {
obj = obj.tweet;
}
if (obj.birdwatch_pivot) {
//It's pretty neat that you can just delete properties and the markup instantly adapts, ngl
delete obj.birdwatch_pivot.callToAction;
delete obj.birdwatch_pivot.footer;
obj.birdwatch_pivot.title = obj.birdwatch_pivot.shorttitle;
//Unfortunately, the full URLs of community notes aren't in the tweet itself. It's another API call
}
if (obj.hasOwnProperty("note_tweet")) {
//Thank God for this property or this would simply be impossible.
//For some reason the full text of the tweet is stored here. So put it in where the webapp is fetching the tweet text
//Also put the entities with their indices
obj.legacy.full_text = obj.note_tweet.note_tweet_results.result.text;
obj.legacy.display_text_range = [0, 9999999];
if ("media" in obj.legacy.entities) {
for (var media of obj.legacy.entities.media) {
if (media.display_url.startsWith("pic.twitter.com/")) {
media.indices = [1000000, 1000001];
}
}
}
for (var key of ["user_mentions", "urls", "hashtags", "symbols"]) {
obj.legacy.entities[key] = obj.note_tweet.note_tweet_results.result.entity_set[key];
}
}
//Remove shortlinks for urls
for (let url of obj.legacy.entities.urls) {
url.display_url = url.expanded_url.replace(/^https?:\/\//, "");
url.url = url.expanded_url;
}
if (obj.legacy.quoted_status_permalink) {
obj.legacy.quoted_status_permalink.display = obj.legacy.quoted_status_permalink.expanded.replace(/^https?:\/\//, "")
}
//Edit user descriptions to remove the shortlinks
} else if (obj.__typename === "User" && obj.legacy && obj.legacy.entities) {
if (obj.legacy.entities.description) {
for (let url of obj.legacy.entities.description.urls) {
url.url = url.expanded_url;
}
}
if (obj.legacy.entities.url) {
for (let url of obj.legacy.entities.url.urls) {
url.url = url.expanded_url;
}
}
//Remove "who to follow" shelf
} else if (obj.entryId && obj.entryId.startsWith("who-to-follow-")) {
obj.content = {};
}
// Recursively iterate over properties
for (var prop in obj) {
if (obj.hasOwnProperty(prop) && typeof obj[prop] === "object") {
//Timeline scrubber. Directly remove all useless tweets
//Todo: check for entryId.startsWith("profile-conversation-") and delete the whole conversation if deleting the last tweet?
if (obj[prop].__typename === "Tweet" && obj[prop].core) {
if (shouldRemoveTweet(obj[prop])) {
delete obj[prop];
} else {
fixTweetGraph(obj[prop]);
}
} else if (obj[prop].__typename === "TweetWithVisibilityResults" && obj[prop].tweet) {
if (shouldRemoveTweet(obj[prop].tweet)) {
delete obj[prop];
} else {
fixTweetGraph(obj[prop]);
}
//Buggy. Todo later
//} else if (obj.type === "TimelinePinEntry" && accountsWithNoPinnedTweets.includes(obj.entry.content.itemContent.tweet_results.result.core.user_results.result.legacy.screen_name.toLowerCase())) {
// delete obj.entry;
} else {
fixTweetGraph(obj[prop]);
}
}
}
return obj;
}
//It's absolutely crazy that the only viable way of expanding a tweet is to hook the XMLHttpRequest object itself.
//Big thanks to https://stackoverflow.com/a/28513219/4851350 because all other methods did not work.
//Apparently it's only in firefox. If it doesn't work in Chrome, cry about it.
var accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
Object.defineProperty(XMLHttpRequest.prototype, 'responseText', {
get: function() {
if (this.responseURL.includes("/UserTweets") || this.responseURL.includes("/HomeTimeline")|| this.responseURL.includes("/HomeLatestTimeline") || this.responseURL.includes("/TweetDetail")) {
var originalResponseText = accessor.get.call(this);
console.log(JSON.parse(originalResponseText));
originalResponseText = JSON.stringify(fixTweetGraph(JSON.parse(originalResponseText)));
console.log(JSON.parse(originalResponseText));
return originalResponseText;
} else {
return accessor.get.call(this);
}
},
set: function(str) {
console.log('set responseText: %s', str);
return accessor.set.call(this, str);
},
configurable: true
});
})();