// ==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/fomantic-ui@2.8.7/dist/semantic.min.css" integrity="sha256-2+dssJtgusl/DZZZ8gF9ayAgRzcewXQsaP86E4Ul+ss=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fancybox@3.0.1/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/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.7/dist/semantic.min.js" integrity="sha256-yibQd6vg4YwSTFUcgd+MwPALTUAVCKTjh4jMON4j+Gk=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/fancybox@3.0.1/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)