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