Reddit Free Awards

Give away free awards on Reddit

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 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 });
})();