Greasy Fork镜像 支持简体中文。

Show ctime of retweets

ES2017(ES8).

目前為 2023-03-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name               Show ctime of retweets
// @namespace          https://gf.qytechs.cn/ja/scripts/461409-show-ctime-of-retweets
// @version            0.2
// @description        ES2017(ES8).
// @author             AeamaN
// @contributionURL    bitcoin:1DC6uWJWzzwU3iRJDXhUquv6QAYaRvtfFJ
// @match              https://twitter.com/*
// @match              https://mobile.twitter.com/*
// @match              https://twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/*
// @match              https://mobile.twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/*
// @grant              none
// @run-at             document-idle
// ==/UserScript==

'use strict';

// //////// Setings //////// //
// Date formats
// 1. ye-mo/da ho:mi
// 2. ye-mo/da(we) ho:mi,se
// 3. ye/mo/da ho:mi
// 4. ye/mo/da(we) ho:mi:se
const FMT = 4;
// ///////////////////////// //

const MYNAME = 'sctrt02';
const BTKN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs'
           + '=1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
const EBTKN = encodeURIComponent(BTKN);
const INTL = 1000;

let top_sr = '1';
let json_sr = [];
let json_sr_new = [];
let timeout_sr = false;
let s_mus = null;
let observer = new MutationObserver(function(mutations) {
	s_mus = mutations;
});


let cookie = { // "https://qiita.com/aqril_1132/items/925a7cb04276d9f916d7"
	getObj: function() {
		let cookie = document.cookie;
		let cookieObj = {};
		if (!!cookie) {
			Array.prototype.forEach.call(cookie.split(';'), function(c) {
				let array = [c][0].split('=').map(function(a) {return a.trim()});
				let key = ~c.indexOf('=') ? unescape(array[0]) : '';
				let val = ~c.indexOf('=') ? unescape(array[1]) : unescape(array[0]);
				if (!cookieObj.hasOwnProperty(key)) {
					cookieObj[key] = [val];
				} else {
					cookieObj[key].push(val);
				}
			});
		}
		return cookieObj;
	},
	getByName: function(name) {
		let ret = [];
		let cookieObj = this.getObj();
		if (cookieObj.hasOwnProperty(name)) {
			ret = cookieObj[name];
		}
		return ret;
	},
	deleteByName : function(name, path) {
	var str = escape(name)
	        + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT'
	        + (path ? '; path=' + path : '');
	document.cookie = str;
	}
};


function loop() {
	setTimeout(async () => {
		if(s_mus) await main_track();
		s_mus = null; // 非同期処理用
		loop();
	}, INTL);
}


async function main_track() {
	s_mus = null;
	
	const SEL_END = 'main div[data-testid="primaryColumn"] section article span.css-cens5h.r-b88u0q';
	const SEL_RTTO = `div[data-testid="User-Names"] a.css-901oao.r-qvutc0:not(.us-${MYNAME})`; // UTL, HTL
	const SEL_RTTO_2 = `div[data-testid="User-Name"] a.css-901oao.r-qvutc0:not(.us-${MYNAME})`; // UTL, HTL
	const SEL_RTTO_3 = `div.css-901oao.r-14j79pv.r-1tl8opc a.css-901oao.r-qvutc0:not(.us-${MYNAME})`; // Retweet
	// const SEL_RTTO_3 = `div.css-901oao.r-115tad6.r-1tl8opc a.css-901oao.r-qvutc0:not(.us-${MYNAME})`; // ?
	const SEL_ADD = `div.us-${MYNAME}`;
	
	let elms, old;
	let pe, fpe, xpe, e2, div, div2, a;
	let sn, rtto, date;
	let id, stats;
	
	elms = document.querySelectorAll(SEL_END);
	
	for(let e of elms) {
		pe = e.parentNode;
		fpe = e.parentNode.parentNode.parentNode.parentNode;
		sn = pe.getAttribute('href').slice(1);
		
		xpe = e.closest('article');
		e2 = xpe.querySelector(SEL_RTTO);
		if(!e2) e2 = xpe.querySelector(SEL_RTTO_2);
		if(!e2) e2 = xpe.querySelector(SEL_RTTO_3);
		rtto = e2.getAttribute('href').split('/')[3];
		
		id = await touid(sn); // screen name -> id, created_at, screen_name
		if(!id) continue;
		
		stats = await statsrt(id[0], rtto); // id, rtto -> tid, tca
		
		old = fpe.querySelectorAll(SEL_ADD)
		if(old.length) for(let e of old) e.remove();
		
		div = document.createElement('div');
		div.className = `us-${MYNAME}`;
		div.style.margin = '0px 3px 0px 3px';
		div.textContent = '·';
		div.style.color = getComputedStyle(e, null).color;
		div.style.font = getComputedStyle(e, null).font;
		div.style.lineHeight = getComputedStyle(pe, null).lineHeight;
		
		div2 = document.createElement('div');
		div2.className = `us-${MYNAME}`;
		div2.style.lineHeight = getComputedStyle(pe, null).lineHeight;
		
		a = document.createElement('a');
		a.className = `us-${MYNAME}`;
		a.setAttribute('dir', 'ltr');
		a.setAttribute('role', 'link');
		a.setAttribute('href', `/${sn}/status/${stats[0]}`);
		a.setAttribute('target', '_blank');
		a.setAttribute('rel', 'noopener noreferrer');
		date = datef(new Date(stats[1]), FMT);
		a.textContent = date;
		a.style.color = getComputedStyle(e, null).color;
		a.style.font = getComputedStyle(e, null).font;
		a.style.textDecoration = getComputedStyle(e, null).textDecoration;
		
		fpe.appendChild(div);
		fpe.appendChild(div2);
		div2.appendChild(a);
		
		s_mus = null;
	}
}


