diff --git a/README.md b/README.md index 000a720..ba0de70 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Various Userscripts ### Installation -| Site | Link | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Jellyfin QuickDelete | [![Install this Userscript](https://img.shields.io/badge/Install-Userscript-282828.svg)](https://git.m3.fyi/Marsn3/userscripts/~raw/main/src/jellyfin-quick-delete.user.js) | \ No newline at end of file + +| Site | Link | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Jellyfin QuickDelete | [![Install this Userscript](https://img.shields.io/badge/Install-Userscript-282828.svg)](https://git.m3.fyi/Marsn3/userscripts/~raw/main/src/jellyfin-quick-delete.user.js) | +| Character.ai History Dumper | [![Install this Userscript](https://img.shields.io/badge/Install-Userscript-282828.svg)](https://git.m3.fyi/Marsn3/userscripts/~raw/main/src/charai-history-dumper.user.js) | diff --git a/src/charai-history-dumper.user.js b/src/charai-history-dumper.user.js new file mode 100644 index 0000000..cc8a628 --- /dev/null +++ b/src/charai-history-dumper.user.js @@ -0,0 +1,118 @@ +// ==UserScript== +// @name CharacterAI History Dumper +// @namespace https://git.m3.fyi/Marsn3/userscripts +// @match https://beta.character.ai/* +// @grant none +// @version 1.0 +// @author Marsn3 +// @description Allows downloading saved chat messages from CharacterAI. +// @downloadURL https://git.m3.fyi/Marsn3/userscripts/~raw/main/src/charai-history-dumper.user +// @updateURL https://git.m3.fyi/Marsn3/userscripts/~raw/main/src/charai-history-dumper.user +// ==/UserScript== + +const log = (firstArg, ...remainingArgs) => + console.log( + `[CharacterAI History Dumper v1.0] ${firstArg}`, + ...remainingArgs + ); +log.error = (firstArg, ...remainingArgs) => + console.error( + `[CharacterAI History Dumper v1.0] ${firstArg}`, + ...remainingArgs + ); + +// Endpoints to intercept. +const CHARACTER_MESSAGES_URL = + "https://beta.character.ai/chat/history/external/msgs/?history"; +/** Maps a character's identifier to their basic info + chat histories. */ +const characterToSavedDataMap = {}; + +/** Creates the "Download" link on the "View Saved Chats" page. */ +const addDownloadLinkInSavedChats = (dataString, filename) => { + // Don't create duplicate links. + if (document.getElementById("injected-chat-dl-link")) { + return; + } + + // We want to add a link next to the "your past conversations with XXX" text. + const element = document.getElementsByClassName("postButtonCaption")[0]; + + const dataBlob = new Blob([dataString], { type: "text/plain" }); + const downloadLink = document.createElement("a"); + downloadLink.id = "injected-chat-dl-link"; + downloadLink.textContent = "Download"; + downloadLink.href = URL.createObjectURL(dataBlob); + downloadLink.download = filename; + downloadLink.style = "padding-left: 8px"; + element.appendChild(downloadLink); +}; + +/** Escapes a string so it can be used inside a regex. */ +const escapeStringForRegExp = (stringToGoIntoTheRegex) => { + return stringToGoIntoTheRegex.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + +/** Takes in chat histories and anonymizes them. */ + +/** Configures XHook to intercept the endpoints we care about. */ +const configureXHookIntercepts = () => { + xhook.after((_req, res) => { + try { + const endpoint = res.finalUrl; + if (endpoint !== undefined) { + if (endpoint.split("=")[0] !== CHARACTER_MESSAGES_URL) { + // We don't care about other endpoints. + return; + } else { + console.log("Found URL"); + } + + const data = JSON.parse(res.data); + let characterIdentifier; + + characterIdentifier = data.messages[0].src__name.trim(); + + // We have all the downloadable data for this character, and we're on the + // correct page. Create the download link. + log( + `Got all the data for ${characterIdentifier}, creating download link.` + ); + + //log("If it doesn't show up, here's the data:", JSON.stringify(data)); + + // For some reason, the link doesn't get added if we call this right now, + // so we wait a little while instead. Probably React re-render fuckery. + addDownloadLinkInSavedChats( + JSON.stringify(data), + `${characterIdentifier}.json` + ); + } + } catch (err) { + log.error("ERROR:", err); + } + }); +}; + +// This is where XHook (lib for intercepting XHR/AJAX calls) gets injected into +// the document, and once it gets properly parsed it'll call out to the setup +// function. +// +// Copy-pasted and slightly adapted from: https://stackoverflow.com/a/8578840 +log("Injecting XHook to intercept XHR/AJAX calls."); +(function (document, elementTagName, elementTagId) { + var js, + fjs = document.getElementsByTagName(elementTagName)[0]; + if (document.getElementById(elementTagId)) { + return; + } + js = document.createElement(elementTagName); + js.id = elementTagId; + js.onload = function () { + log("Done! Configuring intercepts."); + configureXHookIntercepts(); + }; + // Link to hosted version taken from the official repo: + // https://github.com/jpillora/xhook + js.src = "https://jpillora.com/xhook/dist/xhook.min.js"; + fjs.parentNode.insertBefore(js, fjs); +})(document, "script", "xhook"); diff --git a/src/jellyfin-quick-delete.user.js b/src/jellyfin-mediautils.user.js similarity index 72% rename from src/jellyfin-quick-delete.user.js rename to src/jellyfin-mediautils.user.js index bbe361f..c0de86b 100644 --- a/src/jellyfin-quick-delete.user.js +++ b/src/jellyfin-mediautils.user.js @@ -1,12 +1,14 @@ // ==UserScript== -// @name QuickDelete -// @description Quickly delete or refresh items in Jellyfin +// @name Jellyfin MediaUtils +// @description Utilities for Jellyfin to quickly delete, update or compare media // @namespace https://git.m3.fyi/Marsn3/userscripts -// @version 0.4 +// @version 0.5 // @author Marsn3 -// @match https://media.m3.fyi/* -// @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net +// @match YOUR-JELLYFIN-URL // @grant none +// @downloadURL https://git.m3.fyi/Marsn3/userscripts/~raw/main/src/jellyfin-mediautils.user +// @updateURL https://git.m3.fyi/Marsn3/userscripts/~raw/main/src/jellyfin-mediautils.user + // @run-at document-end // ==/UserScript== @@ -14,19 +16,19 @@ "use strict"; // Define Constants - const username = "mars"; - const password = "absw3712mcS"; - const userid = "e55a6ca076a14cb5a6ca9cfaa75498c1"; - const baseURL = window.origin; - const re = /\[(.*)\]/; - var apiKey = "38346c399d57454da3bdbf47ba716765"; + const PASSWORD = "PASSWORD"; + const USERID = "USERID"; + const BASEURL = window.origin; + const RE = /\[(.*)\]/; + const HEADERS = { + "X-Emby-Authorization": `MediaBrowser Client="Jellyfin MediaUtils", Device="Browser", DeviceId="", Version="1.0.0", Token="${apiKey}"`, + }; + var apiKey = "APIKEY"; // Authorize user - fetch(`${baseURL}/Users/${userid}/Authenticate/?pw=${password}`, { + fetch(`${BASEURL}/Users/${USERID}/Authenticate/?pw=${PASSWORD}`, { method: "POST", - headers: { - "X-Emby-Authorization": `MediaBrowser Client="QuickDelete", Device="Browser", DeviceId="", Version="1.0.0", Token="${apiKey}"`, - }, + headers: HEADERS, }) .then((response) => response.json()) .then((data) => (apiKey = data.AccessToken)); // Store AccessToken @@ -53,9 +55,7 @@ // Send refresh request fetch(url, { method: "POST", - headers: { - "X-Emby-Authorization": `MediaBrowser Client="QuickDelete", Device="Chrome", DeviceId="test", Version="10.8.9", Token="${apiKey}"`, - }, + headers: HEADERS, }); // Replace empty image with rotating spinner this.parentElement.firstChild.firstChild.classList.remove("audiotrack"); @@ -75,8 +75,9 @@ }`; // Add style element to document head document.head.appendChild(style); - this.parentElement.style.backgroundColor = "rgba(131, 165, 152, 0.3)" - this.parentElement.firstChild.style.backgroundColor = "rgba(69, 133, 136, 0.5)" + this.parentElement.style.backgroundColor = "rgba(131, 165, 152, 0.3)"; + this.parentElement.firstChild.style.backgroundColor = + "rgba(69, 133, 136, 0.5)"; }; //Return complete element return el; @@ -95,23 +96,22 @@ // Bind get function el.onclick = function () { - let url = `${window.origin}/Users/${userid}/Items/${this.parentElement.dataset.id}`; + let url = `${window.origin}/Users/${USERID}/Items/${this.parentElement.dataset.id}`; console.log(`Fetching ${url}`); // Send deletion request fetch(url, { method: "GET", - headers: { - "X-Emby-Authorization": `MediaBrowser Client="QuickDelete", Device="Chrome", DeviceId="test", Version="10.8.9", Token="${apiKey}"`, - }, + headers: HEADERS, }) .then((response) => response.json()) .then((data) => // Display bitrate alert( - `${data.MediaStreams[0].BitRate.toString().substring(0, 4)}kbps | ${data.MediaStreams[0].BitRate.toString().length} | ${re.exec(data.Path)[1] - }` + `${data.MediaStreams[0].BitRate.toString().substring(0, 4)}kbps | ${ + data.MediaStreams[0].BitRate.toString().length + } | ${RE.exec(data.Path)[1]}` ) ); }; @@ -138,9 +138,7 @@ // Send deletion request fetch(url, { method: "DELETE", - headers: { - "X-Emby-Authorization": `MediaBrowser Client="QuickDelete", Device="Chrome", DeviceId="test", Version="10.8.9", Token="${apiKey}"`, - }, + headers: HEADERS, }); // Remove parent to provide feedback and prevent double deletion @@ -158,8 +156,8 @@ let curr = collection[i]; if (curr.firstChild.firstChild !== null) { if (curr.firstChild.firstChild.classList.contains("audiotrack")) { - curr.style.backgroundColor = "rgba(251, 73, 52, 0.7)" - curr.firstChild.style.backgroundColor = "#cc2412" + curr.style.backgroundColor = "rgba(251, 73, 52, 0.7)"; + curr.firstChild.style.backgroundColor = "#cc2412"; } }