Greasy Fork is available in English.

Export Twitter Bookmarks

Exports the data of your Twitter bookmarks into a JSON file that you can download

// ==UserScript==
// @name         Export Twitter Bookmarks
// @description  Exports the data of your Twitter bookmarks into a JSON file that you can download
// @author       GPP (https://github.com/gpp-0)
// @version      1
// @namespace    https://github.com/gpp-0
// @match        https://x.com/i/bookmarks*
// @grant        none
// @license      MIT
// ==/UserScript==

const bookmarksFound = new Event("bookmarksFound");

const tweetRegex = /.*\/status\/\d+$/;
const excludeRegex = /.*\/i\/status\/\d+$/;
const imageRegex = /https:\/\/pbs.twimg.com\/media\/.*/;

const initConfig = { attributes: true, childList: true, subtree: true };
const config = {
  attributes: true,
  childList: true,
  subtree: true,
  attributeFilter: ["src"],
};

const tweetMap = new Map();
let bookmarksDiv;

const initObserver = new MutationObserver(function findBookmarks(list, observer) {
  for (const mutation of list) {
    if (mutation.type === "childList") {
      for (const node of mutation.addedNodes) {
        bookmarksDiv = node.querySelectorAll(
          'div[aria-label="Timeline: Bookmarks"]'
        );
        if (bookmarksDiv.length) {
          bookmarksDiv = bookmarksDiv[0];
          observer.disconnect();
          document.dispatchEvent(bookmarksFound);
          return;
        }
      }
    }
  }
});
const observer = new MutationObserver(function onBookmarksUpdate(list, observer) {
  for (const mutation of list) {
    if (mutation.type === "childList") {
      for (const node of mutation.addedNodes) {
        const tweetNodes = node.getElementsByTagName("article");
        for (const tweetNode of tweetNodes) {
          const tweetPermaNode = Array.from(
            tweetNode.getElementsByTagName("a")
          ).find(
            (aTag) =>
              tweetRegex.test(aTag.href) && !excludeRegex.test(aTag.href)
          );
          if(!tweetPermaNode) continue;
          const tweetUrl = tweetPermaNode.href;
          if (!tweetMap.has(tweetUrl)) {
            tweetMap.set(tweetUrl, {
              text: "",
              datePosted: tweetPermaNode.firstChild.getAttribute("datetime"),
              images: new Set(),
            });
          }
          const tweet = tweetMap.get(tweetUrl);
          if (tweet.text) break;
          let textNode = tweetNode.querySelectorAll(
            'div[data-testid="tweetText"]'
          );
          if (textNode.length) {
            for (const textPartNode of textNode[0].childNodes) {
              if (textPartNode.nodeName == "IMG"){
                tweet.text += textPartNode.alt;
              } else {
                tweet.text += textPartNode.textContent;
              }
            }
          }
          if (
            tweetNode.querySelectorAll('div[data-testid="tweetPhoto"]').length
          ) {
            tweet.video = true;
          }
        }
      }
    }
    if (mutation.type === "attributes") {
      const imageUrl = mutation.target.src;
      if (imageRegex.test(imageUrl)) {
        const tweetNode = mutation.target.closest("article");
        const tweetUrl = Array.from(tweetNode.getElementsByTagName("a")).find(
          (aTag) => tweetRegex.test(aTag.href) && !excludeRegex.test(aTag.href)
        ).href;
        tweetMap.get(tweetUrl).images.add(imageUrl.split("&name")[0]);
      }
    }
  }
});

document.addEventListener("bookmarksFound", (event) => {
  observer.observe(bookmarksDiv, config);
  bookmarksDiv.style.border = "4px solid red";
  console.log("Observing...");

  addEventListener("keydown", (keyEvent) => {
    if (keyEvent.code == "F2") {
      const text = JSON.stringify(
        Object.fromEntries(tweetMap),
        (_, value) => (value instanceof Set ? [...value] : value),
        2
      );
      var element = document.createElement("a");
      element.setAttribute(
        "href",
        "data:text/json;charset=utf-8," + encodeURIComponent(text)
      );
      element.setAttribute("download", "bookmarks.json");
      element.style.display = "none";
      element.click();
      console.log("Saved bookmarks to file");
    }
  });

  addEventListener("keydown", (keyEvent) => {
    if (keyEvent.code == "F4") {
      console.log(tweetMap);
    }
  });
});

initObserver.observe(document.body, initConfig);