Mastodon status2html

Save status to a html file.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name        Mastodon status2html
// @namespace   https://blog.bgme.me
// @match       https://*/web/*
// @match       https://bgme.me/*
// @match       https://bgme.bid/*
// @match       https://c.bgme.bid/*
// @grant       none
// @run-at      document-end
// @version     1.0.2
// @author      bgme
// @description Save status to a html file.
// @supportURL  https://github.com/yingziwu/Greasemonkey/issues
// @license     AGPL-3.0-or-later
// ==/UserScript==


/* eslint-disable @typescript-eslint/explicit-member-accessibility */
class Status {
    token = JSON.parse(document.querySelector('#initial-state').text).meta.access_token;

    constructor(domain, statusID, sortbytime = false) {
        this.API = {
            'status': `https://${domain}/api/v1/statuses/${statusID}`,
            'context': `https://${domain}/api/v1/statuses/${statusID}/context`
        };
        this.sortbytime = sortbytime;
    }

    async init() {
        const status = await this.request(this.API.status);
        const context = await this.request(this.API.context);

        const statusList = [];
        const statusMap = new Map();
        const statusIndents = new Map();

        if (context.ancestors.length) {
            for (const obj of context.ancestors) {
                spush(obj)
            }
        }
        spush(status);
        if (context.descendants.length) {
            for (const obj of context.descendants) {
                spush(obj);
            }
        }
        if (this.sortbytime) {
            statusList.sort((a, b) => ((new Date(a.created_at)) - (new Date(b.created_at))));
        }
        this.statusList = statusList;

        statusList.forEach(obj => {
            let k = obj.id;
            statusIndents.set(k, getIndent(k));
        })
        this.statusIndents = statusIndents;

        function spush(obj) {
            statusList.push(obj);
            if (obj.in_reply_to_id) {
                statusMap.set(obj.id, obj.in_reply_to_id);
            }
        }
        function getIndent(id) {
            if (statusMap.get(id)) {
                return 1 + getIndent(statusMap.get(id))
            } else {
                return 0
            }
        }
    }

    async request(url) {
        console.log(`正在请求:${url}`);
        const resp = await fetch(url, {
            headers: {
                Authorization: `Bearer ${this.token}`,
            },
            method: 'GET',
        });
        return await resp.json();
    }

