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

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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