Download character.ai chat

Downloads the current character.ai chat as a text file. Right click on page to use.

// ==UserScript==
// @name         Download character.ai chat
// @namespace    https://github.com/wiger3/downloadchat/
// @version      2.5
// @author       wiger3
// @description  Downloads the current character.ai chat as a text file. Right click on page to use.
// @match        https://old.character.ai/chat2*
// @match        https://character.ai/chat/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=character.ai
// @run-at       context-menu
// ==/UserScript==

/* Changelog
    2.0 - First version, the chat gets printed into the browser console
    2.1 - Replaced the console logging with actual downloading. The chat will now download as "<character name> <date>.txt"
    2.2 - Fixed an issue with downloading longer chats
    2.3 - Added a dialog to select how to format the downloaded file. First public release
    2.4 - Added support for new character.ai website
    2.5 - Fixed an issue with downloading empty messages. Now will download as empty instead of "undefined".
*/

(async ()=>{
    var cai_version = -1;
    if(location.hostname == "old.character.ai")
        cai_version = 1;
    else if(location.pathname.startsWith("/chat/"))
        cai_version = 2;
    else
        return alert("Unsupported character.ai version");

    var token;
    if(cai_version == 1)
        token = JSON.parse(localStorage['char_token']).value;
    else if(cai_version == 2)
        token = JSON.parse(document.getElementById("__NEXT_DATA__").innerHTML).props.pageProps.token;

    var _cache; // avoid double request
    async function _fetchchats(charid) {
        if(!_cache) {
            let url = 'https://neo.character.ai/chats/recent/' + charid;

            let response = await fetch(url, {
                headers: { "Authorization": `Token ${token}` }
            });
            let json = await response.json();
            _cache = json['chats'];
        }
        return _cache;
    }
    async function getChats(charid) {
        let json = await _fetchchats(charid);
        let chats = [];
        for(let x of json) chats.push(x.chat_id);
        return chats;
    }
    async function getMessages(chat, format) {
        let url = 'https://neo.character.ai/turns/' + chat + '/';
        let next_token = null;

        let turns = [];
        do {
            let url2 = url;
            if(next_token != null)
                url2 += "?next_token=" + encodeURIComponent(next_token);
            let response = await fetch(url2, {
                headers: { "Authorization": `Token ${token}` }
            });
            let json = await response.json();

            for(let turn of json['turns']) {
                let o = {};
                if(format == "definition")
                    o.author = turn.author.is_human ? "{{user}}" : "{{char}}";
                else if(format == "names")
                    o.author = turn.author.name;
                o.message = turn.candidates.find(x => x.candidate_id === turn.primary_candidate_id).raw_content;
                if (o.message == undefined)
                    o.message = "";
                turns.push(o);
            }
            next_token = json['meta']['next_token'];
        } while(next_token != null);
        return turns.reverse();
    }
    async function getCharacterName(charid) {
        let json = await _fetchchats(charid);
        return json[0].character_name;
    }
    async function saveChat(e) {
        let format = e.formData.get('format');
        dialog.close();
        let char;
        if(cai_version == 1)
            char = params('char');
        else if(cai_version == 2)
            char = location.pathname.split("/")[2];
        let history = params('hist');
        if(history === null) {
            let chats = await getChats(char);
            history = chats[0];
        }
        let msgs = await getMessages(history, format);
        let str = "";
        for(let msg of msgs) {
            str += `${msg.author}: ${msg.message}\n`;
        }
        let date = new Date();
        let date_str = `${date.getDate()}-${date.getMonth()+1}-${date.getFullYear()} ${date.getHours()}.${date.getMinutes()}`;
        download(`${await getCharacterName(char)} ${date_str}.txt`, str.trimEnd());
    }
    
    var dialog = open("", "caiDownloader", "popup");
    if(!dialog)
        return alert("Failed to open downloader dialog. Check browser pop-up settings?");
    dialog.resizeTo(600, 600);
    let ddocument = dialog.document;
    ddocument.body.style.backgroundColor = "white";
    ddocument.body.style.fontFamily = "sans-serif";
    let el, label;
    el = ddocument.createElement("h2");
    el.appendChild(ddocument.createTextNode("Please select downloader format"));
    ddocument.body.appendChild(el);
    let form = ddocument.createElement("form");
        el = ddocument.createElement("input"); // like definition
            el.type = "radio";
            el.name = "format";
            el.id = "definition";
            el.value = el.id;
            el.checked = true;
            form.appendChild(el);
            label = ddocument.createElement("label");
                label.htmlFor = "definition";
                label.innerHTML = "Like definition";
                el = ddocument.createElement("div");
                    el.style.paddingLeft = "2em";
                    el.style.color = "gray";
                    el.innerHTML = "{{char}}: Hello! I am the bot!<br>{{user}}: Hi. I'm the person chatting with the bot.";
                    label.appendChild(el);
            form.appendChild(label);
        el = ddocument.createElement("input"); // using names
            el.type = "radio";
            el.name = "format";
            el.id = "names";
            el.value = el.id;
            form.appendChild(el);
            label = ddocument.createElement("label");
                label.htmlFor = "names";
                label.innerHTML = "Using names";
                el = ddocument.createElement("div");
                    el.style.paddingLeft = "2em";
                    el.style.color = "gray";
                    el.innerHTML = "Chatty AI: Hello! I am the bot!<br>You: Hi. I'm the person chatting with the bot.";
                    label.appendChild(el);
            form.appendChild(label);
        el = document.createElement("button"); // submit
            el.innerHTML = "Download";
            el.style.float = "right";
            el.style.backgroundColor = "cornflowerblue";
            el.style.borderRadius = "1em";
            el.style.padding = "0.5em";
            el.style.margin = "2em";
            form.appendChild(el);
        form.onformdata = saveChat;
    ddocument.body.appendChild(form);

    function download(filename, text) {
        var element = document.createElement('a');
        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
        element.setAttribute('download', filename);
        element.style.display = 'none';
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
    }
    function params(parameterName) {
        var result = null,
            tmp = [];
        location.search
            .substr(1)
            .split("&")
            .forEach(function (item) {
              tmp = item.split("=");
              if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
            });
        return result;
    }
})();