[Twitter]Engagement Restorer

Retweet and Like List Restorer for Twitter.

// ==UserScript==
// @name			[Twitter]返ってこい!リツイート欄!
// @name:ja			[Twitter]返ってこい!リツイート欄!
// @name:en			[Twitter]Engagement Restorer
// @namespace		https://greasyfork.org/ja/users/1023652
// @version			1145141919810.0.12
// @description		リツイートや引用、いいねをした人を表示するリンクを追加します。
// @description:ja			リツイートや引用、いいねをした人を表示するリンクを追加します。
// @description:en			Retweet and Like List Restorer for Twitter.
// @author			ゆにてぃー
// @match			https://twitter.com/*
// @match			https://x.com/*
// @match			https://X.com/*
// @connect			api.twitter.com
// @icon			https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @grant			GM_xmlhttpRequest
// @license			MIT
// @run-at			document-end
// ==/UserScript==

(function(){
	'use strict';
	let currentUrl = document.location.href;
	let updating = false;
	locationChange();
	const engagementsTextColor = {
		"0": {"count": "rgb(15, 20, 25)","text": "rgb(83, 100, 113)"},
		"1": {"count": "rgb(247, 249, 249)","text": "rgb(139, 152, 165)"},
		"2": {"count": "rgb(231, 233, 234)","text": "rgb(113, 118, 123)"}
	};
	async function main(){
		if(!currentUrl.match(/https?\:\/\/twitter\.com\/\w*\/status\/[0-9]*($|\?.*)/) || document.getElementById('restoreEngagements'))return;
		const tweetLink = currentUrl.match(/https?\:\/\/twitter\.com\/\w*\/status\/[0-9].*/)[0];
		const tweetId = tweetLink.match(/\/status\/(\d+)/)[1];
		let response = (await request(new requestObject_twitter_graphQL(tweetId))).response.data.threaded_conversation_with_injections_v2.instructions[0];
		response = response.entries[response.entries.findIndex((tmp) => tmp.entryId == `tweet-${tweetId}`)].content.itemContent.tweet_results.result.legacy;
		const engagemants = {"favorite_count": response.favorite_count,"quote_count": response.quote_count,"retweet_count": response.retweet_count};
		console.log(engagemants);
		const target_node = Array.from((await wait_load_Element('article[data-testid="tweet"]'))).find(node => {
			const timeParents = Array.from(node.querySelectorAll('time')).map(time => time.parentNode);
			return timeParents.some(parent => parent.href && parent.href.match(tweetId));
		});
		const engagemants_aria = target_node.querySelector('[role="group"]');
		if(!engagemants_aria)return;
		let envEngagementsTextColor = engagementsTextColor[GetCookie("night_mode") || "0"];
		const flexContainer = document.createElement('div');
		flexContainer.style.display = 'flex';
		flexContainer.style.justifyContent = 'space-between';
		flexContainer.style.width = '70%';
		flexContainer.id = 'restoreEngagements';
		const links = [
			{
				"name": "retweets",
				"href": tweetLink + "/retweets",
				"count": round_half_up(engagemants.retweet_count,env_Text.roundingScale,env_Text.decimalPlaces,env_Text.units),
				"text": env_Text.retweet
			},
			{
				"name": "quotes",
				"href": tweetLink + "/quotes",
				"count": round_half_up(engagemants.quote_count,env_Text.roundingScale,env_Text.decimalPlaces,env_Text.units),
				"text": env_Text.quoted,
			},
			{
				"name": "likes",
				"href": tweetLink + "/likes",
				"count": round_half_up(engagemants.favorite_count,env_Text.roundingScale,env_Text.decimalPlaces,env_Text.units),
				"text": env_Text.like,
			},
		];
		links.forEach((a, index) => {
			const newLink = document.createElement('a');
			newLink.style.textDecoration = 'none';
			newLink.href = a.href;
			const countText = document.createElement('span');
			countText.textContent = a.count;
			countText.style.color = envEngagementsTextColor.count;
			newLink.appendChild(countText);

			const textPart = document.createElement('span');
			textPart.textContent = " " + a.text;
			textPart.style.color = envEngagementsTextColor.text;
			newLink.appendChild(textPart);

			newLink.addEventListener('click', (e) => {
				e.preventDefault();
				clickTab(a.name,target_node);
			});
			flexContainer.appendChild(newLink);
		});
		if(currentUrl.match(/https?\:\/\/twitter\.com\/\w*\/status\/[0-9]*($|\?.*)/))engagemants_aria.parentNode.prepend(flexContainer);
	}
	Text.ja = {
		"retweet": "リツイート",
		"quoted": "件の引用",
		"like": "いいね",
		"units": "万",
		"roundingScale": 10000,
		"decimalPlaces": 2,
	}

	Text.en = {
		"retweet": "Retweets",
		"quoted": "Quotes",
		"like": "Likes",
		"units": "K",
		"roundingScale": 1000,
		"decimalPlaces": 1,
	}

	let env_Text = Text[GetCookie("lang")] || Text.en;
	function update(){
		if(updating) return;
		updating = true;
		main();
		setTimeout(() => {updating = false;}, 1000);
	}
	function locationChange(){
		const observer = new MutationObserver(mutations => {
			mutations.forEach(() => {
				if(currentUrl !== document.location.href){
					currentUrl = document.location.href;
					main();
				}
			});
		});
		const target = document.getElementById("react-root");
		const config = {childList: true,subtree: true};
		observer.observe(target, config);
	}
	async function clickTab(name,target_node){
		target_node.querySelector('[data-testid="caret"]').click();
		document.querySelector('[data-testid="tweetEngagements"]').click();
		const engagemants_aria = (await wait_load_Element('nav[aria-live="polite"]'))[0];
		const regex = new RegExp(name + '$');
		engagemants_aria.querySelectorAll('[role="presentation"] a').forEach((e)=>{
			if(e.href.match(regex)) e.click();
		});
	}
	function round_half_up(original_value,where_round_off,decimal_place = 0,unit_str = ""){
		if(Number(original_value)>=Number(where_round_off)){
			var tmp_value;
			tmp_value = Math.round(Number(original_value) / Number(where_round_off) * Math.pow(10,Number(decimal_place))) / Math.pow(10,Number(decimal_place));
			if(unit_str == ""){
				return tmp_value;
			}else{
				return `${tmp_value}${unit_str}`
			}
		}else{
			return original_value;
		}
	}
	function wait_load_Element(Element_Name,interval = 100,retry = 25){
		return new Promise((resolve, reject) => {
			const MAX_RETRY_COUNT = retry;
			let retry_counter = 0;
			let set_interval_id = setInterval(find_target_element, interval);
			function find_target_element(){
				retry_counter++;
				if(retry_counter > MAX_RETRY_COUNT){
					clearInterval(set_interval_id);
					return reject("Max retry count reached");
				}
				let target_elements = document.querySelectorAll(`${Element_Name}`);
				if(target_elements.length > 0){
					clearInterval(set_interval_id);
					return resolve(target_elements);
				}
			}
		});
	}
	function GetCookie(name){
		let arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
		if(arr = document.cookie.match(reg)){
			return decodeURIComponent(arr[2]);
		}else{
			return null;
		}
	}
	async function request(object, maxRetries = 1, timeout = 60000){
		let retryCount = 0;
		while(retryCount <= maxRetries){
			try{
				return await new Promise((resolve, reject) => {
					GM_xmlhttpRequest({
						method: object.method,
						url: object.url,
						headers: object.headers,
						responseType: object.respType,
						data: object.body,
						anonymous: object.anonymous,
						timeout: timeout,
						onload: function(responseDetails){
							resolve(responseDetails);
						},
						ontimeout: function(responseDetails){
							reject(`[request]time out:\nresponse ${responseDetails}`);
						},
						onerror: function(responseDetails){
							reject(`[request]error:\nresponse ${responseDetails}`);
						}
					});
				});
			}catch(error){
				retryCount++;
				console.warn(`Retry ${retryCount}: Failed to fetch ${object.url}. Reason: ${error}`);
				if(retryCount === maxRetries){
					throw new Error(`Failed to fetch ${object.url} after ${maxRetries} retries.`);
				}
			}
		}
	}
	class requestObject_twitter_1_1{
		constructor(ID){
			this.method = 'GET';
			this.respType = 'json';
			this.url = "https://api.twitter.com/1.1/statuses/lookup.json?id=" + ID + "&tweet_mode=extended";
			this.body = null;
			this.headers = {
				"Content-Type": "application/json",
				'Referer': "https://twitter.com/",
				'Authorization': `Bearer AAAAAAAAAAAAAAAAAAAAAIK1zgAAAAAA2tUWuhGZ2JceoId5GwYWU5GspY4%3DUq7gzFoCZs1QfwGoVdvSac3IniczZEYXIcDyumCauIXpcAPorE`,
				'x-csrf-token': GetCookie("ct0"),
			};
			this.package = null;
			this.anonymous = false;
		}
	}
	class requestObject_twitter_graphQL{
		constructor(ID){
			this.method = 'GET';
			this.respType = 'json';
			this.url = `https://api.twitter.com/graphql/NNiD2K-nEYUfXlMwGCocMQ/TweetDetail?variables=%7B%22focalTweetId%22%3A%22${ID}%22%2C%22with_rux_injections%22%3Afalse%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withBirdwatchNotes%22%3Atrue%2C%22withSuperFollowsUserFields%22%3Atrue%2C%22withDownvotePerspective%22%3Afalse%2C%22withReactionsMetadata%22%3Afalse%2C%22withReactionsPerspective%22%3Afalse%2C%22withSuperFollowsTweetFields%22%3Atrue%2C%22withVoice%22%3Atrue%2C%22withV2Timeline%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22vibe_api_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Afalse%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Afalse%2C%22interactive_text_enabled%22%3Atrue%2C%22responsive_web_text_conversations_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D`;
			this.body = null;
			this.headers = {
				"Content-Type": "application/json",
				'User-agent': navigator.userAgent || navigator.vendor || window.opera,
				'accept': '*/*',
				'Referer': "https://twitter.com/",
				'Host': 'api.twitter.com',
				'authorization': `Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA`,
				'x-csrf-token': GetCookie("ct0"),
			};
			this.package = null;
			this.anonymous = false;
		}
	}
	main();
})();