Skip to content

Instantly share code, notes, and snippets.

@rickosborne
Last active April 23, 2023 23:45
Show Gist options
  • Save rickosborne/22c70f6f88f2b6c516813a36d3e75e16 to your computer and use it in GitHub Desktop.
Save rickosborne/22c70f6f88f2b6c516813a36d3e75e16 to your computer and use it in GitHub Desktop.
Bookmarklet to extract a Mastodon thread into JSON
javascript:(function () {
/* ========================================
Mastodon thread scraper, by Rick Osborne.
Copy this entire file, from the opening "javascript:" to the last "();".
Create a new bookmark in your browser, using the copied text as the URL.
When you are on a Mastodon thread page, activate this bookmark.
======================================== */
function findOne(parent, selector, block) {
if (parent == null) {
return undefined;
}
var els = parent.querySelectorAll(selector);
if (els.length === 1) {
if (block != null) {
block(els[0]);
}
return els[0];
}
console.log(`Not found: ${selector}`, els);
return undefined;
}
function numberish(s) {
return ["number", "string"].includes(typeof s) ? Number(s) : undefined;
}
function ancestor(el, className) {
if (el == null || el.classList.contains(className)) {
return el;
}
return ancestor(el.parentElement, className);
}
function author(el) {
var a = {};
findOne(el, ".display-name .display-name__html", e => a.name = e.innerText);
findOne(el, ".display-name .display-name__account", e => a.handle = e.innerText);
findOne(el, ".detailed-status__display-name, .status__display-name", header => {
author.href = header.href;
var avatar = findOne(header, ".account__avatar img");
author.avatar = avatar?.src;
author.id = avatar?.alt;
});
return a;
}
function elChildren(el, tagName, block) {
const children = [...el.children].filter(e => e.tagName);
if (block != null) {
block(children);
}
return children;
}
function elNum(el, tagName, index, block) {
const child = elChildren(el, tagName).at(index);
if (block != null && child != null) {
block(child);
}
return child;
}
var top = document.querySelector(".detailed-status");
if (top == null) {
alert("No detailed status found. Use this bookmarklet on a detailed status page.");
return;
}
var post = {
author: author(top)
};
findOne(top, ".status__content .status__content__text", e => post.html = e.innerHTML);
findOne(top, ".detailed-status__meta", (meta) => {
findOne(meta, ".detailed-status__datetime", dt => {
post.url = dt.href;
findOne(dt, "span", e => post.timestamp = e.innerText);
});
findOne(meta, ".dropdown-menu__text-button > span", e => post.edited = e.innerText);
findOne(meta, "i:first-of-type", e => post.visibility = e.title);
findOne(meta, ".detailed-status__reblogs span span span", e => post.boosts = numberish(e.innerText));
findOne(meta, ".detailed-status__link span span span", e => post.favorites = numberish(e.innerText));
});
var scrollable = ancestor(top, "scrollable");
elNum(scrollable, "DIV", 1, replyContainer => {
var replies = elChildren(replyContainer, "DIV");
post.replies = replies.map(replyEl => {
var reply = {};
findOne(replyEl, ".status", statusEl => {
reply.id = statusEl.getAttribute("data-id");
findOne(statusEl, ".status__relative-time", e => reply.href = e.href);
findOne(statusEl, ".status__visibility-icon i", e => reply.visibility = e.title);
findOne(statusEl, "time", e => reply.timestamp = e.getAttribute("datetime"));
reply.author = author(statusEl);
findOne(statusEl, ".status__content .status__content__text", e => reply.html = e.innerHTML);
});
return reply;
});
});
console.log(post);
if (navigator.clipboard) {
navigator.clipboard.writeText(JSON.stringify(post, null, 2)).then(() => {
alert("The thread has been copied to your clipboard in JSON format.")
});
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment