Reddit Free Awards – Posts & Comments (Queued + Toggle)

One-click free awards for posts & comments with floating toggle and rate-limit safe queue

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Reddit Free Awards – Posts & Comments (Queued + Toggle)
// @namespace    https://tampermonkey.net/reddit-free-awards-toggle-floating
// @author       dapro26
// @version      3.4.2
// @description  One-click free awards for posts & comments with floating toggle and rate-limit safe queue
// @match        https://www.reddit.com/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(() => {
  "use strict";

  /* ===================== CONFIG ===================== */

  const STORAGE_KEY = "free_awards_enabled";
  const AWARD_DELAY_MS = 2500; // ⏱️ delay between awards (rate-limit safe)

  const AWARDS = [
    { name: "Heartwarming", id: "award_free_heartwarming", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/Heartwarming_40.png" },
    { name: "Popcorn", id: "award_free_popcorn_2", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/Popcorn_40.png" },
    { name: "Bravo", id: "award_free_bravo", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/bravo_40.png" },
    { name: "Regret", id: "award_free_regret_2", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/regret_40.png" },
    { name: "Mindblown", id: "award_free_mindblown", img: "https://i.redd.it/snoovatar/snoo_assets/marketing/mindblown_40.png" },
  ];

  /* ===================== STATE ===================== */

  const isEnabled = () => localStorage.getItem(STORAGE_KEY) !== "false";
  const setEnabled = (v) => localStorage.setItem(STORAGE_KEY, String(v));

  /* ===================== AWARD QUEUE ===================== */

  const awardQueue = [];
  let isProcessing = false;

  const processQueue = async () => {
    if (isProcessing || awardQueue.length === 0) return;
    isProcessing = true;

    const { thingId, awardId, btn, img } = awardQueue.shift();

    try {
      await sendAward(thingId, awardId);
      btn.textContent = "✓";
      btn.style.background = "#4BB543";
      btn.style.color = "white";
    } catch {
      btn.textContent = "✕";
      btn.style.background = "#EA0027";
      btn.style.color = "white";
      setTimeout(() => {
        btn.textContent = "";
        btn.appendChild(img);
        btn.style.background = "transparent";
        btn.style.color = "";
        btn.disabled = false;
      }, 2000);
    }

    setTimeout(() => {
      isProcessing = false;
      processQueue();
    }, AWARD_DELAY_MS);
  };

  /* ===================== HELPERS ===================== */

  const getCurrentUserId = () =>
    document.querySelector("[user-id]")?.getAttribute("user-id") || null;

  const getCsrfToken = () => {
    const c = document.cookie.match(/csrf_token=([^;]+)/);
    if (c) return c[1];
    const app = document.querySelector("shreddit-app");
    return app?.csrfToken || app?.getAttribute("csrf-token") || app?.getAttribute("spp");
  };

  const sendAward = async (thingId, awardId) => {
    const token = getCsrfToken();
    if (!token) throw new Error("No CSRF");

    const res = await fetch("https://www.reddit.com/svc/shreddit/graphql", {
      method: "POST",
      headers: { "Content-Type": "application/json", "X-Csrf-Token": token },
      body: JSON.stringify({
        operation: "CreateAwardOrder",
        variables: { input: { nonce: crypto.randomUUID(), thingId, awardId, isAnonymous: false } },
        csrf_token: token,
      }),
    });

    const json = await res.json();
    if (!json?.data?.createAwardOrder?.ok) throw new Error("Award failed");
  };

  /* ===================== UI ===================== */

  const createAwardButton = (award, thingId) => {
    const btn = document.createElement("button");
    btn.title = award.name;

    btn.style.cssText = `
      background: transparent;
      border: none;
      border-radius: 999px;
      cursor: pointer;
      width: 28px;
      height: 28px;
      display: flex;
      align-items: flex-end;
      justify-content: center;
      padding: 0;
    `;

    const img = document.createElement("img");
    img.src = award.img;
    img.style.cssText = "width:20px;height:20px;margin-bottom:2px;pointer-events:none;";
    btn.appendChild(img);

    btn.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      if (btn.disabled) return;

      btn.disabled = true;
      btn.style.opacity = "0.6";

      awardQueue.push({ thingId, awardId: award.id, btn, img });
      processQueue();
    };

    return btn;
  };

  /* ===================== INJECTION ===================== */

  const removeAllButtons = () =>
    document.querySelectorAll(".free-award-wrap").forEach(e => e.remove());

  const injectCommentButtons = () => {
    document.querySelectorAll("shreddit-comment-action-row").forEach(row => {
      if (!isEnabled() || row.querySelector(".free-award-wrap")) return;

      const comment = row.closest("shreddit-comment");
      if (!comment || comment.hasAttribute("is-author")) return;

      const thingId = row.getAttribute("comment-id");
      if (!thingId) return;

      const wrap = document.createElement("div");
      wrap.className = "free-award-wrap";
      wrap.setAttribute("slot", "comment-award");
      wrap.style.cssText = "display:inline-flex;gap:2px;margin-left:8px;";

      AWARDS.forEach(a => wrap.appendChild(createAwardButton(a, thingId)));

      const existing = row.querySelector('[slot="comment-award"]');
      existing ? existing.after(wrap) : row.appendChild(wrap);
    });
  };

const injectPostButtons = (uid) => {
  document.querySelectorAll("shreddit-post").forEach(post => {
    if (!isEnabled() || post.dataset.freeAwardInjected) return;
    if (post.getAttribute("author-id") === uid) return;

    const thingId = post.id || post.getAttribute("postid");
    if (!thingId) return;

    const wrap = document.createElement("div");
    wrap.className = "free-award-wrap";
    wrap.style.cssText = "display:inline-flex;gap:4px;margin-left:8px;";

    AWARDS.forEach(a => wrap.appendChild(createAwardButton(a, thingId)));

    let target =
      post.querySelector('[slot="post-award"]') ||
      post.querySelector("award-button");

    // 🔑 SHADOW DOM FALLBACK
    if (!target && post.shadowRoot) {
      target =
        post.shadowRoot.querySelector('[slot="post-award"]') ||
        post.shadowRoot.querySelector("award-button");
    }

    if (target) {
      target.after(wrap);
      post.dataset.freeAwardInjected = "true";
    }
  });
};

  /* ===================== FLOATING TOGGLE ===================== */

  const injectToggle = () => {
    if (document.querySelector("#free-award-toggle")) return;

    const btn = document.createElement("button");
    btn.id = "free-award-toggle";

    btn.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 999999;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 10px 16px;
      height: 36px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 700;
      border: none;
      cursor: pointer;
      box-shadow: 0 4px 12px rgba(0,0,0,0.25);
      color: white;
    `;

    const update = () => {
      btn.textContent = `Free Awards: ${isEnabled() ? "ON" : "OFF"}`;
      btn.style.background = isEnabled() ? "#FF4500" : "#555";
    };

    btn.onclick = () => {
      setEnabled(!isEnabled());
      removeAllButtons();
      update();
      run();
    };

    update();
    document.body.appendChild(btn);
  };

  /* ===================== INIT ===================== */

  const run = () => {
    const uid = getCurrentUserId();
    if (!uid) return;
    injectToggle();
    injectCommentButtons();
    injectPostButtons(uid);
  };

  run();
  new MutationObserver(run).observe(document.body, { childList: true, subtree: true });
})();