    html(anonymity_list = []) {
        const HTMLTemplate = `<html>
        <head>
            <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css" integrity="sha256-2+dssJtgusl/DZZZ8gF9ayAgRzcewXQsaP86E4Ul+ss=" crossorigin="anonymous">
            <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/jquery.fancybox.css" integrity="sha256-iK+zjGHeeTQux1laFiGc4EZWPacH5acc6CnZBGji1ns=" crossorigin="anonymous">
            <style>
                .ui.feed > .event > .content .user > img {
                    max-height: 1.5em;
                    padding-left: 0.2em;
                }
                .emojione {
                    max-height: 1.5em;
                }
                .ui.feed > .event > .content .meta {
                    padding-left: 0.5em;
                }
                .ui.feed > .event > .content .meta > button {
                    position: relative;
                    top: -1.1em;
                }
                .ui.feed > .event.hidden {
                    display: none;
                }
                body {
                    overflow-x: scroll;
                }
            </style>
        </header>
        <body>
            <main id="main">
                <div id="main-content" class="ui text container">
                    <div class="ui large feed" id="main-feed"></div>
                </div>
            </main>

            <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
            <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js" integrity="sha256-yibQd6vg4YwSTFUcgd+MwPALTUAVCKTjh4jMON4j+Gk=" crossorigin="anonymous"></script>
            <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/jquery.fancybox.pack.js" integrity="sha256-VRL0AMrD+7H9+7Apie0Jj4iir1puS6PYigObxCHqf/4=" crossorigin="anonymous"></script>    <script>
                $(document).ready(function() {
                    $('.image-reference').fancybox();
                    document.querySelectorAll('.ui.feed > .event > .content .meta > button.jump')
                        .forEach(button => {
                            button.addEventListener('click', function() {
                                    const pid = this.parentElement.parentElement.parentElement.getAttribute('pid');
                                    document.location.hash = pid;
                                }
                            );
                        }
                    );
                    document.querySelectorAll('.ui.feed > .event > .content .meta > button.stream')
                        .forEach(button => {
                            button.addEventListener('click', function() {
                                    const event = this.parentElement.parentElement.parentElement;
                                    const id = event.id;

                                    document.querySelectorAll('.ui.feed > .event').forEach(e => e.classList.add('hidden'));
                                    displayAncestor(id);
                                    displayDescendant(id);

                                    document.location.hash = id;

                                    function displayAncestor(id) {
                                        const event = document.getElementById(id);
                                        event.classList.remove('hidden');
                                          
                                        if (event.getAttribute('pid')) {
                                            return displayAncestor(event.getAttribute('pid'));
                                        } else {
                                            return
                                        }
                                    }
                                    function displayDescendant(id) {
                                        const event = document.getElementById(id);
                                        event.classList.remove('hidden');
                                        
                                        const s = '.event[pid="' + id + '"]'
                                        const descendants = document.querySelectorAll(s);
                                        if (descendants.length) {
                                            return descendants.forEach(event => displayDescendant(event.id));
                                        } else {
                                            return
                                        }
                                    }
                                }
                            );
                        }
                    );
                    document.querySelectorAll('.ui.feed > .event > .content .meta > button.show-all')
                        .forEach(button => {
                            button.addEventListener('click', function() {
                                    const event = this.parentElement.parentElement.parentElement;
                                    const id = event.id;

                                    document.querySelectorAll('.ui.feed > .event.hidden').forEach(e => e.classList.remove('hidden'));

                                    document.location.hash = id;
                                }
                            );
                        }
                    );
                });
            </script>
        </body>
        </html>`;
        const HTML = new DOMParser().parseFromString(HTMLTemplate, "text/html");
        const feeds = HTML.getElementById('main-feed');

        for (const obj of this.statusList) {
            let feed;
            if (anonymity_list.includes(obj.account.acct)) {
                feed = this.feed(obj, true);
            } else {
                feed = this.feed(obj);
            }
            feeds.append(feed);
        }

        return HTML.documentElement.outerHTML
    }

    feed(obj, anonymity = false) {
        let feedHtml;
        let content = obj.content;
        if (obj.emojis) {
            for (const emoji of obj.emojis) {
                content = content.replace(`:${emoji.shortcode}:`, `<img src="${emoji.url}" alt=":${emoji.shortcode}:" class="emojione">`);
            }
        }

        let displayName;
        if (obj.account.display_name) {
            displayName = obj.account.display_name;
            for (const emoji of obj.account.emojis) {
                displayName = displayName.replace(`:${emoji.shortcode}:`, `<img src="${emoji.url}" alt=":${emoji.shortcode}:" class="emojione">`);
            }
        } else {
            displayName = obj.account.username;
        }

        if (anonymity) {
            feedHtml = `<div class="event">
            <div class="label">
                <img src="https://bgme.me/avatars/original/missing.png">
            </div>
            <div class="content">
                <div class="user">Anonymity</div>
                <div class="content">${content}</div>
                <span class="date">${obj.created_at.replace('T', ' ').replace(/\.\d+Z$/, ' UTC')}</span>
            </div>
            </div>`
        } else {
            feedHtml = `<div class="event">
            <div class="label">
                <a href="${obj.account.url}" rel="noopener noreferrer" target="_blank">
                    <img src="${obj.account.avatar}">
                </a>
            </div>
            <div class="content">
                <div class="user">${(displayName)}</div>
                <div class="content">${content}</div>
                <a href="${obj.url}" rel="noopener noreferrer" target="_blank" class="date">${obj.created_at.replace('T', ' ').replace(/\.\d+Z$/, ' UTC')}</a>
            </div>
            </div>`
        }
        const feed = (new DOMParser().parseFromString(feedHtml, "text/html")).documentElement.querySelector('.event');

        feed.id = obj.id;
        feed.classList.add(`child-${this.statusIndents.get(obj.id)}`);
        if (this.statusIndents.get(obj.id) && !this.sortbytime) {
            feed.style = `margin-left: ${this.statusIndents.get(obj.id)}em;`
        }
        if (obj.in_reply_to_id) {
            feed.setAttribute('pid', obj.in_reply_to_id);
        }

        if (obj.media_attachments.length) {
            const images = document.createElement('div');
            images.className = 'extra images';
            for (const media_attachment of obj.media_attachments) {
                const img = document.createElement('img');
                img.src = media_attachment.preview_url;
                if (media_attachment.description) {
                    img.alt = media_attachment.description;
                }

                const a = document.createElement('a');
                a.href = media_attachment.url;
                a.className = 'image-reference';

                a.append(img);
                images.append(a);
                feed.querySelector('.date').before(images);
            }
        }

        const button0 = genButton('jump', 'arrow up');
        const button1 = genButton('stream', 'stream');
        const button2 = genButton('show-all', 'globe');

        const meta = document.createElement('div');
        meta.className = 'meta';
        meta.textContent = `层级${this.statusIndents.get(obj.id)}`;
        if (this.statusIndents.get(obj.id)) {
            meta.append(button0);
            meta.append(button1);
        }
        meta.append(button2);
        feed.querySelector('.date').after(meta);

        return feed

        function genButton(className, iconName) {
            const button = document.createElement('button');
            button.className = `mini ui icon tertiary button ${className}`;
            const icon = document.createElement('i');
            icon.className = `${iconName} icon`;
            button.append(icon);
            return button
        }
    }
}

