Reddit Free Awards

Give away free awards on Reddit

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Reddit Free Awards
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Give away free awards on Reddit
// @author       nrmu9
// @match        https://www.reddit.com/*
// @license      CC-BY-NC-SA-4.0
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const AWARD_ICON = `<svg style="transform: translateY(1px)" fill="currentColor" width="16" height="16" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M28 8.75h-0.211c0.548-0.833 0.874-1.854 0.874-2.952 0-0.448-0.054-0.884-0.157-1.3l0.008 0.037c-0.487-1.895-2.008-3.337-3.912-3.702l-0.031-0.005c-0.234-0.035-0.505-0.055-0.78-0.055-2.043 0-3.829 1.096-4.804 2.732l-0.014 0.026-2.973 4.279-2.974-4.279c-0.989-1.662-2.776-2.758-4.818-2.758-0.275 0-0.545 0.020-0.81 0.058l0.030-0.004c-1.935 0.37-3.455 1.812-3.934 3.672l-0.008 0.035c-0.095 0.379-0.149 0.815-0.149 1.263 0 1.097 0.326 2.119 0.886 2.972l-0.013-0.021h-0.212c-1.794 0.002-3.248 1.456-3.25 3.25v3c0.002 1.343 0.817 2.495 1.979 2.99l0.021 0.008v10.002c0.002 1.794 1.456 3.248 3.25 3.25h20c1.794-0.001 3.249-1.456 3.25-3.25v-10.002c1.183-0.503 1.998-1.656 2-2.998v-3c-0.002-1.794-1.456-3.248-3.25-3.25h-0zM28.75 12v3c-0.006 0.412-0.338 0.744-0.749 0.75h-10.751v-4.5h10.75c0.412 0.006 0.744 0.338 0.75 0.749v0.001zM21.027 4.957c0.544-1.009 1.593-1.683 2.8-1.683 0.104 0 0.207 0.005 0.309 0.015l-0.013-0.001c0.963 0.195 1.718 0.915 1.963 1.842l0.004 0.018c0.021 0.149 0.033 0.322 0.033 0.497 0 1.28-0.635 2.412-1.608 3.097l-0.012 0.008h-6.112zM5.911 5.147c0.248-0.944 1.002-1.664 1.949-1.857l0.016-0.003c0.092-0.010 0.199-0.015 0.307-0.015 1.204 0 2.251 0.675 2.783 1.667l0.008 0.017 2.636 3.793h-6.113c-0.984-0.692-1.619-1.823-1.619-3.101 0-0.177 0.012-0.351 0.036-0.521l-0.002 0.020zM3.25 12c0.006-0.412 0.338-0.744 0.749-0.75h10.751v4.5h-10.75c-0.412-0.006-0.744-0.338-0.75-0.749v-0.001zM5.25 28v-9.75h9.5v10.5h-8.75c-0.412-0.006-0.744-0.338-0.75-0.749v-0.001zM26.75 28c-0.006 0.412-0.338 0.744-0.749 0.75h-8.751v-10.5h9.5z"></path></svg>`;

  const style = document.createElement("style");
  style.textContent = `
        .free-award-btn {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            padding: 0 0.5rem;
            height: 32px;
            border-radius: 9999px;
            cursor: pointer;
            color: var(--color-neutral-content);
            font-family: var(--font-sans);
            font-size: 12px;
            font-weight: 600;
            background: transparent;
            border: none;
        }
        .free-award-btn:hover {
            background-color: var(--color-neutral-background-hover);
        }
        .free-award-btn svg {
            width: 16px;
            height: 16px;
        }
        .award-modal-overlay {
            opacity: 0;
            transition: opacity 0.2s ease-in-out;
        }
        .award-modal-overlay.visible {
            opacity: 1;
        }
    `;
  document.head.appendChild(style);

  function createAwardButton(onClick, isComment = false) {
    const btn = document.createElement("button");
    const baseClasses =
      "button border-md flex flex-row justify-center items-center h-xl font-semibold relative text-12 inline-flex items-center px-sm";
    const variantClass = isComment ? "button-plain-weak" : "button-secondary";

    btn.className = `${baseClasses} ${variantClass}`;
    btn.style.cssText =
      "height: var(--size-button-sm-h); font: var(--font-button-sm);";

    btn.innerHTML = `
            <span class="flex items-center">
                <span class="flex text-16 me-[var(--rem6)]">${AWARD_ICON}</span>
                <span>Free Award</span>
            </span>
        `;

    btn.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      onClick();
    };

    return btn;
  }

  function openAwardSelection(thingId, isComment) {
    const script = document.createElement("script");
    script.textContent = `
        (async () => {
            const getToken = () => {
                const c = document.cookie.match(/csrf_token=([^;]+)/);
                if (c) return c[1];
                const a = document.querySelector('shreddit-app');
                return a?.csrfToken || a?.getAttribute('spp') || a?.getAttribute('csrf-token');
            };

            const showToast = async (message, isError = false) => {
                const findToaster = () => {
                    const alertController = document.querySelector('alert-controller');
                    return alertController?.shadowRoot?.querySelector('toaster-lite');
                };

                let toaster = findToaster();
                if (!toaster) {
                    for (let i = 0; i < 50; i++) {
                        await new Promise(r => setTimeout(r, 100));
                        toaster = findToaster();
                        if (toaster) break;
                    }
                }
                
                if (!toaster) {
                    alert(message);
                    return;
                }
                
                toaster.dispatchEvent(new CustomEvent('show-toast', {
                    bubbles: true,
                    composed: true,
                    detail: {
                        message,
                        level: 3,
                        namedContent: {}
                    }
                }));
            };
            
            const ctx = { id: "${thingId}", type: "${
      isComment ? "Comment" : "Post"
    }" };
            const token = getToken();
            
            if (!token) return showToast('Error: Could not find CSRF token.', true);
            
            let author = null;
            const selector = "${isComment}" === "true" ? 'shreddit-comment[thingid="${thingId}"]' : 'shreddit-post';
            const el = document.querySelector(selector);
            if (el?.getAttribute('author')) author = el.getAttribute('author');
            
            const titleText = author ? \`Award \${author}'s \${ctx.type}\` : \`Award \${ctx.type}\`;
            
            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' }
            ];
            
            const container = document.createElement('div');
            container.className = 'award-modal-overlay';
            Object.assign(container.style, {
                position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
                backgroundColor: 'rgba(0,0,0,0.85)', zIndex: '999999', display: 'flex',
                alignItems: 'center', justifyContent: 'center',
                fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif'
            });
            
            const dialog = document.createElement('div');
            Object.assign(dialog.style, {
                backgroundColor: '#1a1a1b', color: 'white', padding: '32px 24px',
                borderRadius: '24px', border: '1px solid #343536', textAlign: 'center',
                boxShadow: '0 12px 40px rgba(0,0,0,0.8)', width: '360px',
                boxSizing: 'border-box', maxHeight: '90vh', overflowY: 'auto'
            });
            
            dialog.innerHTML = \`<h2 style="margin:0 0 8px 0;font-size:22px;font-weight:700;color:white;line-height:1.2;letter-spacing:-0.5px">\${titleText}</h2><p style="margin:0 0 24px 0;font-size:14px;color:#818384;line-height:1.4">Select a free award:</p>\`;
            
            const btnList = document.createElement('div');
            Object.assign(btnList.style, { display: 'flex', flexDirection: 'column', gap: '12px' });
            
            awards.forEach(award => {
                const btn = document.createElement('button');
                Object.assign(btn.style, {
                    height: '56px', display: 'flex', alignItems: 'center', justifyContent: 'center',
                    position: 'relative', cursor: 'pointer', backgroundColor: '#d7dadc',
                    border: 'none', borderRadius: '28px', fontSize: '18px', fontWeight: '700',
                    color: '#1a1a1b', width: '100%', margin: '0', padding: '0 50px',
                    boxSizing: 'border-box', transition: 'transform 0.1s'
                });
                
                btn.innerHTML = \`<img src="\${award.img}" style="height:32px;width:32px;position:absolute;left:20px;top:50%;transform:translateY(-50%);flex-shrink:0"><span style="line-height:1;display:block;width:100%;text-align:center">\${award.name}</span>\`;
                
                btn.onclick = async () => {
                    btn.disabled = true;
                    btn.querySelector('span').innerText = 'Sending...';
                    try {
                        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: ctx.id,
                                        awardId: award.id,
                                        isAnonymous: false
                                    }
                                },
                                csrf_token: token
                            })
                        });
                        
                        const j = await res.json();
                        if (j.data?.createAwardOrder?.ok) {
                            showToast('Award Sent!');
                            close();
                            setTimeout(() => location.reload(), 2000);
                        } else {
                            let errorMsg = j.data?.createAwardOrder?.errors?.[0]?.message || j.errors?.[0]?.message || 'Unknown error';
                            if (errorMsg === 'Error creating award order') {
                                errorMsg += ' (You might be rate limited)';
                            }
                            showToast('Failed: ' + errorMsg, true);
                            btn.disabled = false;
                            btn.querySelector('span').innerText = award.name;
                        }
                    } catch (e) {
                        showToast('Network Error', true);
                        btn.disabled = false;
                        btn.querySelector('span').innerText = award.name;
                    }
                };
                btnList.appendChild(btn);
            });
            
            dialog.appendChild(btnList);
            
            const cancelBtn = document.createElement('button');
            cancelBtn.innerText = 'Cancel';
            Object.assign(cancelBtn.style, {
                background: 'transparent', color: '#818384', border: 'none',
                marginTop: '20px', cursor: 'pointer', fontSize: '15px',
                fontWeight: '600', width: '100%', padding: '8px'
            });
            
            const close = () => {
                container.classList.remove('visible');
                setTimeout(() => {
                    if (container.parentNode) document.body.removeChild(container);
                }, 200);
            };
            
            cancelBtn.onclick = close;
            container.onclick = (e) => {
                if (e.target === container) close();
            };
            
            dialog.appendChild(cancelBtn);
            container.appendChild(dialog);
            document.body.appendChild(container);
            
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    container.classList.add('visible');
                });
            });
        })();
        `;
    document.body.appendChild(script);
    document.body.removeChild(script);
  }

  function processPost(post) {
    if (post.dataset.freeAwardProcessed) return;

    const postId = post.id || post.getAttribute("postId");

    const existingShareBtn = post.querySelector(
      'shreddit-post-share-button[slot="share-button"]'
    );

    if (existingShareBtn && postId) {
      const btn = createAwardButton(
        () => openAwardSelection(postId, false),
        false
      );
      btn.setAttribute("slot", "share-button");
      post.insertBefore(btn, existingShareBtn);
      post.dataset.freeAwardProcessed = "true";
      return;
    }

    if (post.shadowRoot && postId) {
      const container = post.shadowRoot.querySelector(
        ".shreddit-post-container"
      );
      if (container) {
        const awardBtn = container.querySelector("award-button");
        if (awardBtn) {
          const btn = createAwardButton(
            () => openAwardSelection(postId, false),
            false
          );
          awardBtn.parentNode.insertBefore(btn, awardBtn.nextSibling);
          post.dataset.freeAwardProcessed = "true";
          return;
        }
      }
    }
  }

  function processComment(comment) {
    if (comment.dataset.freeAwardProcessed) return;

    const actionRow = comment.querySelector("shreddit-comment-action-row");
    if (!actionRow) return;

    const shareBtn = actionRow.querySelector('[slot="comment-share"]');
    const thingId = comment.getAttribute("thingid");

    if (shareBtn && thingId) {
      const btn = createAwardButton(
        () => openAwardSelection(thingId, true),
        true
      );
      btn.setAttribute("slot", "comment-award");

      shareBtn.parentNode.insertBefore(btn, shareBtn);
      comment.dataset.freeAwardProcessed = "true";
    }
  }

  function run() {
    document.querySelectorAll("shreddit-post").forEach(processPost);
    document.querySelectorAll("shreddit-comment").forEach(processComment);
  }

  run();

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