widgets.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /**
  2. * Drop-in replacement for Twitter's widgets.js
  3. * Converts twitter-tweet/twitter-video blockquotes to Nitter embeds
  4. *
  5. * Usage: LibRedirect can redirect platform.twitter.com/widgets.js to nitter.net/widgets.js
  6. */
  7. (function () {
  8. "use strict";
  9. // Idempotent - only run once
  10. if (window.__nitterWidgets) return;
  11. window.__nitterWidgets = true;
  12. // Determine Nitter instance URL from script src, or fall back to current origin
  13. var scripts = document.querySelectorAll('script[src*="widgets.js"]');
  14. var NITTER = scripts.length
  15. ? new URL(scripts[scripts.length - 1].src).origin
  16. : location.origin;
  17. var TWEET_PATTERN = /(?:twitter\.com|x\.com)\/([^\/]+)\/status\/(\d+)/i;
  18. var SELECTOR = "blockquote.twitter-tweet, blockquote.twitter-video";
  19. // Ready callback queue
  20. var readyCallbacks = [];
  21. var isReady = false;
  22. function parseTweetUrl(url) {
  23. if (!url) return null;
  24. var match = TWEET_PATTERN.exec(url);
  25. if (match) return { username: match[1], id: match[2] };
  26. // Fallback: extract any large number (tweet ID)
  27. var idMatch = url.match(/(\d{15,})/);
  28. return idMatch ? { username: null, id: idMatch[1] } : null;
  29. }
  30. function createIframe(tweet, options) {
  31. var embedUrl = tweet.username
  32. ? NITTER + "/" + tweet.username + "/status/" + tweet.id + "/embed"
  33. : NITTER + "/i/status/" + tweet.id + "/embed";
  34. var iframe = document.createElement("iframe");
  35. iframe.src = embedUrl;
  36. iframe.className = "nitter-embed-frame";
  37. iframe.setAttribute("allowtransparency", "true");
  38. iframe.setAttribute("frameborder", "0");
  39. iframe.setAttribute("scrolling", "no");
  40. iframe.loading = "lazy";
  41. // Styling with data attribute support
  42. var width = options.width || "550";
  43. var align = options.align || "center";
  44. var margin = align === "center" ? "10px auto" :
  45. align === "right" ? "10px 0 10px auto" : "10px auto 10px 0";
  46. iframe.style.cssText = "width:100%;max-width:" + width + "px;height:250px;border:none;display:block;margin:" + margin;
  47. return iframe;
  48. }
  49. function processBlockquote(bq) {
  50. if (bq.dataset.nitterProcessed) return false;
  51. bq.dataset.nitterProcessed = "true";
  52. // Find tweet URL in links
  53. var tweet = null;
  54. var links = bq.querySelectorAll("a[href]");
  55. for (var i = 0; i < links.length; i++) {
  56. tweet = parseTweetUrl(links[i].href);
  57. if (tweet) break;
  58. }
  59. if (!tweet) {
  60. console.warn("[Nitter widgets.js] No tweet URL found in blockquote");
  61. return false;
  62. }
  63. // Read Twitter's data attributes
  64. var options = {
  65. width: bq.dataset.width,
  66. align: bq.dataset.align,
  67. theme: bq.dataset.theme
  68. };
  69. var iframe = createIframe(tweet, options);
  70. // Hide original (keep as fallback), insert iframe after
  71. bq.style.display = "none";
  72. bq.parentNode.insertBefore(iframe, bq.nextSibling);
  73. return true;
  74. }
  75. function processEmbeds(container) {
  76. var root = container || document;
  77. var blockquotes = root.querySelectorAll(SELECTOR + ":not([data-nitter-processed])");
  78. var count = 0;
  79. for (var i = 0; i < blockquotes.length; i++) {
  80. if (processBlockquote(blockquotes[i])) count++;
  81. }
  82. if (count > 0) {
  83. console.log("[Nitter widgets.js] Processed " + count + " embed(s)");
  84. }
  85. }
  86. function handleResize(event) {
  87. if (!Array.isArray(event.data) || event.data[0] !== "resizeIframe") return;
  88. var h = event.data[1] && event.data[1].h;
  89. if (!h || h <= 0) return;
  90. // Find iframe by matching contentWindow
  91. var iframes = document.querySelectorAll("iframe.nitter-embed-frame");
  92. for (var i = 0; i < iframes.length; i++) {
  93. if (iframes[i].contentWindow === event.source) {
  94. iframes[i].style.height = h + "px";
  95. break;
  96. }
  97. }
  98. }
  99. function observeDOM() {
  100. if (!window.MutationObserver || !document.body) return;
  101. new MutationObserver(function (mutations) {
  102. for (var i = 0; i < mutations.length; i++) {
  103. var nodes = mutations[i].addedNodes;
  104. for (var j = 0; j < nodes.length; j++) {
  105. var node = nodes[j];
  106. if (node.nodeType !== 1) continue;
  107. if ((node.matches && node.matches(SELECTOR)) ||
  108. (node.querySelector && node.querySelector(SELECTOR))) {
  109. processEmbeds();
  110. return;
  111. }
  112. }
  113. }
  114. }).observe(document.body, { childList: true, subtree: true });
  115. }
  116. function fireReady() {
  117. isReady = true;
  118. for (var i = 0; i < readyCallbacks.length; i++) {
  119. try { readyCallbacks[i](window.twttr); } catch (e) {}
  120. }
  121. readyCallbacks = [];
  122. }
  123. // Expose twttr API for compatibility
  124. var prevTwttr = window.twttr;
  125. window.twttr = {
  126. widgets: {
  127. load: function (el) { processEmbeds(el); },
  128. createTweet: function (id, container, opts) {
  129. if (!container) return Promise.reject("No container");
  130. var iframe = createIframe({ id: id, username: null }, opts || {});
  131. container.appendChild(iframe);
  132. return Promise.resolve(iframe);
  133. },
  134. loaded: true
  135. },
  136. ready: function (cb) {
  137. if (typeof cb !== "function") return;
  138. if (isReady) cb(window.twttr);
  139. else readyCallbacks.push(cb);
  140. },
  141. _e: []
  142. };
  143. // Process any callbacks queued before we loaded (twttr._e pattern)
  144. if (prevTwttr && prevTwttr._e) {
  145. for (var i = 0; i < prevTwttr._e.length; i++) {
  146. try { prevTwttr._e[i](); } catch (e) {}
  147. }
  148. }
  149. // Remove Twitter scripts that might have snuck in
  150. var twitterScripts = document.querySelectorAll(
  151. 'script[src*="platform.twitter.com"], script[src*="platform.x.com"]'
  152. );
  153. for (var i = 0; i < twitterScripts.length; i++) {
  154. twitterScripts[i].remove();
  155. }
  156. // Initialize
  157. function init() {
  158. window.addEventListener("message", handleResize);
  159. processEmbeds();
  160. observeDOM();
  161. fireReady();
  162. }
  163. if (document.readyState === "loading") {
  164. document.addEventListener("DOMContentLoaded", init);
  165. } else {
  166. init();
  167. }
  168. })();