Skip to content

[Discussion] Automate the permission granting with Greasemonkey script #1

@pe200012

Description

@pe200012

(* I'm sorry that this is an discussion rather than an issue. *)

For those who are tired of granting permissions again and again, here is a workaround for you:

Install Greasemonkey (or other similar extensions), create a new script with the following content and you are good to go:

// ==UserScript==
// @name         ChatGPT CatDesk approval allowlist
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  const cfg = {
    armed: false,
    dryRun: false,

    connector: /^CatDesk$/,
    allowedBody: /[\s\S]/,
    allowButtonText: /^(許可する|Allow)$/i,
    blockedSharedDataKeys: /^$/,

    cooldownMs: 2000,
  };

  let lastClickAt = 0;
  let ui = null;

  const norm = (s) => (s || "").replace(/\s+/g, " ").trim();

  function isVisible(el) {
    const r = el.getBoundingClientRect();
    const s = getComputedStyle(el);
    return r.width > 0 && r.height > 0 && s.display !== "none" && s.visibility !== "hidden";
  }

  function cardText(card) {
    return norm(card.innerText || card.textContent);
  }

  function getConnectorName(card) {
    const img = card.querySelector("img[alt]");
    if (img?.alt) return norm(img.alt);

    const candidates = [...card.querySelectorAll("div, span")]
      .map((el) => norm(el.textContent))
      .filter(Boolean);

    return candidates.find((t) => cfg.connector.test(t)) || "";
  }

  function getTitle(card) {
    return norm(card.querySelector("h2")?.textContent);
  }

  function getSharedDataKeys(card) {
    return [...card.querySelectorAll("dt")]
      .map((dt) => norm(dt.textContent))
      .filter(Boolean);
  }

  function isAllowedCard(card) {
    const connector = getConnectorName(card);
    const body = cardText(card);
    const sharedKeys = getSharedDataKeys(card);
    const blockedKey = sharedKeys.find((k) => cfg.blockedSharedDataKeys.test(k));

    const result = {
      connector,
      title: getTitle(card),
      sharedKeys,
      matchedConnector: cfg.connector.test(connector),
      matchedBody: cfg.allowedBody.test(body),
      blockedKey,
    };

    console.debug("[CatDesk approval] inspected card:", result);

    return result.matchedConnector && result.matchedBody && !result.blockedKey;
  }

  function findAllowButton(card) {
    const buttonArea = card.querySelector('[data-testid="tool-action-buttons"]') || card;

    return [...buttonArea.querySelectorAll("button")]
      .filter(isVisible)
      .find((btn) => cfg.allowButtonText.test(norm(btn.textContent)));
  }

  function updateFloatingStatus(extraText = "") {
    if (!ui) return;

    const armed = cfg.armed;
    const dry = cfg.dryRun;

    ui.dot.style.background = armed ? "#22c55e" : "#ef4444";
    ui.label.textContent = armed ? "CatDesk armed" : "CatDesk disarmed";
    ui.mode.textContent = dry ? "dry-run" : "live";
    ui.extra.textContent = extraText;

    ui.root.title = `CatDesk approval: ${armed ? "armed" : "disarmed"}, ${dry ? "dry-run" : "live"}`;
  }

  function createFloatingStatus() {
    const host = document.createElement("div");
    host.id = "catdesk-approval-status-host";

    const shadow = host.attachShadow({ mode: "open" });

    const style = document.createElement("style");
    style.textContent = `
      :host {
        all: initial;
      }

      button {
        position: fixed;
        right: 18px;
        bottom: 18px;
        z-index: 2147483647;
        display: flex;
        align-items: center;
        gap: 8px;
        border: 1px solid rgba(255, 255, 255, 0.18);
        border-radius: 999px;
        padding: 8px 11px;
        background: rgba(20, 20, 20, 0.88);
        color: white;
        font: 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
        cursor: pointer;
        user-select: none;
        backdrop-filter: blur(8px);
      }

      button:hover {
        background: rgba(32, 32, 32, 0.94);
      }

      .dot {
        width: 10px;
        height: 10px;
        border-radius: 999px;
        flex: 0 0 auto;
      }

      .text {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
      }

      .label {
        font-weight: 650;
      }

      .sub {
        display: flex;
        gap: 6px;
        opacity: 0.75;
      }

      .extra:empty {
        display: none;
      }
    `;

    const button = document.createElement("button");
    button.type = "button";
    button.setAttribute("aria-label", "Toggle CatDesk approval auto-allow");

    const dot = document.createElement("span");
    dot.className = "dot";

    const text = document.createElement("span");
    text.className = "text";

    const label = document.createElement("span");
    label.className = "label";

    const sub = document.createElement("span");
    sub.className = "sub";

    const mode = document.createElement("span");
    mode.className = "mode";

    const extra = document.createElement("span");
    extra.className = "extra";

    sub.append(mode, extra);
    text.append(label, sub);
    button.append(dot, text);
    shadow.append(style, button);

    button.addEventListener("click", () => {
      toggleArmed();
    });

    document.documentElement.append(host);

    ui = { root: button, dot, label, mode, extra };
    updateFloatingStatus();
  }

  function flashStatus(message) {
    updateFloatingStatus(message);
    setTimeout(() => updateFloatingStatus(), 1600);
  }

  function toggleArmed() {
    cfg.armed = !cfg.armed;
    console.info(`[CatDesk approval] ${cfg.armed ? "armed" : "disarmed"}; dryRun=${cfg.dryRun}`);
    updateFloatingStatus();
    scan();
  }

  function scan() {
    if (!cfg.armed) return;
    if (Date.now() - lastClickAt < cfg.cooldownMs) return;

    const cards = [...document.querySelectorAll('[data-testid="tool-approval-card"]')]
      .filter(isVisible);

    for (const card of cards) {
      if (!isAllowedCard(card)) continue;

      const allowButton = findAllowButton(card);
      if (!allowButton) continue;

      console.info("[CatDesk approval] matched card:", {
        title: getTitle(card),
        sharedKeys: getSharedDataKeys(card),
        dryRun: cfg.dryRun,
      });

      flashStatus(cfg.dryRun ? "matched" : "clicked");

      if (!cfg.dryRun) {
        lastClickAt = Date.now();
        allowButton.click();
      }

      return;
    }
  }

  createFloatingStatus();

  new MutationObserver(scan).observe(document.documentElement, {
    childList: true,
    subtree: true,
    characterData: true,
  });

  setInterval(scan, 1000);

  window.addEventListener("keydown", (ev) => {
    if (ev.ctrlKey && ev.altKey && ev.key.toLowerCase() === "y") {
      toggleArmed();
    }
  });

  console.info("[CatDesk approval] loaded. Click the floating pill or press Ctrl+Alt+Y to toggle.");
})();

The switch button will be at the bottom right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions