Duck Google

Adds DuckDuckGo !bangs to Google search

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
})();