Duck Google

Adds DuckDuckGo !bangs to Google search

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Duck Google
// @namespace    garcialnk.com
// @license      MIT
// @version      1.0.1
// @description  Adds DuckDuckGo !bangs to Google search
// @author       GarciaLnk
// @match        *://www.google.com/search*
// @match        *://google.com/search*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// @sandbox      DOM
// @tag          utilities
// @connect      duckduckgo.com
// ==/UserScript==
/* jshint esversion: 11 */

(function () {
  "use strict";

  /**
   * DuckDuckGo bang object
   * @typedef {Object} Bang
   * @property {string} s - The search engine name
   * @property {string} t - The bang command
   * @property {string} u - The search URL with {{{s}}} as the search query
   */

  const CUSTOM_BANGS = [
    {
      s: "T3 Chat",
      t: "t3",
      u: "https://www.t3.chat/new?q={{{s}}}",
    },
  ];

  const BANG_CACHE_KEY = "bangs-cache";
  const BANG_CACHE_EXPIRY_KEY = "bangs-cache-expiry";
  const CACHE_EXPIRY_MS = 1000 * 60 * 60 * 24 * 7; // 7 days

  /**
   * Fetches the DuckDuckGo bangs from the cache or the server
   * @returns {Promise<Bang[]>} The list of bangs
   */
  function getBangs() {
    return new Promise((resolve) => {
      const cachedBangs = GM_getValue(BANG_CACHE_KEY);
      const cacheExpiry = GM_getValue(BANG_CACHE_EXPIRY_KEY, 0);

      if (cachedBangs && Date.now() < cacheExpiry) {
        resolve([...CUSTOM_BANGS, ...cachedBangs]);
        return;
      }

      GM_xmlhttpRequest({
        method: "GET",
        url: "https://duckduckgo.com/bang.js",
        onload: function (response) {
          try {
            const bangs = JSON.parse(response.responseText);
            GM_setValue(BANG_CACHE_KEY, bangs);
            GM_setValue(BANG_CACHE_EXPIRY_KEY, Date.now() + CACHE_EXPIRY_MS);

            resolve([...CUSTOM_BANGS, ...bangs]);
          } catch (e) {
            console.error("Error parsing bangs:", e);
            resolve(CUSTOM_BANGS);
          }
        },
        onerror: function () {
          console.error("Failed to fetch bangs");
          resolve(CUSTOM_BANGS);
        },
      });
    });
  }

  /**
   * Parses the bang from the query
   * @param {string} query The search query
   * @returns {{bang: string, cleanQuery: string} | null} The bang and the clean query
   */
  function parseBang(query) {
    // regex to match !bang at start or end of query
    const match = query.match(/^!(\S+)\s+(.+)$|^(.+)\s+!(\S+)$/i);

    if (match) {
      if (match[1]) {
        // !bang query
        return {
          bang: match[1].toLowerCase(),
          cleanQuery: match[2].trim(),
        };
      } else {
        // query !bang
        return {
          bang: match[4].toLowerCase(),
          cleanQuery: match[3].trim(),
        };
      }
    }

    return null;
  }

  /**
   * Redirects to the search engine URL
   * @param {Bang[]} bangs The list of bangs
   * @returns {void}
   */
  function redirectToBang(bangs) {
    const url = new URL(window.location.href);
    const query = url.searchParams.get("q")?.trim() ?? "";

    if (!query) return;

    const bangData = parseBang(query);
    if (!bangData) return;

    const selectedBang = bangs.find((b) => b.t === bangData.bang);
    if (!selectedBang) return;

    // Format of the url is:
    // https://www.google.com/search?q={{{s}}}
    const searchUrl = selectedBang.u.replace(
      "{{{s}}}",
      // Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck"
      encodeURIComponent(bangData.cleanQuery).replace(/%2F/g, "/"),
    );

    if (searchUrl) {
      window.location.replace(searchUrl);
    }
  }

  /**
   * Initializes the script
   * @returns {void}
   */
  async function init() {
    const bangs = await getBangs();
    redirectToBang(bangs);
  }

  init();
})();