Greasy Fork is available in English.

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;
    }
})();