function saveFile(data, filename, type) {
    const file = new Blob([data], { type: type });
    const a = document.createElement('a');
    const url = URL.createObjectURL(file);
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    setTimeout(function () {
        document.body.removeChild(a);
        window.URL.revokeObjectURL(url);
    }, 0);
}

function chromeClickChecker(event) {
    return (
        event.target.tagName.toLowerCase() === 'i' &&
        event.target.classList.contains('fa-ellipsis-h') &&
        document.querySelector('div.dropdown-menu') === null
    );
}

function firefoxClickChecker(event) {
    return (
        event.target.tagName.toLowerCase() === 'button' &&
        event.target.classList.contains('icon-button') &&
        document.querySelector('div.dropdown-menu') === null
    );
}

function activate() {
    document.querySelector('body').addEventListener('click', function (event) {
        if (chromeClickChecker(event) || firefoxClickChecker(event)) {
            // Get the status for this event
            let status = event.target.parentNode.parentNode.parentNode.parentNode.parentNode;
            if (status.className.match('detailed-status__wrapper')) {
                addLink(status);
            }
        };
    }, false);
}

function addLink(status) {
    setTimeout(function () {
        const url = status.querySelector('.detailed-status__link').getAttribute('href');
        const id = url.match(/\/(\d+)\//)[1];

        const dropdown = document.querySelector('div.dropdown-menu ul');
        const separator = dropdown.querySelector('li.dropdown-menu__separator');

        const listItem = document.createElement('li');
        listItem.classList.add('dropdown-menu__item');
        listItem.classList.add('mastodon__lottery');

        const link = document.createElement('a');
        link.setAttribute('href', '#');
        link.setAttribute('target', '_blank');
        link.textContent = 'Save as HTML';

        link.addEventListener('click', function (e) {
            e.preventDefault();
            if (!window.Running) {
                window.Running = true;
                link.textContent = 'Saving, please wait……';
                run(id)
                    .then(() => { window.Running = false; })
                    .catch(e => {
                        window.Running = false;
                        throw e;
                    });
            }
        }, false);

        listItem.appendChild(link);
        dropdown.insertBefore(listItem, separator);
    }, 100);
}

function run(id) {
    const domain = document.location.host;

    const s1 = new Status(domain, id, false);
    s1.init().then(() => {
        const html = s1.html();
        saveFile(html, `${id}.html`, 'text/plain; charset=utf-8');
    });

    const s2 = new Status(domain, id, true);
    s2.init().then(() => {
        const html = s2.html();
        saveFile(html, `${id}-time.html`, 'text/plain; charset=utf-8');
    });
}


window.addEventListener('load', function () {
    activate();
}, false)