widgets.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * Drop-in replacement for Twitter's widgets.js
  3. * Redirects twitter-tweet blockquotes to Nitter embeds
  4. */
  5. (function () {
  6. "use strict";
  7. if (window.__nitterWidgets) return;
  8. window.__nitterWidgets = true;
  9. var scripts = document.querySelectorAll('script[src*="widgets.js"]');
  10. var NITTER = scripts.length
  11. ? new URL(scripts[scripts.length - 1].src).origin
  12. : location.origin;
  13. var TWEET_RE = /(?:twitter\.com|x\.com)\/([^\/]+)\/status\/(\d+)/i;
  14. var SELECTOR = "blockquote.twitter-tweet, blockquote.twitter-video";
  15. var readyCallbacks = [];
  16. var eventCallbacks = {};
  17. var isReady = false;
  18. function safeCall(fn, arg) {
  19. try { fn(arg); } catch (e) {}
  20. }
  21. function fireEvent(name, data) {
  22. (eventCallbacks[name] || []).forEach(function (cb) { safeCall(cb, data); });
  23. }
  24. function parseTweetUrl(url) {
  25. if (!url) return null;
  26. var m = TWEET_RE.exec(url);
  27. if (m) return { user: m[1], id: m[2] };
  28. m = url.match(/(\d{15,})/);
  29. return m ? { user: null, id: m[1] } : null;
  30. }
  31. function createIframe(tweet, opts) {
  32. var url;
  33. if (opts.videoOnly) {
  34. url = NITTER + "/i/videos/tweet/" + tweet.id;
  35. } else {
  36. var path = tweet.user ? "/" + tweet.user : "/i";
  37. url = NITTER + path + "/status/" + tweet.id + "/embed";
  38. if (opts.theme) {
  39. var theme = opts.theme === "dark" ? "nitter" :
  40. opts.theme === "light" ? "twitter" : opts.theme;
  41. url += "?theme=" + encodeURIComponent(theme);
  42. }
  43. }
  44. var iframe = document.createElement("iframe");
  45. iframe.src = url;
  46. iframe.className = "nitter-embed-frame";
  47. iframe.loading = "lazy";
  48. iframe.setAttribute("allowtransparency", "true");
  49. iframe.setAttribute("frameborder", "0");
  50. iframe.setAttribute("scrolling", "no");
  51. if (opts.videoOnly) iframe.setAttribute("allowfullscreen", "true");
  52. var width = opts.width || 550;
  53. var margin = opts.align === "center" ? "10px auto" :
  54. opts.align === "right" ? "10px 0 10px auto" : "10px 0";
  55. iframe.style.cssText =
  56. "width:100%;max-width:" + width + "px;height:250px;" +
  57. "border:none;display:block;margin:" + margin;
  58. iframe.addEventListener("load", function () {
  59. fireEvent("rendered", { target: iframe });
  60. });
  61. return iframe;
  62. }
  63. function processBlockquote(bq) {
  64. if (bq.dataset.nitterProcessed) return false;
  65. bq.dataset.nitterProcessed = "true";
  66. var tweet = null;
  67. var links = bq.querySelectorAll("a[href]");
  68. for (var i = 0; i < links.length && !tweet; i++) {
  69. tweet = parseTweetUrl(links[i].href);
  70. }
  71. if (!tweet) return false;
  72. var d = bq.dataset;
  73. var iframe = createIframe(tweet, {
  74. width: d.mediaMaxWidth || d.width,
  75. align: d.align,
  76. theme: d.theme,
  77. videoOnly: d.mediaMaxWidth !== undefined
  78. });
  79. bq.style.display = "none";
  80. bq.parentNode.insertBefore(iframe, bq.nextSibling);
  81. return true;
  82. }
  83. function processEmbeds(root) {
  84. var bqs = (root || document).querySelectorAll(SELECTOR + ":not([data-nitter-processed])");
  85. for (var i = 0; i < bqs.length; i++) processBlockquote(bqs[i]);
  86. }
  87. function handleResize(e) {
  88. if (!Array.isArray(e.data) || e.data[0] !== "resizeIframe") return;
  89. var h = e.data[1] && e.data[1].h;
  90. if (!h || h <= 0) return;
  91. var frames = document.querySelectorAll("iframe.nitter-embed-frame");
  92. for (var i = 0; i < frames.length; i++) {
  93. if (frames[i].contentWindow === e.source) {
  94. frames[i].style.height = h + "px";
  95. return;
  96. }
  97. }
  98. }
  99. function observeDOM() {
  100. if (!window.MutationObserver || !document.body) return;
  101. function matches(el) {
  102. return el.matches(SELECTOR) || el.querySelector(SELECTOR);
  103. }
  104. new MutationObserver(function (muts) {
  105. var found = muts.some(function (mut) {
  106. return Array.prototype.some.call(mut.addedNodes, function (n) {
  107. return n.nodeType === 1 && matches(n);
  108. });
  109. });
  110. if (found) processEmbeds();
  111. }).observe(document.body, { childList: true, subtree: true });
  112. }
  113. function embedTweet(id, container, opts) {
  114. if (!container) return Promise.reject("No container");
  115. var iframe = createIframe({ id: id, user: null }, opts || {});
  116. container.appendChild(iframe);
  117. return Promise.resolve(iframe);
  118. }
  119. var prevTwttr = window.twttr;
  120. window.twttr = {
  121. widgets: {
  122. load: processEmbeds,
  123. createTweet: embedTweet,
  124. createTweetEmbed: embedTweet,
  125. createVideo: embedTweet,
  126. loaded: true
  127. },
  128. events: {
  129. bind: function (name, cb) {
  130. if (typeof cb !== "function") return;
  131. if (!eventCallbacks[name]) eventCallbacks[name] = [];
  132. eventCallbacks[name].push(cb);
  133. },
  134. unbind: function (name, cb) {
  135. if (!eventCallbacks[name]) return;
  136. eventCallbacks[name] = cb
  137. ? eventCallbacks[name].filter(function (f) { return f !== cb; })
  138. : [];
  139. }
  140. },
  141. ready: function (cb) {
  142. if (typeof cb !== "function") return;
  143. if (isReady) cb(window.twttr);
  144. else readyCallbacks.push(cb);
  145. },
  146. _e: []
  147. };
  148. // Process callbacks queued before load (twttr._e pattern)
  149. if (prevTwttr && prevTwttr._e) {
  150. prevTwttr._e.forEach(function (cb) { safeCall(cb); });
  151. }
  152. // Remove any Twitter scripts that snuck through
  153. document.querySelectorAll('script[src*="platform.twitter.com"], script[src*="platform.x.com"]')
  154. .forEach(function (s) { s.remove(); });
  155. function init() {
  156. window.addEventListener("message", handleResize);
  157. processEmbeds();
  158. observeDOM();
  159. isReady = true;
  160. readyCallbacks.forEach(function (cb) { safeCall(cb, window.twttr); });
  161. readyCallbacks = [];
  162. }
  163. if (document.readyState === "loading") {
  164. document.addEventListener("DOMContentLoaded", init);
  165. } else {
  166. init();
  167. }
  168. })();