DGG Auto Image Embed (10.3.2 + Imgur album support)

Preview images linked in destiny.gg chat. NSFW blur, hide-link, sticky autoscroll, optional 4chan block. Stable de-duplication + Imgur page/album resolver.

// ==UserScript==
// @name        DGG Auto Image Embed (10.3.2 + Imgur album support)
// @namespace   boofus
// @description Preview images linked in destiny.gg chat. NSFW blur, hide-link, sticky autoscroll, optional 4chan block. Stable de-duplication + Imgur page/album resolver.
// @match       https://www.destiny.gg/embed/chat*
// @icon        https://cdn.destiny.gg/2.49.0/emotes/6296cf7e8ccd0.png
// @version     10.3.2
// @author      boofus (credits: legolas, vyneer)
// @license     MIT
// @grant       GM_xmlhttpRequest
// @run-at      document-end
// @connect     discordapp.net
// @connect     discordapp.com
// @connect     pbs.twimg.com
// @connect     polecat.me
// @connect     imgur.com
// @connect     i.imgur.com
// @connect     gyazo.com
// @connect     redd.it
// @connect     i.redd.it
// @connect     files.catbox.moe
// @connect     catbox.moe
// @connect     i.4cdn.org
// ==/UserScript==

(function(){
  'use strict';

  // No 'g' flag; .test must be stateless
  var directImageRegex = /https?:\/\/(?:i\.redd\.it|redd\.it|pbs\.twimg\.com|(?:media|cdn)\.discordapp\.(?:net|com)|i\.imgur\.com|imgur\.com|gyazo\.com|polecat\.me|(?:files\.)?catbox\.moe|i\.4cdn\.org)\/[^\s"'<>]+?\.(?:png|jpe?g|gif|gifv|webp)(?:\?[^\s"'<>]*)?$/i;
  // Imgur page (album/gallery/single) — no file extension
  var imgurPageRegex = /^https?:\/\/(?:www\.)?imgur\.com\/(a|gallery)\/[A-Za-z0-9]+(?:[/?#].*)?$/i;
  var imgurSinglePageRegex = /^https?:\/\/(?:www\.)?imgur\.com\/(?!a\/|gallery\/)[A-Za-z0-9]+(?:[/?#].*)?$/i;

  var overlay;

  // ---------------- settings ----------------
  function ConfigItem(keyName, defaultValue){ this.keyName = keyName; this.defaultValue = defaultValue; }
  var configItems = {
    BlurNSFW : new ConfigItem("BlurNSFW", true),
    HideLink : new ConfigItem("HideLink", true),
    Block4cdn: new ConfigItem("Block4cdn", true)
  };
  function Config(items, prefix){
    this._prefix = prefix;
    for (var key in items){
      (function(self, key, item){
        var priv = "#" + item.keyName;
        Object.defineProperty(self, key, {
          set: function(v){
            self[priv] = v;
            unsafeWindow.localStorage.setItem(prefix + item.keyName, JSON.stringify(v));
          },
          get: function(){
            if (typeof self[priv] === "undefined"){
              var raw = unsafeWindow.localStorage.getItem(prefix + item.keyName);
              self[priv] = raw != null ? JSON.parse(raw) : item.defaultValue;
            }
            return self[priv];
          }
        });
      })(this, key, items[key]);
    }
  }
  var config = new Config(configItems, "img-util.");

  // ---------------- helpers ----------------
  var shouldStickToBottom = true;

  function getChatEl(){ return unsafeWindow.document.getElementsByClassName("chat-lines")[0]; }
  function isAtBottom(el, threshold){ if (!threshold) threshold = 2; return (el.scrollHeight - el.scrollTop - el.clientHeight) <= threshold; }
  function scrollToBottom(el){ requestAnimationFrame(function(){ el.scrollTop = el.scrollHeight; }); }

  function waitForElm(selector){
    return new Promise(function(resolve){
      var found = unsafeWindow.document.querySelector(selector);
      if (found) return resolve(found);
      var obs = new MutationObserver(function(){
        var el = unsafeWindow.document.querySelector(selector);
        if (el){ obs.disconnect(); resolve(el); }
      });
      obs.observe(unsafeWindow.document.body, { childList:true, subtree:true });
    });
  }

  function addSettings(){
    var area = document.querySelector("#chat-settings-form");
    if (!area){ console.warn("[DGG Img Preview] settings form not found"); return; }
    var title = document.createElement("h4"); title.textContent = "D.GG Img Preview Settings";
    function mk(labelText, keyName){
      var wrap = document.createElement("div"); wrap.className = "form-group checkbox";
      var label = document.createElement("label"); label.textContent = labelText;
      var input = document.createElement("input"); input.type = "checkbox"; input.name = keyName; input.checked = !!config[keyName];
      input.addEventListener("change", function(){ config[keyName] = input.checked; });
      label.prepend(input); wrap.appendChild(label); return wrap;
    }
    area.appendChild(title);
    area.appendChild(mk("Blur nsfw/nsfl images", "BlurNSFW"));
    area.appendChild(mk("Hide link after preview", "HideLink"));
    area.appendChild(mk("Disable 4chan (i.4cdn.org) auto-embeds", "Block4cdn"));
    console.log("[DGG Img Preview] Settings Added");
  }

  // Find message container (direct child of .chat-lines)
  function findMessageContainer(node){
    var chatEl = getChatEl();
    var cur = node;
    while (cur && cur.parentNode && cur.parentNode !== chatEl){ cur = cur.parentNode; }
    if (cur && cur.parentNode === chatEl) return cur;
    return node.parentNode || node;
  }

  // Build stable key so remounts don’t double-embed
  function getMessageKey(container){
    var ts = "", user = "", fallback = "";
    var timeEl = null;
    if (container && container.querySelector) timeEl = container.querySelector('time[data-unixtimestamp]');
    if (timeEl && timeEl.getAttribute) ts = timeEl.getAttribute('data-unixtimestamp') || "";
    if (container && container.getAttribute) user = container.getAttribute('data-username') || "";
    if (!user && container && container.querySelector){
      var uEl = container.querySelector('[data-username]');
      if (uEl && uEl.getAttribute) user = uEl.getAttribute('data-username') || "";
    }
    if (!ts && !user){
      var txt = (container && container.textContent) ? container.textContent : "";
      fallback = txt.slice(0, 64);
    }
    return ts + "|" + user + "|" + fallback;
  }

  function hasPreviewForHref(container, href){
    if (!container || !container.querySelectorAll) return false;
    var imgs = container.querySelectorAll('img[data-dgg-preview-src], img.__dggPreview');
    for (var i=0; i<imgs.length; i++){
      var a = imgs[i].getAttribute('data-dgg-preview-src');
      if (a === href) return true;
    }
    return false;
  }

  // --------- global de-dupe ---------
  var processedKeys = new Set();
  var inFlight = new WeakSet();

  // --------- resolvers ---------
  function isDirectImageUrl(u){ return directImageRegex.test(u); }
  function isImgurPage(u){ return imgurPageRegex.test(u) || imgurSinglePageRegex.test(u); }

  // Resolve Imgur page -> direct i.imgur.com image using og:image
  function resolveImgurToDirect(u, onSuccess, onFail){
    GM_xmlhttpRequest({
      method: "GET",
      url: u,
      responseType: "text",
      onload: function(res){
        try{
          var html = res.responseText || "";
          var doc = new DOMParser().parseFromString(html, "text/html");
          var meta = null;
          if (doc && doc.querySelector){
            meta = doc.querySelector('meta[property="og:image"]');
            if (!meta) meta = doc.querySelector('meta[name="twitter:image"]');
          }
          var direct = meta && meta.getAttribute ? meta.getAttribute('content') : "";
          if (direct && /^https?:\/\//i.test(direct)) {
            onSuccess(direct);
          } else {
            if (onFail) onFail();
          }
        } catch(e){
          if (onFail) onFail();
        }
      },
      onerror: function(){ if (onFail) onFail(); }
    });
  }

  // Fetch an image as base64 and append preview
  function fetchAndAppend(el, fetchUrl, originalHref){
    GM_xmlhttpRequest({
      method: "GET",
      url: fetchUrl,
      responseType: "blob",
      onload: function(res){
        var reader = new FileReader();
        reader.readAsDataURL(res.response);
        reader.onloadend = function(){
          var base64 = reader.result;

          var container = findMessageContainer(el);
          // Re-check existence just before append (covers fast remounts)
          if (hasPreviewForHref(container, originalHref)){
            if (config.HideLink && el.parentNode) el.parentNode.removeChild(el);
            inFlight.delete(el);
            return;
          }

          var img = unsafeWindow.document.createElement("img");
          img.src = base64;
          img.setAttribute("data-dgg-preview-src", originalHref); // key by the original link
          img.className = "__dggPreview";
          img.style.maxHeight = "300px";
          img.style.maxWidth  = "300px";
          img.style.marginLeft = "5px";
          img.style.marginBottom= "10px";
          img.style.marginTop   = "10px";
          img.style.display     = "block";
          img.style.cursor      = "pointer";

          var blurred = false;
          if (config.BlurNSFW && ((el.className && el.className.indexOf("nsfw") !== -1) || (el.className && el.className.indexOf("nsfl") !== -1))){
            img.style.filter = "blur(15px)";
            blurred = true;
          }

          img.onclick = function(){
            if (blurred){
              img.style.filter = "blur(0px)";
              blurred = false;
            } else {
              overlay.style.display = "flex";
              var full = unsafeWindow.document.createElement("img");
              full.src = base64;
              full.style.maxHeight = "70%";
              full.style.maxWidth  = "70%";
              full.style.display   = "block";
              full.style.position  = "relative";
              overlay.appendChild(full);

              var open = document.createElement("a");
              open.href = fetchUrl; // open the resolved direct file
              open.textContent = "Open Original";
              open.target = "_blank";
              open.style.marginTop = "5px";
              open.style.color = "#999";
              overlay.appendChild(open);
            }
          };

          var chatEl = getChatEl();
          function onReady(){
            if (shouldStickToBottom){
              requestAnimationFrame(function(){ chatEl.scrollTop = chatEl.scrollHeight; });
            }
          }

          if (typeof img.decode === "function"){
            img.decode().then(onReady).catch(onReady);
          } else {
            img.addEventListener("load", onReady, { once:true });
            img.addEventListener("error", onReady, { once:true });
          }

          if (el.parentNode){
            el.parentNode.appendChild(img);
            if (config.HideLink) el.parentNode.removeChild(el);
          }
          inFlight.delete(el);
        };
      }
    });
  }

  function handleLink(el){
    // Block 4cdn if configured
    if (config.Block4cdn && /https?:\/\/i\.4cdn\.org\//i.test(el.href)) return;

    var container = findMessageContainer(el);
    var msgKey = getMessageKey(container);
    var globalKey = msgKey + "|" + el.href;

    // Stable global de-dupe (message, href)
    if (processedKeys.has(globalKey)) return;
    processedKeys.add(globalKey);

    // If this message already has a preview tied to this href, skip
    if (hasPreviewForHref(container, el.href)){
      if (config.HideLink && el.parentNode) el.parentNode.removeChild(el);
      return;
    }

    // Per-link guards
    if (el.dataset && el.dataset.dggPreviewed === "1") return;
    if (inFlight.has(el)) return;
    if (el.dataset) el.dataset.dggPreviewed = "1";
    inFlight.add(el);

    // Direct image → fetch blob
    if (isDirectImageUrl(el.href)){
      fetchAndAppend(el, el.href, el.href);
      return;
    }

    // Imgur album/single page → resolve og:image then fetch
    if (isImgurPage(el.href)){
      resolveImgurToDirect(el.href, function(directUrl){
        fetchAndAppend(el, directUrl, el.href);
      }, function(){
        // Couldn’t resolve — just stop gracefully
        inFlight.delete(el);
      });
      return;
    }

    // Other non-direct pages (e.g., gyazo.com/<id>) could be added similarly:
    // - implement a gyazo resolver using og:image if needed
    inFlight.delete(el);
  }

  var chatObserver = new MutationObserver(function(mutations){
    for (var mi=0; mi<mutations.length; mi++){
      var m = mutations[mi];
      for (var ni=0; ni<m.addedNodes.length; ni++){
        var n = m.addedNodes[ni];
        if (!n || n.nodeType !== 1) continue; // ELEMENT_NODE
        if (!n.querySelectorAll) continue;
        var links = n.querySelectorAll('.externallink:not([data-dgg-previewed="1"])');
        for (var i=0; i<links.length; i++){
          handleLink(links[i]);
        }
      }
    }
  });

  // ---- bootstrap ----
  console.log("[DGG Img Preview] Connecting");
  waitForElm(".chat-lines").then(function(chatEl){
    // Overlay
    overlay = document.createElement("div");
    overlay.style.position = "fixed";
    overlay.style.justifyContent = "center";
    overlay.style.alignItems = "center";
    overlay.style.inset = "0px";
    overlay.style.margin = "0px";
    overlay.style.background = "rgba(0,0,0,0.85)";
    overlay.style.zIndex = "999";
    overlay.style.height = "100%";
    overlay.style.width = "100%";
    overlay.style.display = "none";
    overlay.style.flexDirection = "column";
    overlay.onclick = function(){ overlay.style.display = "none"; overlay.innerHTML = ""; };
    document.body.appendChild(overlay);

    // Sticky-bottom engine
    shouldStickToBottom = isAtBottom(chatEl);
    chatEl.addEventListener("scroll", function(){ shouldStickToBottom = isAtBottom(chatEl); }, { passive:true });

    var ro = new ResizeObserver(function(){ if (shouldStickToBottom) scrollToBottom(chatEl); });
    ro.observe(chatEl);

    chatObserver.observe(chatEl, { attributes:false, childList:true, characterData:false, subtree:true });

    console.log("[DGG Img Preview] Connected");
    console.log("[DGG Img Preview] Adding Settings");
    addSettings();
  });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址