Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name CAI-Downloader
- // @homepageURL https://rentry.co/uscai-download
- // @namespace ScriptCAI
- // @description README at https://rentry.co/uscai-download
- // @include https://beta.character.ai/*
- // @version 0.1.0
- // @author Anon
- // @require https://raw.githubusercontent.com/eligrey/FileSaver.js/master/dist/FileSaver.min.js
- // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
- // @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@0.7
- // @grant GM_addStyle
- // @grant GM_xmlhttpRequest
- // ==/UserScript==
- (function () {
- 'use strict';
- function uiCreateButton(text, classes, style) {
- const button = document.createElement('button');
- button.innerHTML = text;
- if (classes != null) button.classList.add(...classes);
- if (style != null) button.style = style;
- return button;
- }
- const urlRegex = /chat/;
- const rxCharInfo = /character.info/;
- const rxChatHistory = /history.continue/;
- const rxFullMessageHistory = /chat.character.histories/; // Use to download full chat history
- let apiData = {
- chatCharInfo: null,
- chatInfo: null,
- chatId: null,
- chatHistories: null
- };
- function updateRequestInfo(req) {
- let url = req.responseURL;
- if (url.search(rxCharInfo) != -1) {
- apiData.chatCharInfo = JSON.parse(req.response).character;
- //console.log('CHARACTER INFO', apiData.chatCharInfo);
- } else if (url.search(rxChatHistory) != -1) {
- apiData.chatInfo = JSON.parse(req.response);
- console.log('CHAT INFO', apiData.chatInfo);
- } else if (url.search(rxFullMessageHistory) != -1) {
- apiData.chatHistories = JSON.parse(req.response);
- console.log('FULL MESSAGE HISTORY', apiData.chatHistories);
- }
- }
- function setRequestListeners() {
- // Attach Listener to XHP requests
- (function (open) {
- XMLHttpRequest.prototype.open = function () {
- this.addEventListener('readystatechange', () => {
- if (this.readyState === 4 && this.status === 200 && this.responseURL.search(urlRegex) != -1) {
- // console.log('XHR_INTERCEPT', JSON.parse(this.response));
- updateRequestInfo(this);
- }
- }, false);
- open.apply(this, arguments);
- };
- })(XMLHttpRequest.prototype.open);
- }
- const selector = {
- chat: {
- container: 'div.container-fluid.justify-content-center',
- chatTitle: '.chattitle',
- msgRow: '.msg-row',
- msgWrapper: '.markdown-wrapper',
- msgLastVisible: 'div[aria-hidden="false"]',
- msgRatingButtons: 'div.annotation-buttons-container',
- dropdown: 'span[data-toggle="dropdown"]',
- dropdownButtons: 'button.dropdown-item'
- }
- };
- const ids = {
- scrollbar: 'scrollBar'
- };
- // Classes to tag injected elements
- const cls = {
- Chat: 'chat-inject',
- Menu: 'menu-inject'
- };
- document.getElementsByClassName('chattitle');
- document.getElementsByClassName('annotation-buttons-container');
- document.getElementsByClassName('typing-dot');
- document.getElementsByClassName('infinite-scroll-component__outerdiv');
- document.getElementsByClassName('react-swipeable-view-container');
- document.getElementsByClassName('markdown-wrapper');
- let displayedMessageRows = document.getElementsByClassName('msg-row');
- document.getElementsByClassName('markdown-wrapper-last-msg');
- function parseChatHTML(text) {
- text = text.replaceAll(/<\/p>|<\/li>/gm, '\n'); // newlines
- text = text.replaceAll(/<em>|<\/em>/gm, '*'); // italics
- text = text.replaceAll(/<strong>|<\/strong>/gm, '**'); // bold
- text = text.replaceAll(/<del>|<\/del>/gm, '~~'); // strikethrough
- text = text.replaceAll(/<h([1-9])>/gm, (a, b) => {
- return '#'.repeat(Number(b)) + ' ';
- }); // '#'.repeat(parseInt('$1'))
- text = text.replaceAll(/<.*?src="([^"]+).*?>/gm, '![]($1)'); // images
- text = text.replaceAll(/<code>|<\/code>/gm, '`'); // code/highlight
- text = text.replaceAll(/<li>/gm, '- '); // lists
- text = text.replace(/<[^>]*>?/gm, ''); // remove all other html
- text = text.replaceAll(/\n$/gm, ''); // prevent double newlines
- return text;
- }
- function getChatText() {
- const messageNodes = document.querySelectorAll(selector.chat.msgRow);
- const lastVisible = document.querySelector(selector.chat.msgLastVisible);
- let messages = [...messageNodes].slice(1).reverse();
- messages.push(lastVisible);
- let chatMessages = '';
- let speakers = [];
- messages.forEach(el => {
- if (el) {
- let textElement = el.querySelector(selector.chat.msgWrapper);
- let speakerElement = el.querySelector('span').childNodes[0];
- let text = parseChatHTML(textElement.innerHTML);
- let speaker = speakerElement.textContent;
- if (!speakers.includes(speaker) && !speaker.includes('@')) {
- speakers.push(speaker);
- }
- chatMessages += `\n${speaker}:\n${text}\n`;
- }
- });
- return [chatMessages, speakers];
- }
- function archiveChat() {
- let data = apiData.chatInfo;
- let chatTitle = document.querySelector('.chattitle').childNodes[1];
- let title = chatTitle.textContent;
- let date = new Date().toISOString().slice(0, 10);
- let time = new Date().toTimeString().slice(0, 8);
- let filename = `${title}_${date}_${time}`.replaceAll(':', '-');
- let chatText = getChatText();
- let speakers = data.participants.map(p => p.name);
- let created = data.created.split('.')[0].replace('T', '_');
- let lastInteract = data.last_interaction.split('.')[0].replace('T', '_');
- let header = `// Archived from ${window.location.href} on ${date} at ${time}
- // Conversation between: ${speakers.join(', ')}
- // Started ${created}, with last interaction on ${lastInteract}.\n`;
- const blob = new Blob([header, chatText[0]], {
- type: 'text/plain;charset=utf-8'
- });
- saveAs(blob, `[CAI]${filename.replaceAll(' ', '_')}.txt`);
- VM.showToast(`Saved current chat as ${filename}.txt`);
- }
- const firstMessageMarker = '\n@';
- function archiveAllChat() {
- const scroll = document.getElementById(ids.scrollbar);
- let finished = false;
- // Download when start of chat is reached
- const checkStart = () => {
- let msgs = [...displayedMessageRows].map(el => el.innerText);
- let first4 = msgs.splice(msgs.length - 4, msgs.length);
- if (first4.some(el => el.includes(firstMessageMarker)) && !finished) {
- finished = true;
- scrollObserver.disconnect();
- // attachChatObserver();
- setTimeout(() => {
- archiveChat();
- }, 500);
- }
- };
- const scrollObserver = new MutationObserver(m => {
- checkStart();
- scroll.scrollTo({
- top: -scroll.scrollHeight
- });
- });
- scrollObserver.observe(document.body, {
- subtree: true,
- childList: true
- });
- // detachChatObserver();
- scroll.scrollTo({
- top: -scroll.scrollHeight,
- behavior: 'smooth'
- });
- checkStart();
- }
- function clickDropdownElement(buttonText, clickDropup = false) {
- let dropdown = document.querySelectorAll(selector.chat.dropdown);
- dropdown = clickDropup ? dropdown[1] : dropdown[0];
- dropdown.click();
- let button = [...document.querySelectorAll(selector.chat.dropdownButtons)].find(el => el.textContent === buttonText);
- button.click();
- }
- function downloadHistory() {
- if (apiData.chatHistories.histories) {
- console.log('HISTORY', apiData.chatHistories.histories);
- // Remove chats with < 2 messages
- let filteredChats = apiData.chatHistories.histories.filter(chat => chat.msgs.length > 1);
- filteredChats.forEach(chat => {
- downloadSingleChat(chat);
- });
- VM.showToast(`Downloaded ${filteredChats.length} chats with ${apiData.chatCharInfo.participant__name}`);
- }
- }
- function downloadSingleChat(chat) {
- let filename = `${chat.msgs[0].src.name}_Chat-${chat.external_id}`;
- let participants = [chat.msgs[0].src.name, chat.msgs[0].tgt.name];
- let dDate = new Date().toISOString().slice(0, 10);
- let dTime = new Date().toTimeString().slice(0, 8);
- let created = chat.created.split('.')[0].replace('T', '_');
- let lastInteract = chat.last_interaction.split('.')[0].replace('T', '_');
- let chatText = [...chat.msgs.map(msg => `${msg.src.name}:\n` + msg.text)].join('\n\n');
- console.log('Downloading...', chat);
- let header = `// Source: https://beta.character.ai/chat?char=${apiData.chatCharInfo.external_id}&hist=${chat.external_id}
- // Archived on ${dDate} at ${dTime}.
- // Conversation between: ${participants.join(', ')}
- // Started ${created}, ${chat.msgs.length} messages with last interaction on ${lastInteract}.\n\n`;
- const blob = new Blob([header, chatText], {
- type: 'text/plain;charset=utf-8'
- });
- saveAs(blob, `[CAI]${filename.replaceAll(' ', '_')}.txt`);
- }
- function getChatByIndex(index) {
- if (apiData.chatHistories.histories) {
- let chats = apiData.chatHistories.histories;
- // Remove chats with < 2 messages
- let filteredChats = chats.filter(chat => chat.msgs.length > 1);
- console.log('FILTERED CHATS', filteredChats);
- let chat = filteredChats[index];
- downloadSingleChat(chat);
- }
- }
- function createChatElements() {
- let button = uiCreateButton('<b>⬇</b>', [cls.Chat, 'btn', 'border'], 'top:0; right:0; position:absolute; padding:0 0.4em 0 0.4em; font-size:1.5em; z-index: 1025');
- button.addEventListener('click', () => archiveAllChat());
- document.body.appendChild(button);
- }
- function setupChatTools() {
- removeInjectedTools();
- createChatElements();
- VM.shortcut.register('alt-5', () => {
- archiveChat();
- });
- VM.shortcut.register('alt-6', () => {
- archiveAllChat();
- });
- VM.shortcut.register('alt-1', () => {
- clickDropdownElement('Regenerate Last Message', true);
- });
- VM.shortcut.register('alt-2', () => {
- clickDropdownElement('Remove Messages');
- });
- VM.shortcut.enable();
- }
- function removeInjectedTools() {
- const chatElements = document.querySelectorAll(`.${cls.Chat}`);
- chatElements.forEach(el => {
- el.remove();
- });
- }
- function setupEditTools() {
- VM.shortcut.register('alt-1', () => {
- [...document.querySelectorAll('button[type="submit"]')].find(el => el.textContent === 'Save ').click();
- });
- VM.shortcut.enable();
- }
- function createHistoryElements() {
- let button = uiCreateButton('▼', [cls.Chat, 'btn', 'border'], 'top:0;right:0;position:absolute;z-index: 1025');
- button.addEventListener('click', () => downloadHistory());
- document.body.appendChild(button);
- let conversations = document.getElementsByClassName('character-row');
- VM.observe(document.body, () => {
- if (document.querySelector('.character-row')) {
- [...conversations].forEach((conversation, i) => {
- let downloadChat = uiCreateButton('DOWNLOAD ▼', [cls.Chat, 'btn', 'border'], 'font-size: 0.8em;');
- // let recentMessages = conversation.querySelectorAll('.markdown-wrapper');
- // let searchText = recentMessages[1].innerHTML;
- conversation.appendChild(downloadChat);
- downloadChat.addEventListener('click', () => getChatByIndex(i));
- });
- return true;
- }
- });
- }
- function setupHistoryTools() {
- removeInjectedTools();
- createHistoryElements();
- }
- let previousUrl = '';
- function checkUrlMutation(url) {
- // Chat pages
- if (url.indexOf('/chat?char=') > -1 || url.indexOf('/chat?hist=') > -1) {
- setupChatTools();
- } else {
- removeInjectedTools();
- }
- // Editing Page
- if (url.indexOf('/editing?char') > -1) {
- setupEditTools();
- }
- // History Page
- if (url.indexOf('/histories?char') > -1) {
- setupHistoryTools();
- }
- }
- const docObserver = new MutationObserver(m => {
- // Observe Url changes
- if (location.href != previousUrl) {
- previousUrl = location.href;
- console.log('NEW PAGE');
- checkUrlMutation(location.href);
- }
- });
- const config = {
- subtree: false,
- childList: true
- };
- function attachDocObserver() {
- console.log('Doc MutationObserver Enabled!');
- docObserver.observe(document.body, config);
- }
- setRequestListeners();
- attachDocObserver();
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement