// ==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
// @downloadURL https://update.greasyfork.cloud/scripts/416569/Mastodon%20status2html.user.js
// @updateURL https://update.greasyfork.cloud/scripts/416569/Mastodon%20status2html.meta.js
// ==/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 = `
`;
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}:`, `
`);
}
}
let displayName;
if (obj.account.display_name) {
displayName = obj.account.display_name;
for (const emoji of obj.account.emojis) {
displayName = displayName.replace(`:${emoji.shortcode}:`, `
`);
}
} else {
displayName = obj.account.username;
}
if (anonymity) {
feedHtml = `
Anonymity
${content}
${obj.created_at.replace('T', ' ').replace(/\.\d+Z$/, ' UTC')}
`
} else {
feedHtml = ``
}
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)