Greasy Fork is available in English.
Save status to a html file.
// ==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 feedfunction 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 eventlet 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)