mirror of
https://github.com/zedeus/nitter
synced 2026-09-05 14:49:32 +00:00
Enhance widgets.js Twitter replacement
This commit is contained in:
parent
a00c6cc11d
commit
5733297f92
1 changed files with 160 additions and 98 deletions
|
|
@ -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");
|
|
||||||
|
var iframe = document.createElement("iframe");
|
||||||
|
iframe.src = embedUrl;
|
||||||
|
iframe.className = "nitter-embed-frame";
|
||||||
|
iframe.setAttribute("allowtransparency", "true");
|
||||||
|
iframe.setAttribute("frameborder", "0");
|
||||||
|
iframe.setAttribute("scrolling", "no");
|
||||||
|
iframe.loading = "lazy";
|
||||||
|
|
||||||
|
// Styling with data attribute support
|
||||||
|
var width = options.width || "550";
|
||||||
|
var align = options.align || "center";
|
||||||
|
var margin = align === "center" ? "10px auto" :
|
||||||
|
align === "right" ? "10px 0 10px auto" : "10px auto 10px 0";
|
||||||
|
|
||||||
|
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++) {
|
for (var i = 0; i < blockquotes.length; i++) {
|
||||||
var blockquote = blockquotes[i];
|
if (processBlockquote(blockquotes[i])) count++;
|
||||||
var tweetInfo = findTweetInfo(blockquote);
|
}
|
||||||
if (!tweetInfo) continue;
|
|
||||||
|
|
||||||
var embedUrl =
|
if (count > 0) {
|
||||||
NITTER_URL +
|
console.log("[Nitter widgets.js] Processed " + count + " embed(s)");
|
||||||
"/" +
|
|
||||||
tweetInfo.username +
|
|
||||||
"/status/" +
|
|
||||||
tweetInfo.tweetId +
|
|
||||||
"/embed";
|
|
||||||
|
|
||||||
var iframe = document.createElement("iframe");
|
|
||||||
iframe.src = embedUrl;
|
|
||||||
iframe.style.cssText =
|
|
||||||
"width: 100%; max-width: 550px; height: 250px; border: none; display: block;";
|
|
||||||
iframe.loading = "lazy";
|
|
||||||
|
|
||||||
// Track iframe for resize messages
|
|
||||||
if (!iframesByUrl[embedUrl]) {
|
|
||||||
iframesByUrl[embedUrl] = [];
|
|
||||||
}
|
|
||||||
iframesByUrl[embedUrl].push(iframe);
|
|
||||||
|
|
||||||
blockquote.parentNode.replaceChild(iframe, blockquote);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
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");
|
||||||
|
for (var i = 0; i < iframes.length; i++) {
|
||||||
var iframes = iframesByUrl[data.url];
|
if (iframes[i].contentWindow === event.source) {
|
||||||
if (iframes) {
|
iframes[i].style.height = h + "px";
|
||||||
for (var i = 0; i < iframes.length; i++) {
|
break;
|
||||||
iframes[i].style.height = data.h + "px";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue