diff --git a/extras/characterai-dumper/README.md b/extras/characterai-dumper/README.md index 95f63c4..27ff7f1 100644 --- a/extras/characterai-dumper/README.md +++ b/extras/characterai-dumper/README.md @@ -1,6 +1,6 @@ # CharacterAI Dumper Userscript -This userscript allows you to download your saved messages with any bot you've ever talked to, given you can reach their chat history page. +This userscript allows you to download your saved messages with any bot you've ever talked to, given you can reach their chat history page. If you're a bot creator, it also allows you to separately download your bot's definitions. ## How to use @@ -12,8 +12,26 @@ This userscript allows you to download your saved messages with any bot you've e - After a few seconds, a `Download` link should pop up next to the "Your past conversations with so-and-so" header: ![What the download link looks like](./example-images/02.png) - Clicking on the link will download a `.json` file containing the bot's basic info (name, description, greeting) and all the interactions you've ever had with it. +- If you're a bot creator, you can also head over into the Character Editor to download a bot's definitions: + ![Where to find the definitions download](./example-images/03.png) -**NOTE:** The script attempts to anonymize the dumped data (it scrubs known sensitive fields and attempts to replace any instances of your name within messages), but if you're paranoid, you should open the downloaded JSON and search for your username/email/display name just to make sure. +--- + +**NOTE 1:** If you've never used the "Save and Start New Chat" feature, you won't have the "View Saved Chats" option shown in the first screenshot. + +That's fine, it just means you'll need to manually access the histories page by replacing the `/chat` path with `/histories` in the URL. For example, if you're at: + +https://beta.character.ai/chat?char={{BOT_ID_HERE}} + +Just rewrite the URL so it reads: + +https://beta.character.ai/histories?char={{BOT_ID_HERE}} + +And you'll reach the page that should show the `Download` link. + +--- + +**NOTE 2:** The script attempts to anonymize the dumped data (it scrubs known sensitive fields and attempts to replace any instances of your name within messages), but if you're paranoid, you should open the downloaded JSON and search for your username/email/display name just to make sure. ## Troubleshooting diff --git a/extras/characterai-dumper/characterai-dumper.user.js b/extras/characterai-dumper/characterai-dumper.user.js index ca13dc2..278b7b1 100644 --- a/extras/characterai-dumper/characterai-dumper.user.js +++ b/extras/characterai-dumper/characterai-dumper.user.js @@ -3,28 +3,29 @@ // @namespace Violentmonkey Scripts // @match https://beta.character.ai/* // @grant none -// @version 1.2 +// @version 1.3 // @author 0x000011b -// @description Allows downloading saved chat messages from CharacterAI. +// @description Allows downloading saved chat messages and character definitions from CharacterAI. // @downloadURL https://git.fuwafuwa.moe/waifu-collective/toolbox/raw/branch/master/extras/characterai-dumper/characterai-dumper.user.js // @updateURL https://git.fuwafuwa.moe/waifu-collective/toolbox/raw/branch/master/extras/characterai-dumper/characterai-dumper.user.js // ==/UserScript== const log = (firstArg, ...remainingArgs) => - console.log(`[CharacterAI Dumper v1.2] ${firstArg}`, ...remainingArgs); + console.log(`[CharacterAI Dumper v1.3] ${firstArg}`, ...remainingArgs); log.error = (firstArg, ...remainingArgs) => - console.error(`[CharacterAI Dumper v1.2] ${firstArg}`, ...remainingArgs); + console.error(`[CharacterAI Dumper v1.3] ${firstArg}`, ...remainingArgs); +// Endpoints to intercept. const CHARACTER_INFO_URL = "https://beta.character.ai/chat/character/info/"; +const CHARACTER_EXTRA_INFO_URL = "https://beta.character.ai/chat/character/"; const CHARACTER_HISTORIES_URL = "https://beta.character.ai/chat/character/histories/"; +/** Maps a character's identifier to their basic info + chat histories. */ const characterToSavedDataMap = {}; -// -// Code to add download link to the page. -// -const addDownloadLinkFor = (dataString, filename) => { +/** 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; @@ -48,13 +49,41 @@ const addDownloadLinkFor = (dataString, filename) => { } }; -// -// Logic to remove personal data from the dumps. -// +/** Creates the "Download" link in the "Character Editor" page. */ +const addDownloadLinkInCharacterEditor = ( + dataString, + filename, + characterName +) => { + if (document.getElementById("injected-character-info-dl-link")) { + return; + } + + const suspectedElements = document.querySelectorAll( + "div.p-0.m-1.mb-3.border.rounded.m-1" + ); + for (const element of suspectedElements) { + if (!element.textContent.includes(characterName)) { + continue; + } + + const dataBlob = new Blob([dataString], { type: "text/plain" }); + const downloadLink = document.createElement("a"); + downloadLink.id = "injected-character-info-dl-link"; + downloadLink.textContent = "Download"; + downloadLink.href = URL.createObjectURL(dataBlob); + downloadLink.download = filename; + downloadLink.style = "padding-left: 66px"; + 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. */ const anonymizeHistories = (histories) => { const namesToReplace = new Set(); @@ -137,16 +166,15 @@ const anonymizeHistories = (histories) => { return histories; }; -// -// Request intercept and data handling logic. -// +/** Configures XHook to intercept the endpoints we care about. */ const configureXHookIntercepts = () => { xhook.after((_req, res) => { try { const endpoint = res.finalUrl; if ( endpoint !== CHARACTER_INFO_URL && - endpoint !== CHARACTER_HISTORIES_URL + endpoint !== CHARACTER_HISTORIES_URL && + endpoint !== CHARACTER_EXTRA_INFO_URL ) { // We don't care about other endpoints. return; @@ -157,6 +185,8 @@ const configureXHookIntercepts = () => { if (res.finalUrl === CHARACTER_INFO_URL) { characterIdentifier = data.character.name; + data.character.user__username = "[BOT_CREATOR_NAME_REDACTED]"; + log(`Got character info for ${characterIdentifier}, caching...`); if (!characterToSavedDataMap[characterIdentifier]) { @@ -172,6 +202,30 @@ const configureXHookIntercepts = () => { } characterToSavedDataMap[characterIdentifier].histories = anonymizeHistories(data); + } else if (res.finalUrl === CHARACTER_EXTRA_INFO_URL) { + characterIdentifier = data.character.name; + data.user__username = "[BOT_CREATOR_NAME_REDACTED]"; + data.character.user__username = "[BOT_CREATOR_NAME_REDACTED]"; + + log( + `Got definitions for ${characterIdentifier}, creating download link.` + ); + + log("If it doesn't show up, here's the data:", JSON.stringify(data)); + + // The character editor returns all the info we want in a single + // request, so we can just create the button and return from this + // function already. + setTimeout( + () => + addDownloadLinkInCharacterEditor( + JSON.stringify(data), + `${characterIdentifier} (Definitions).json`, + characterIdentifier + ), + 2000 + ); + return; } const currentCharacter = characterToSavedDataMap[characterIdentifier]; @@ -191,7 +245,7 @@ const configureXHookIntercepts = () => { // so we wait a little while instead. Probably React re-render fuckery. setTimeout( () => - addDownloadLinkFor( + addDownloadLinkInSavedChats( JSON.stringify(currentCharacter), `${characterIdentifier}.json` ), diff --git a/extras/characterai-dumper/example-images/03.png b/extras/characterai-dumper/example-images/03.png new file mode 100644 index 0000000..3d916b2 Binary files /dev/null and b/extras/characterai-dumper/example-images/03.png differ