async function touid(sn) {
	if(localStorage.getItem(`${MYNAME}_idl`) === null) { // 無い時
		let s = JSON.stringify(await getuid(sn)); // screen name -> id, created_at, screen_name
		if(s) {
			localStorage.setItem(`${MYNAME}_idl`, s);
		} else {
			console.log(`${MYNAME}: touid:error.`);
			return null; // エラー
		}
	}
	
	let str = localStorage.getItem(`${MYNAME}_idl`); // ある時はココから
	let json = JSON.parse(str);
	
	for(let e of json) {
		if(e[2] == sn) return e;
	}
	
	json = json.concat(await getuid(sn)); // あるけど、無い時
	str = JSON.stringify(json);
	localStorage.setItem(`${MYNAME}_idl`, str);

	for(let e of json) {
		if(e[2] == sn) return e;
	}
	
	console.log(`${MYNAME}: touid:error:end.`);
	return null; // エラー
}


async function getuid(s_name) {
	let url = 'https://api.twitter.com/graphql/rePnxwe9LZ51nQ7Sn_xN_A/UserByScreenName';
	if(/^[^:]+:\/\/[^/]+\.onion\//i.test(document.URL)) {
		url = 'https://api.twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/'
		    + 'graphql/rePnxwe9LZ51nQ7Sn_xN_A/UserByScreenName';
	}
	let q = '?variables={'
		  + `"screen_name":"${s_name}",`
		  + '"withSafetyModeUserFields":true,'
	      + '"withSuperFollowsUserFields":true'
		  + '}'
	      + '&features={'
		  + '"responsive_web_twitter_blue_verified_badge_is_enabled":true,'
	      + '"responsive_web_graphql_exclude_directive_enabled":false,' // false
	      + '"verified_phone_label_enabled":true,' // false
	      + '"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,'
	      + '"responsive_web_graphql_timeline_navigation_enabled":true'
		  + '}';
	let eq = encodeURI(q);
	
	let controller = new AbortController();
	let req = mkreq(url, eq, EBTKN, controller);
	let res = {};
	
	try {
		setTimeout(function() {controller.abort()}, 60000);
		res = await fetch(req);
		if(!res.ok) {
			console.log(`${MYNAME}: getuid:error:` + res.ok + '.');
	 		return null;
		} // 失敗なら空で終わり
	} catch(err) {
		console.log(`${MYNAME}: getuid:error:` + err + '.');
		return null; // 失敗なら空で終わり
	}
	
	let json = JSON.parse(await res.text());
	
	let id, ca, sn;
	id = json.data.user.result.rest_id;
	ca = json.data.user.result.legacy.created_at;
	sn = json.data.user.result.legacy.screen_name;
	
	return [[id, ca, sn]];
}


async function statsrt(uid, rtto) { // 連続している事、entries[]の順序が正しい事
	let num = localStorage.length;
	let keys = [];
	let str = '';
	let time = null;
	
	top_sr = '1';
	json_sr = [];
	json_sr_new = [];
	timeout_sr = false; // 初期化
	
	for(let i = 0; i < num; i++) {
		keys.push(localStorage.key(i));
	}
	
	if(!keys.includes(`${MYNAME}_tl_` + uid)) { // 無い時
		let s = JSON.stringify(await gettl(uid, null)); // id, cursor -> tid, tca, s, rtid, rtca
		if(s) {
			localStorage.setItem(`${MYNAME}_tl_` + uid, s);
		} else {
			console.log(`${MYNAME}: statsrt:error.`);
			return ['0000000000000000000', 'Thu Jan 01 00:00:00 +0000 1970']; // エラー
		}
	}
	
	if(localStorage.getItem(`${MYNAME}_timerl`) === null) {
		time = Date.now();
		localStorage.setItem(`${MYNAME}_timerl`, JSON.stringify([[uid, time]]));
	} else {
		let j = JSON.parse(localStorage.getItem(`${MYNAME}_timerl`));
		for(let e of j) if(e[0] == uid) time = e[1];
		if(!time) { // 項目が無い時
			time = Date.now(); // この時点の保存値にセットされる
			j = j.concat([[uid, time]]);
			localStorage.setItem(`${MYNAME}_timerl`, JSON.stringify(j));
		}
	}
	
	str = localStorage.getItem(`${MYNAME}_tl_` + uid); // ある時はココから
	json_sr = JSON.parse(str);
	
	for(let e of json_sr) {
		if(e[3] == rtto) return [e[0], e[1]]; // あれば終わり
	}
	
	for(let e of json_sr) {
		if(/^[0-9]/i.test(e[0])) {
				top_sr = e[0];
				break;
			}
		} // 上作成
	
	await statsrt_new(uid, rtto); // json_sr、json_sr_new更新
	
	json_sr = json_sr_new.concat(json_sr); // json_sr更新
	str = JSON.stringify(json_sr); // str更新
	localStorage.setItem(`${MYNAME}_tl_` + uid, str); // とりあえず、ストレージに保存
	
	for(let e of json_sr) {
		if(e[3] == rtto) return [e[0], e[1]]; // あれば終わり
	}
	
	let ret = await statsrt_old(uid, rtto); // json_sr更新、保存
	if(ret) return ret;
	
	let j = JSON.parse(localStorage.getItem(`${MYNAME}_timerl`));
	for(let e of j) if(e[0] == uid) time = e[1]; // time更新
	
	if(!timeout_sr && Date.now() - time > 600000) {
		localStorage.removeItem(`${MYNAME}_tl_${uid}`); // センシティブ
		console.log(`${MYNAME}: statsrt:remove:${MYNAME}_tl_${uid}.`);
		
		j = j.filter(function(e) {
			return e[0] != uid;
		});
		// j = j.concat([[uid, Date.now()]]);
		localStorage.setItem(`${MYNAME}_timerl`, JSON.stringify(j));
		time = null;
	}
	
	console.log(`${MYNAME}: statsrt:end.`);
	// return null; // エラー
	return ['0000000000000000000', 'Thu Jan 01 00:00:00 +0000 1970'];
	// TLがとても古いか、センシティブか、エラー
}


async function statsrt_new(uid, rtto) { // json_sr、json_sr_new更新
	let btmtmp = '';
	
	outer_block:
	for(const start = Date.now(); 1;) { // 新しい方、top_srを見る
		let ret = await gettl(uid, btmtmp);
		json_sr_new = json_sr_new.concat(ret); // json_sr_new更新
		
		for(let e of json_sr_new.slice().reverse()) { // 不要
			if(/^[0-9]/i.test(e[0])) {
				btmtmp = e[0];
				break;
			}
		} // 下更新
		
		if(ret.length < 2) {
			json_sr = []; // 非連続
			
			let j = JSON.parse(localStorage.getItem(`${MYNAME}_timerl`));
			j = j.filter(function(e) {
				return e[0] != uid;
			});
			j = j.concat([[uid, Date.now()]]);
			localStorage.setItem(`${MYNAME}_timerl`, JSON.stringify(j));
			
			break;
		} // 空の時は終了、一回目は無い(一個の時以外)
		
		for(let e of json_sr_new) {
			if(e[0] == top_sr) break outer_block; // あれば終了
		}
		
		if(Date.now() - start > 150000) { // 時間経過時打ち切り
			json_sr = []; // 非連続かも
			
			let j = JSON.parse(localStorage.getItem(`${MYNAME}_timerl`));
			j = j.filter(function(e) {
				return e[0] != uid;
			});
			j = j.concat([[uid, Date.now()]]);
			localStorage.setItem(`${MYNAME}_timerl`, JSON.stringify(j));
			
			break;
		}
		
		await new Promise(resolve => setTimeout(resolve, 200));
	}
}


