Enhance widgets.js Twitter replacement

This commit is contained in:
Zed 2026-08-09 02:11:50 +07:00
commit 5733297f92

View file

@ -1,135 +1,197 @@
/** /**
* Drop-in replacement for Twitter's widgets.js * Drop-in replacement for Twitter's widgets.js
* Include this script to automatically convert twitter-tweet blockquotes to Nitter embeds * Converts twitter-tweet/twitter-video blockquotes to Nitter embeds
*
* Usage: LibRedirect can redirect platform.twitter.com/widgets.js to nitter.net/widgets.js
*/ */
(function () { (function () {
"use strict"; "use strict";
// Determine the Nitter instance URL from the script src, or fall back to current origin // Idempotent - only run once
var widgetScripts = document.querySelectorAll('script[src*="widgets.js"]'); if (window.__nitterWidgets) return;
var NITTER_URL = widgetScripts.length window.__nitterWidgets = true;
? new URL(widgetScripts[widgetScripts.length - 1].src).origin
// Determine Nitter instance URL from script src, or fall back to current origin
var scripts = document.querySelectorAll('script[src*="widgets.js"]');
var NITTER = scripts.length
? new URL(scripts[scripts.length - 1].src).origin
: location.origin; : location.origin;
var TWEET_URL_PATTERN = var TWEET_PATTERN = /(?:twitter\.com|x\.com)\/([^\/]+)\/status\/(\d+)/i;
/^https?:\/\/(?:twitter\.com|x\.com)\/([^\/]+)\/status\/(\d+)/i; var SELECTOR = "blockquote.twitter-tweet, blockquote.twitter-video";
// Track iframes by URL for resize messages // Ready callback queue
var iframesByUrl = {}; var readyCallbacks = [];
var isReady = false;
/** function parseTweetUrl(url) {
* Extract tweet info (username and ID) from a blockquote's links if (!url) return null;
*/ var match = TWEET_PATTERN.exec(url);
function findTweetInfo(blockquote) { if (match) return { username: match[1], id: match[2] };
var links = blockquote.querySelectorAll("a"); // Fallback: extract any large number (tweet ID)
for (var i = 0; i < links.length; i++) { var idMatch = url.match(/(\d{15,})/);
var match = TWEET_URL_PATTERN.exec(links[i].href); return idMatch ? { username: null, id: idMatch[1] } : null;
if (match) {
return { username: match[1], tweetId: match[2] };
}
}
return null;
} }
/** function createIframe(tweet, options) {
* Transform all twitter-tweet blockquotes into Nitter embed iframes var embedUrl = tweet.username
*/ ? NITTER + "/" + tweet.username + "/status/" + tweet.id + "/embed"
function transformBlockquotes() { : NITTER + "/i/status/" + tweet.id + "/embed";
var blockquotes = document.querySelectorAll("blockquote.twitter-tweet");
for (var i = 0; i < blockquotes.length; i++) {
var blockquote = blockquotes[i];
var tweetInfo = findTweetInfo(blockquote);
if (!tweetInfo) continue;
var embedUrl =
NITTER_URL +
"/" +
tweetInfo.username +
"/status/" +
tweetInfo.tweetId +
"/embed";
var iframe = document.createElement("iframe"); var iframe = document.createElement("iframe");
iframe.src = embedUrl; iframe.src = embedUrl;
iframe.style.cssText = iframe.className = "nitter-embed-frame";
"width: 100%; max-width: 550px; height: 250px; border: none; display: block;"; iframe.setAttribute("allowtransparency", "true");
iframe.setAttribute("frameborder", "0");
iframe.setAttribute("scrolling", "no");
iframe.loading = "lazy"; iframe.loading = "lazy";
// Track iframe for resize messages // Styling with data attribute support
if (!iframesByUrl[embedUrl]) { var width = options.width || "550";
iframesByUrl[embedUrl] = []; var align = options.align || "center";
} var margin = align === "center" ? "10px auto" :
iframesByUrl[embedUrl].push(iframe); align === "right" ? "10px 0 10px auto" : "10px auto 10px 0";
blockquote.parentNode.replaceChild(iframe, blockquote); iframe.style.cssText = "width:100%;max-width:" + width + "px;height:250px;border:none;display:block;margin:" + margin;
return iframe;
}
function processBlockquote(bq) {
if (bq.dataset.nitterProcessed) return false;
bq.dataset.nitterProcessed = "true";
// Find tweet URL in links
var tweet = null;
var links = bq.querySelectorAll("a[href]");
for (var i = 0; i < links.length; i++) {
tweet = parseTweetUrl(links[i].href);
if (tweet) break;
}
if (!tweet) {
console.warn("[Nitter widgets.js] No tweet URL found in blockquote");
return false;
}
// Read Twitter's data attributes
var options = {
width: bq.dataset.width,
align: bq.dataset.align,
theme: bq.dataset.theme
};
var iframe = createIframe(tweet, options);
// Hide original (keep as fallback), insert iframe after
bq.style.display = "none";
bq.parentNode.insertBefore(iframe, bq.nextSibling);
return true;
}
function processEmbeds(container) {
var root = container || document;
var blockquotes = root.querySelectorAll(SELECTOR + ":not([data-nitter-processed])");
var count = 0;
for (var i = 0; i < blockquotes.length; i++) {
if (processBlockquote(blockquotes[i])) count++;
}
if (count > 0) {
console.log("[Nitter widgets.js] Processed " + count + " embed(s)");
} }
} }
/** function handleResize(event) {
* Handle resize messages from Nitter embeds
*/
function handleResizeMessage(event) {
if (!Array.isArray(event.data) || event.data[0] !== "resizeIframe") return; if (!Array.isArray(event.data) || event.data[0] !== "resizeIframe") return;
var h = event.data[1] && event.data[1].h;
if (!h || h <= 0) return;
var data = event.data[1]; // Find iframe by matching contentWindow
if (!data.h || data.h <= 0) return; var iframes = document.querySelectorAll("iframe.nitter-embed-frame");
var iframes = iframesByUrl[data.url];
if (iframes) {
for (var i = 0; i < iframes.length; i++) { for (var i = 0; i < iframes.length; i++) {
iframes[i].style.height = data.h + "px"; if (iframes[i].contentWindow === event.source) {
iframes[i].style.height = h + "px";
break;
} }
} }
} }
// Remove any Twitter widget scripts that might have been loaded function observeDOM() {
if (!window.MutationObserver || !document.body) return;
new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var nodes = mutations[i].addedNodes;
for (var j = 0; j < nodes.length; j++) {
var node = nodes[j];
if (node.nodeType !== 1) continue;
if ((node.matches && node.matches(SELECTOR)) ||
(node.querySelector && node.querySelector(SELECTOR))) {
processEmbeds();
return;
}
}
}
}).observe(document.body, { childList: true, subtree: true });
}
function fireReady() {
isReady = true;
for (var i = 0; i < readyCallbacks.length; i++) {
try { readyCallbacks[i](window.twttr); } catch (e) {}
}
readyCallbacks = [];
}
// Expose twttr API for compatibility
var prevTwttr = window.twttr;
window.twttr = {
widgets: {
load: function (el) { processEmbeds(el); },
createTweet: function (id, container, opts) {
if (!container) return Promise.reject("No container");
var iframe = createIframe({ id: id, username: null }, opts || {});
container.appendChild(iframe);
return Promise.resolve(iframe);
},
loaded: true
},
ready: function (cb) {
if (typeof cb !== "function") return;
if (isReady) cb(window.twttr);
else readyCallbacks.push(cb);
},
_e: []
};
// Process any callbacks queued before we loaded (twttr._e pattern)
if (prevTwttr && prevTwttr._e) {
for (var i = 0; i < prevTwttr._e.length; i++) {
try { prevTwttr._e[i](); } catch (e) {}
}
}
// Remove Twitter scripts that might have snuck in
var twitterScripts = document.querySelectorAll( var twitterScripts = document.querySelectorAll(
'script[src*="platform.twitter.com/widgets.js"], script[src*="platform.x.com/widgets.js"]', 'script[src*="platform.twitter.com"], script[src*="platform.x.com"]'
); );
for (var i = 0; i < twitterScripts.length; i++) { for (var i = 0; i < twitterScripts.length; i++) {
twitterScripts[i].remove(); twitterScripts[i].remove();
} }
// Listen for resize messages from embeds // Initialize
window.addEventListener("message", handleResizeMessage); function init() {
window.addEventListener("message", handleResize);
processEmbeds();
observeDOM();
fireReady();
}
// Transform existing blockquotes
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", transformBlockquotes); document.addEventListener("DOMContentLoaded", init);
} else { } else {
transformBlockquotes(); init();
} }
// Watch for dynamically added blockquotes
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var addedNodes = mutations[i].addedNodes;
for (var j = 0; j < addedNodes.length; j++) {
var node = addedNodes[j];
if (node.nodeType !== 1) continue;
var isTwitterBlockquote =
node.matches && node.matches("blockquote.twitter-tweet");
var containsTwitterBlockquote =
node.querySelector && node.querySelector("blockquote.twitter-tweet");
if (isTwitterBlockquote || containsTwitterBlockquote) {
transformBlockquotes();
return;
}
}
}
});
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
}
// Provide a fake twttr object for compatibility with sites that check for it
window.twttr = window.twttr || {};
window.twttr.widgets = {
load: transformBlockquotes,
loaded: true,
};
})(); })();