widgets.js 5.8 KB

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