Kour.io Safe Verify UI (Yellow Badge)

Safe verification UI for developers/admins — shows yellow badge and replaces user's blue badge with yellow one (best-effort).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Kour.io Safe Verify UI (Yellow Badge)
// @namespace    LC
// @version      2.2
// @description  Safe verification UI for developers/admins — shows yellow badge and replaces user's blue badge with yellow one (best-effort).
// @author       LC
// @license      CC BY-ND 4.0
// @match        https://kour.io/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // ---- CONFIG ----
  const config = {
    useServerEndpoint: true, // recommended: server does the verification write
    verifyEndpoint: '/api/admin/verify-user', // server endpoint that accepts POST { uid } and checks ID token
    requiredClaim: 'canVerify', // if doing client-side writes, require this claim or admin
    // Put the data URL of your yellow badge image here (replace placeholder)
    yellowBadgeDataUrl: '__YELLOW_BADGE_DATA_URL__'
  };

  // --- Styles (dark panel) ---
  GM_addStyle(`
    #lcVerifyContainer { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; position: fixed; top: 20px; left: 20px; z-index: 9999; background: #1a1a1a; box-shadow: 0 2px 15px rgba(0,0,0,.3); border-radius: 8px; padding: 15px; width: 320px; border: 1px solid #333; }
    #lcVerifyHeader{ color:#00c853; margin-bottom:12px; font-size:16px; font-weight:600; display:flex; justify-content:space-between; align-items:center;}
    #lcVerifyBtn{ background:linear-gradient(135deg,#008000,#00b04a); color:#fff; border:none; padding:8px 15px; border-radius:6px; font-size:14px; cursor:pointer; width:100%; margin-bottom:8px;}
    #lcVerifyBtn:hover{ opacity:.95; transform:translateY(-1px); }
    #lcHideBtn{ background:#333; color:#00c853; border:none; padding:5px 10px; border-radius:5px; font-size:12px; cursor:pointer; }
    #lcVerifyStatus{ font-size:13px; color:#00c853; margin-top:8px; text-align:center; display:flex; align-items:center; justify-content:center; gap:8px; }
    #lcVerifyStatus img { height: 22px; width: 22px; display:inline-block; vertical-align:middle; }
    .hidden{ display:none; }
  `);

  // --- panel HTML ---
  const panelHTML = `
    <div id="lcVerifyContainer" aria-live="polite">
      <div id="lcVerifyHeader">
        <span>LC Verification</span>
        <button id="lcHideBtn" aria-pressed="false">Hide</button>
      </div>
      <button id="lcVerifyBtn">Verify Account</button>
      <div id="lcVerifyStatus">Ready</div>
    </div>
  `;
  document.body.insertAdjacentHTML('beforeend', panelHTML);

  const container = document.getElementById('lcVerifyContainer');
  const verifyBtn = document.getElementById('lcVerifyBtn');
  const statusText = document.getElementById('lcVerifyStatus');
  const hideBtn = document.getElementById('lcHideBtn');

  hideBtn.addEventListener('click', () => {
    container.classList.toggle('hidden');
    const hidden = container.classList.contains('hidden');
    hideBtn.textContent = hidden ? 'Show' : 'Hide';
    hideBtn.setAttribute('aria-pressed', String(hidden));
  });

  function setStatus(text, ok = true, showBadge = false) {
    // showBadge -> display the yellow badge icon beside status
    statusText.textContent = text;
    statusText.style.color = ok ? '#00c853' : '#ff7043';
    if (showBadge && config.yellowBadgeDataUrl) {
      const img = document.createElement('img');
      img.src = config.yellowBadgeDataUrl;
      img.alt = 'Verified';
      // clear then append
      statusText.innerHTML = '';
      statusText.appendChild(img);
      const span = document.createElement('span');
      span.textContent = ' ' + text;
      statusText.appendChild(span);
    }
  }

  // attempt to replace blue verified badge next to the current user's username
  async function replaceBlueBadgeForCurrentUser(yellowDataUrl, user) {
    if (!yellowDataUrl || !user) return false;
    try {
      const uid = user.uid;
      const displayName = user.displayName || '';
      const email = user.email || '';

      // Strategy:
      // 1) Find elements with data-uid, data-user-id, or data-user attributes matching uid
      // 2) Find elements that contain the user's displayName or email and search for a nearby "verified" badge
      // 3) Replace img/src or svg fill where badge appears with the yellow image (best-effort)

      // helper to replace node (img or svg) with an <img> using data url
      function replaceNodeWithImg(node) {
        if (!node) return false;
        const img = document.createElement('img');
        img.src = yellowDataUrl;
        img.alt = 'Verified';
        img.style.height = (node.clientHeight || 18) + 'px';
        img.style.width = (node.clientWidth || 18) + 'px';
        img.className = (node.className || '') + ' replaced-yellow-verified';
        node.replaceWith(img);
        return true;
      }

      // 1) query by data attributes
      const selectors = [
        `[data-uid="${uid}"]`,
        `[data-user-id="${uid}"]`,
        `[data-user="${uid}"]`,
        `[data-username]`
      ];
      for (const sel of selectors) {
        const els = document.querySelectorAll(sel);
        for (const el of els) {
          // search inside for an <img> or svg that looks like a verified badge
          const candidate = el.querySelector('img[alt*="verify"], img[alt*="Verify"], img[src*="verified"], svg, .verified, .badge, .icon-verified');
          if (candidate) {
            replaceNodeWithImg(candidate);
            return true;
          }
        }
      }

      // 2) Search for username/displayName text nodes, then find sibling badge elements
      if (displayName || email) {
        // collect all textual nodes that match
        const textCandidates = Array.from(document.querySelectorAll('body *')).filter(node => {
          // limit to small elements to avoid huge containers
          const txt = (node.textContent || '').trim();
          if (!txt) return false;
          if (displayName && txt.includes(displayName)) return true;
          if (email && txt.includes(email)) return true;
          return false;
        }).slice(0, 40); // limit

        for (const node of textCandidates) {
          // search nearby for badge
          // check siblings, children, parent
          const nearby = [
            ...Array.from(node.parentElement ? node.parentElement.querySelectorAll('img, svg, .verified, .badge, .icon-verified') : []),
            ...Array.from(node.querySelectorAll ? node.querySelectorAll('img, svg, .verified, .badge, .icon-verified') : [])
          ];
          for (const cand of nearby) {
            // Heuristic: if cand has blue fill or src with 'blue' or 'twitter' or big blue color, replace.
            if (cand.tagName === 'IMG') {
              const src = (cand.getAttribute('src') || '').toLowerCase();
              if (src.includes('blue') || src.includes('verified') || src.includes('twitter') || cand.width > 0) {
                replaceNodeWithImg(cand);
                return true;
              } else {
                // still try replacing if it's likely a small icon
                if ((cand.clientWidth && cand.clientWidth <= 48) || (cand.clientHeight && cand.clientHeight <= 48)) {
                  replaceNodeWithImg(cand);
                  return true;
                }
              }
            } else if (cand.tagName === 'svg') {
              // attempt to replace svg with image
              replaceNodeWithImg(cand);
              return true;
            } else {
              // class-based match
              replaceNodeWithImg(cand);
              return true;
            }
          }
        }
      }

      // 3) global fallback: replace any visible blue circular badge icons (be conservative)
      const imgs = Array.from(document.querySelectorAll('img'));
      for (const im of imgs) {
        const src = (im.getAttribute('src') || '').toLowerCase();
        // heuristics for blue badge images: 'verify', 'check', 'twitter', and blue-ish file names
        if (src.includes('verify') || src.includes('verified') || src.includes('twitter') || src.includes('check')) {
          // check computed color presence via naturalWidth/Height small
          if ((im.clientWidth && im.clientWidth <= 64) || (im.clientHeight && im.clientHeight <= 64)) {
            replaceNodeWithImg(im);
            return true;
          }
        }
      }

      return false;
    } catch (e) {
      console.warn('replaceBlueBadgeForCurrentUser error', e);
      return false;
    }
  }

  verifyBtn.addEventListener('click', async () => {
    setStatus('Checking firebase...');
    if (typeof window.firebase === 'undefined' || !window.firebase.auth) {
      setStatus('firebase not found on page', false);
      return;
    }
    const auth = firebase.auth();
    const user = auth.currentUser;
    if (!user) {
      setStatus('No authenticated user. Please sign in.', false);
      return;
    }

    if (!confirm(`Verify account for ${user.email || user.uid} (uid: ${user.uid})? This action is logged.`)) {
      setStatus('Cancelled by user.', false);
      return;
    }

    setStatus('Obtaining token & claims...');
    try {
      const idTokenResult = await user.getIdTokenResult(true);
      const claims = idTokenResult.claims || {};

      if (config.useServerEndpoint) {
        setStatus('Sending verification request to server...');
        const resp = await fetch(config.verifyEndpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + await user.getIdToken()
          },
          body: JSON.stringify({ uid: user.uid }),
          credentials: 'same-origin'
        });

        if (!resp.ok) {
          const txt = await resp.text().catch(() => resp.statusText);
          setStatus('Server error: ' + txt, false);
          return;
        }
        const json = await resp.json().catch(() => null);
        if (json && json.success) {
          setStatus('Verified', true, true);
          // replace blue badge next to user's name (best-effort)
          await replaceBlueBadgeForCurrentUser(config.yellowBadgeDataUrl, user);
        } else {
          setStatus('Server responded but did not mark verified', false);
        }
        return;
      }

      // CLIENT-SIDE write path (not recommended)
      if (!claims[config.requiredClaim] && !claims.admin) {
        setStatus('Missing required custom claim for client update.', false);
        return;
      }

      setStatus('Updating database (client)...');
      await firebase.database().ref('users/' + user.uid + '/verified').set(true);
      setStatus('Verified', true, true);
      await replaceBlueBadgeForCurrentUser(config.yellowBadgeDataUrl, user);

    } catch (err) {
      console.error(err);
      setStatus('Error: ' + (err && err.message ? err.message : String(err)), false);
    }
  });

  // Attempt to replace badge on page load if we find the user is already marked verified locally
  (async function initReplaceIfAlreadyVerified(){
    if (typeof window.firebase === 'undefined' || !window.firebase.auth) return;
    try {
      const auth = firebase.auth();
      // Wait for auth state
      auth.onAuthStateChanged(async (user) => {
        if (!user) return;
        try {
          // Check local DB or token claims for verification flag
          const idTokenResult = await user.getIdTokenResult();
          const claims = idTokenResult.claims || {};
          const isVerifiedClaim = claims.verified || claims.isVerified || false;

          // Also check realtime DB value (best-effort, do not force offline/online)
          let dbVerified = false;
          try {
            const snap = await firebase.database().ref('users/' + user.uid + '/verified').once('value');
            dbVerified = !!snap.val();
          } catch(e) {
            // ignore DB read failures
          }

          if (isVerifiedClaim || dbVerified) {
            setStatus('Verified', true, true);
            await replaceBlueBadgeForCurrentUser(config.yellowBadgeDataUrl, user);
          }
        } catch (e) {
          // ignore
        }
      });
    } catch(e) {}
  })();

})();