async function statsrt_old(uid, rtto) {
	let btm = '';
	
	for(const start = Date.now(); 1;) { // 古い方、rttoを見る
		for(let e of json_sr.slice().reverse()) { // 不要
			if(/^[0-9]/i.test(e[0])) {
				btm = e[0];
				break;
			}
		} // 下更新
		
		let ret = await gettl(uid, btm);
		json_sr = json_sr.concat(ret); // json_sr更新
		let s = JSON.stringify(json_sr);
		localStorage.setItem(`${MYNAME}_tl_` + uid, s); // とりあえず、ストレージに保存
		
		if(ret.length < 2) {
			 break;
		} // 空の時は終了
		
		for(let e of json_sr) {
			if(e[3] == rtto) return [e[0], e[1]]; // あれば終わり
		}
		
		if(Date.now() - start > 300000) {
			timeout_sr = true;
			break;
		} // 時間経過時打ち切り
		
		await new Promise(resolve => setTimeout(resolve, 200));
	}
}


async function gettl(id, cs) {
	let url = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
	if(/^[^:]+:\/\/[^/]+\.onion\//i.test(document.URL)) {
		url = 'https://api.twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/'
		    + '1.1/statuses/user_timeline.json';
	}
	let kav = '';
	if(cs) kav = `&max_id=${cs}`;
	let q = `?user_id=${id}&count=50${kav}`;
	let eq = encodeURI(q);
	
	let controller = new AbortController();
	let req = mkreq(url, eq, EBTKN, controller);
	let res = {};
	
	let ret = [];
	
	try {
		setTimeout(function() {controller.abort()}, 60000);
		res = await fetch(req);
		if(!res.ok) {
			console.log(`${MYNAME}: gettl:error:` + res.ok + '.');
	 		return null;
	 	} // 失敗なら空で終わり
	} catch(err) {
		console.log(`${MYNAME}: gettl:error:` + err + '.');
		return null; // 失敗なら空で終わり
	}
	
	let json = JSON.parse(await res.text());
	
	let num = Object.keys(json).length;
	let tid, tca, s, rtid, rtca;
	
	for(let i = 0; i < num; i++) {
		tid = json[i].id_str;
		tca = json[i].created_at;
		s = json[i].source;
		if(typeof(json[i].retweeted_status) !== 'undefined') {
			rtid = json[i].retweeted_status.id_str;
			rtca = json[i].retweeted_status.created_at;
		} else {
			rtid = 'none';
			rtca = 'none';
		}
		
		ret.push([tid, tca, s, rtid, rtca]);
	}
	
	return ret;
}


function mkreq(url, eq, ebt, controller) {
	let req;
	
	if(cookie.getByName('gt').length && !cookie.getByName('twid').length) {
		req = new Request(`${url}${eq}`,{
			headers: {
				'authorization': `Bearer ${ebt}`,
				'x-csrf-token': cookie.getByName('ct0')[0],
				'x-guest-token': cookie.getByName('gt')[0]
			},
			cache: 'force-cache',
			redirect: 'follow',
			signal: controller.signal
			});
	} else {
		req = new Request(`${url}${eq}`,{
			headers: {
				'authorization': `Bearer ${ebt}`,
				'x-csrf-token': cookie.getByName('ct0')[0],
				'x-twitter-auth-type': 'OAuth2Session'
			},
			cache: 'force-cache',
			redirect: 'follow',
			mode: 'cors',
			credentials: 'include',
			signal: controller.signal
		});
	}
	
	return req;
}


function datef(date, f) {
	let week_l;
	document.documentElement.getAttribute('lang') == 'ja'
		? week_l = ['日', '月', '火', '水', '木', '金', '土']
		: week_l = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
	
	let ye, mo, da, we, ho, mi, se;
	ye = date.getFullYear().toString().slice(-2);
	mo = ('0' + (date.getMonth() + 1)).slice(-2);
	da = ('0' + date.getDate()).slice(-2);
	// we = week_l[(((date.getDay() % wll) + wll) % wll)];
	we = week_l[date.getDay()];
	ho = ('0' + date.getHours()).slice(-2);
	mi = ('0' + date.getMinutes()).slice(-2);
	se = ('0' + date.getSeconds()).slice(-2);
	
	return f == 1 ? `${ye}-${mo}/${da} ${ho}:${mi}`
		: f == 2 ? `${ye}-${mo}/${da}(${we}) ${ho}:${mi},${se}`
		: f == 3 ? `${ye}/${mo}/${da} ${ho}:${mi}`
		: f == 4 ? `${ye}/${mo}/${da}(${we}) ${ho}:${mi}:${se}`
		: `${ye}/${mo}/${da}(${we}) ${ho}:${mi}:${se}`;
}


(function() {
	console.log(`${MYNAME}: start.`);
	
	main_track(true);
	observer.observe(document.documentElement, {childList:true, subtree:true});
	loop();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址