Undeleddit

The solution to Reddit's biggest problem.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Undeleddit
// @namespace    http://tampermonkey.net/
// @version      1.2.2
// @description  The solution to Reddit's biggest problem.
// @author       JGP
// @icon         data:image/x-icon;base64,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
// @match        https://old.reddit.com/r/*/comments/*
// @match        https://old.reddit.com/prefs/
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      api.pullpush.io
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const MAX_PAGE_SIZE = 100;
    const STEALTH_MODE_KEY = 'pullpush_stealth_mode_enabled';
    let DEFAULT_AUTHOR_COLOR = '#369';

    function getStealthMode() {
        return GM_getValue(STEALTH_MODE_KEY, true);
    }

    function setStealthMode(enabled) {
        GM_setValue(STEALTH_MODE_KEY, enabled);
    }

    if (window.location.pathname === '/prefs/') {
        const targetRow = Array.from(document.querySelectorAll('tr')).find(row => {
            const header = row.querySelector('th');
            return header && header.textContent.trim().toLowerCase() === 'comment options';
        });

        if (targetRow) {
            const containerTd = targetRow.querySelector('.prefright');
            if (containerTd) {
                const preferenceParagraph = document.createElement('p');
                preferenceParagraph.style.marginTop = '15px';
                preferenceParagraph.style.paddingTop = '10px';
                preferenceParagraph.style.borderTop = '1px dashed #ccc';

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.name = 'undeleddit_stealth_mode';
                checkbox.id = 'undeleddit_stealth_mode';
                checkbox.checked = getStealthMode();
                checkbox.style.marginRight = '5px';

                const label = document.createElement('label');
                label.htmlFor = 'undeleddit_stealth_mode';
                label.textContent = 'hide undeleddit marker';

                const details = document.createElement('span');
                details.className = 'details';
                details.innerHTML = ' (blends undeleted comments into the sub theme. when unchecked, comments use custom colors and an obvious marker)';

                preferenceParagraph.appendChild(checkbox);
                preferenceParagraph.appendChild(label);
                preferenceParagraph.appendChild(details);
                containerTd.appendChild(preferenceParagraph);

                checkbox.addEventListener('change', (e) => {
                    setStealthMode(e.target.checked);
                });
            }
        }
        return;
    }

    function getPostId() {
        const match = window.location.pathname.match(/\/comments\/(\w+)\//);
        return match ? match[1] : null;
    }

    function timeSince(createdUtc) {
        const seconds = Math.floor((new Date() - (createdUtc * 1000)) / 1000);
        let interval = seconds / 31536000;
        if (interval > 1) return Math.floor(interval) + " years ago";
        interval = seconds / 2592000;
        if (interval > 1) return Math.floor(interval) + " months ago";
        interval = seconds / 86400;
        if (interval > 1) return Math.floor(interval) + " days ago";
        interval = seconds / 3600;
        if (interval > 1) return Math.floor(interval) + " hours ago";
        interval = seconds / 60;
        if (interval > 1) return Math.floor(interval) + " minutes ago";
        return Math.floor(seconds) + " seconds ago";
    }

    function decodeHtml(html) {
        const txt = document.createElement("textarea");
        txt.innerHTML = html;
        return txt.value;
    }

    function getSubredditDefaultAuthorColor() {
        const existingAuthor = document.querySelector('.comment:not(.pullpush-restored) .author:not([style])');
        if (existingAuthor) {
            const computedStyle = window.getComputedStyle(existingAuthor);
            const color = computedStyle.color;
            if (color) return color;
        }
        return DEFAULT_AUTHOR_COLOR;
    }

    async function fetchPage(postId, after = 0) {
        const fields = 'author,body,body_html,created_utc,id,score,permalink,link_id,parent_id,retrieved_on,retrieved_utc,subreddit';
        let apiUrl = `https://api.pullpush.io/reddit/comment/search/?fields=${fields}&metadata=true&size=${MAX_PAGE_SIZE}&sort=asc&link_id=${postId}`;
        if (after > 0) apiUrl += `&after=${after}`;

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: apiUrl,
                onload: function(response) {
                    if (response.status !== 200) return reject(`API request failed: ${response.status}`);
                    try {
                        const data = JSON.parse(response.responseText);
                        resolve(data.data);
                    } catch (e) {
                        reject("Failed to parse response.");
                    }
                },
                onerror: function(response) {
                    reject(`Network error.`);
                }
            });
        });
    }

    async function fetchAllDeletedComments(postId) {
        let allComments = [];
        let afterTimestamp = 0;
        let pageCount = 0;

        while (true) {
            pageCount++;
            try {
                const comments = await fetchPage(postId, afterTimestamp);
                if (!comments || comments.length === 0) break;

                const newComments = comments.filter(c => !allComments.some(ac => ac.id === c.id));
                allComments.push(...newComments);
                afterTimestamp = comments[comments.length - 1].created_utc;

                if (comments.length < MAX_PAGE_SIZE) break;
                await new Promise(r => setTimeout(r, 1000));
            } catch (error) {
                break;
            }
        }
        return allComments;
    }

    function findPlaceholder(commentId) {
        const fullCommentId = `t1_${commentId}`;
        const thingId = `thing_${fullCommentId}`;
        let placeholder = document.querySelector(`.comment[data-fullname="${fullCommentId}"]`);

        if (!placeholder) {
            placeholder = document.getElementById(thingId);
            if (placeholder && !placeholder.classList.contains('comment')) placeholder = null;
        }
        if (!placeholder) {
            placeholder = document.querySelector(`.comment[data-permalink$="/${commentId}/"]`);
        }
        return placeholder;
    }

    function updatePlaceholder(placeholder, comment) {
        const commentId = comment.id;
        const fullCommentId = `t1_${commentId}`;
        const author = comment.author;
        const score = comment.score || '0';
        const createdUtc = comment.created_utc;
        const isStealth = getStealthMode();

        const dateObj = new Date(createdUtc * 1000);
        const timestampLocal = dateObj.toLocaleString();
        const timestampISO = dateObj.toISOString();
        const timeSinceString = timeSince(createdUtc);

        const stealthColor = DEFAULT_AUTHOR_COLOR;
        const highlightColor = '#E9967A';
        const authorColor = isStealth ? stealthColor : highlightColor;
        const markerDisplay = isStealth ? 'none' : 'inline';

        const tagline = placeholder.querySelector('.tagline');
        if (!tagline) return;

        const expander = tagline.querySelector('.expand');
        tagline.innerHTML = '';
        if (expander) tagline.appendChild(expander);

        tagline.innerHTML += `
            <span class="userattrs"></span>
            <a href="/user/${author}" class="author pullpush-restored-author" style="color: ${authorColor};">${author}</a>
            <span class="score unvoted" title="${score} points">${score} points</span>
            <time title="${timestampLocal}" datetime="${timestampISO}" class="live-timestamp">${timeSinceString}</time>
            <span class="pullpush-restored-marker" style="color: #008000; font-weight: bold; margin-left: 5px; display: ${markerDisplay};">[RESTORED VIA UNDELEDDIT]</span>
        `;

        const entry = placeholder.querySelector('.entry');
        if (entry) {
            let node = tagline.nextSibling;
            while (node) {
                const next = node.nextSibling;
                node.remove();
                node = next;
            }

            let commentBodyContent = comment.body_html ? decodeHtml(comment.body_html) : `<div class="md"><p>${comment.body}</p></div>`;

            const usertextForm = document.createElement('form');
            usertextForm.action = '#';
            usertextForm.className = 'usertext warn-on-unload pullpush-restored-form';
            usertextForm.setAttribute('onsubmit', 'return false;');
            usertextForm.id = `form-${commentId}`;
            usertextForm.innerHTML = `
                <input type="hidden" name="thing_id" value="${fullCommentId}">
                <div class="usertext-body may-blank-within md-container ">
                    ${commentBodyContent}
                </div>
            `;
            entry.appendChild(usertextForm);

            const buttonsList = document.createElement('ul');
            buttonsList.className = 'flat-list buttons';
            buttonsList.innerHTML = `
                <li class="first"><a href="${comment.permalink}" data-event-action="permalink" class="bylink" rel="nofollow">permalink</a></li>
                <li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li>
                <li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li>
                <li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li>
            `;
            entry.appendChild(buttonsList);

            placeholder.classList.remove('deleted', 'removed', 'collapsed');
            placeholder.classList.add('pullpush-restored');
        }
    }

    async function init() {
        const postId = getPostId();
        if (!postId) return;

        const commentArea = document.querySelector('.commentarea');
        if (!commentArea) return;

        DEFAULT_AUTHOR_COLOR = getSubredditDefaultAuthorColor();

        const loadingIndicator = document.createElement('div');
        loadingIndicator.id = 'pullpush-loading';
        loadingIndicator.style.cssText = 'padding: 10px; border-radius: 5px; margin-bottom: 10px;';
        loadingIndicator.innerHTML = '<p style="font-style: italic; color: #555; background: #ffffcc; border: 1px solid #e0e0e0;">[Undeleddit] Undeleting (may take a few seconds for large threads)...</p>';
        commentArea.prepend(loadingIndicator);

        const allPlaceholders = Array.from(document.querySelectorAll('.comment.deleted, .comment.removed')).filter(p => {
            const tagline = p.querySelector('.tagline');
            return tagline && (tagline.textContent.includes('[deleted]') || tagline.textContent.includes('[removed]'));
        });

        const placeholderIds = new Set(allPlaceholders.map(p => {
            const fullname = p.getAttribute('data-fullname');
            if (fullname && fullname.startsWith('t1_')) return fullname.substring(3);
            const id = p.id;
            if (id && id.startsWith('thing_t1_')) return id.substring(8);
            return null;
        }).filter(id => id !== null));

        try {
            const allArchivedComments = await fetchAllDeletedComments(postId);
            const archivedIds = new Set(allArchivedComments.map(c => c.id));
            let restoredCount = 0;

            archivedIds.forEach(commentId => {
                const placeholder = findPlaceholder(commentId);
                const comment = allArchivedComments.find(c => c.id === commentId);

                if (placeholder && comment) {
                    const tagline = placeholder.querySelector('.tagline');
                    const isPlaceholder = tagline && (tagline.textContent.includes('[deleted]') || tagline.textContent.includes('[removed]'));
                    const isArchivedContent = comment.author && comment.author !== '[deleted]' &&
                                              comment.body && comment.body !== '[deleted]' && comment.body !== '[removed]';

                    if (isPlaceholder && isArchivedContent) {
                        updatePlaceholder(placeholder, comment);
                        placeholderIds.delete(commentId);
                        restoredCount++;
                    }
                }
            });

            const totalDeletedOnPage = allPlaceholders.length;
            const loadingIndicatorEl = document.getElementById('pullpush-loading');
            if (loadingIndicatorEl) {
                loadingIndicatorEl.innerHTML = `<p style="font-weight: bold; color: #155724; background: #d4edda; border: 1px solid #c3e6cb;">[Undeleddit] Operation Undelete accomplished. Retrieved ${allArchivedComments.length} archived records, undeleted ${restoredCount} comments (of ${totalDeletedOnPage} total).</p>`;
            }
        } catch (error) {
            const loadingIndicatorEl = document.getElementById('pullpush-loading');
            if (loadingIndicatorEl) {
                loadingIndicatorEl.innerHTML = `<p style="font-weight: bold; color: #721c24; background: #f8d7da; border: 1px solid #f5c6fb;">[Undeleddit] Error during comment retrieval: ${error}. Cannot retrieve comments.</p>`;
            }
        }
    }

    init();
})();