Advertisement
Guest User

Untitled

a guest
Dec 20th, 2022
164
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.83 KB | None | 0 0
  1.  
  2. // ==UserScript==
  3. // @name CAI-Downloader
  4. // @homepageURL https://rentry.co/uscai-download
  5. // @namespace ScriptCAI
  6. // @description README at https://rentry.co/uscai-download
  7. // @include https://beta.character.ai/*
  8. // @version 0.1.0
  9. // @author Anon
  10. // @require https://raw.githubusercontent.com/eligrey/FileSaver.js/master/dist/FileSaver.min.js
  11. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
  12. // @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@0.7
  13. // @grant GM_addStyle
  14. // @grant GM_xmlhttpRequest
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. function uiCreateButton(text, classes, style) {
  21. const button = document.createElement('button');
  22. button.innerHTML = text;
  23. if (classes != null) button.classList.add(...classes);
  24. if (style != null) button.style = style;
  25. return button;
  26. }
  27.  
  28. const urlRegex = /chat/;
  29. const rxCharInfo = /character.info/;
  30. const rxChatHistory = /history.continue/;
  31. const rxFullMessageHistory = /chat.character.histories/; // Use to download full chat history
  32.  
  33. let apiData = {
  34. chatCharInfo: null,
  35. chatInfo: null,
  36. chatId: null,
  37. chatHistories: null
  38. };
  39. function updateRequestInfo(req) {
  40. let url = req.responseURL;
  41. if (url.search(rxCharInfo) != -1) {
  42. apiData.chatCharInfo = JSON.parse(req.response).character;
  43. //console.log('CHARACTER INFO', apiData.chatCharInfo);
  44. } else if (url.search(rxChatHistory) != -1) {
  45. apiData.chatInfo = JSON.parse(req.response);
  46. console.log('CHAT INFO', apiData.chatInfo);
  47. } else if (url.search(rxFullMessageHistory) != -1) {
  48. apiData.chatHistories = JSON.parse(req.response);
  49. console.log('FULL MESSAGE HISTORY', apiData.chatHistories);
  50. }
  51. }
  52. function setRequestListeners() {
  53. // Attach Listener to XHP requests
  54. (function (open) {
  55. XMLHttpRequest.prototype.open = function () {
  56. this.addEventListener('readystatechange', () => {
  57. if (this.readyState === 4 && this.status === 200 && this.responseURL.search(urlRegex) != -1) {
  58. // console.log('XHR_INTERCEPT', JSON.parse(this.response));
  59. updateRequestInfo(this);
  60. }
  61. }, false);
  62. open.apply(this, arguments);
  63. };
  64. })(XMLHttpRequest.prototype.open);
  65. }
  66.  
  67. const selector = {
  68. chat: {
  69. container: 'div.container-fluid.justify-content-center',
  70. chatTitle: '.chattitle',
  71. msgRow: '.msg-row',
  72. msgWrapper: '.markdown-wrapper',
  73. msgLastVisible: 'div[aria-hidden="false"]',
  74. msgRatingButtons: 'div.annotation-buttons-container',
  75. dropdown: 'span[data-toggle="dropdown"]',
  76. dropdownButtons: 'button.dropdown-item'
  77. }
  78. };
  79. const ids = {
  80. scrollbar: 'scrollBar'
  81. };
  82.  
  83. // Classes to tag injected elements
  84. const cls = {
  85. Chat: 'chat-inject',
  86. Menu: 'menu-inject'
  87. };
  88. document.getElementsByClassName('chattitle');
  89. document.getElementsByClassName('annotation-buttons-container');
  90. document.getElementsByClassName('typing-dot');
  91. document.getElementsByClassName('infinite-scroll-component__outerdiv');
  92. document.getElementsByClassName('react-swipeable-view-container');
  93. document.getElementsByClassName('markdown-wrapper');
  94. let displayedMessageRows = document.getElementsByClassName('msg-row');
  95. document.getElementsByClassName('markdown-wrapper-last-msg');
  96.  
  97. function parseChatHTML(text) {
  98. text = text.replaceAll(/<\/p>|<\/li>/gm, '\n'); // newlines
  99. text = text.replaceAll(/<em>|<\/em>/gm, '*'); // italics
  100. text = text.replaceAll(/<strong>|<\/strong>/gm, '**'); // bold
  101. text = text.replaceAll(/<del>|<\/del>/gm, '~~'); // strikethrough
  102. text = text.replaceAll(/<h([1-9])>/gm, (a, b) => {
  103. return '#'.repeat(Number(b)) + ' ';
  104. }); // '#'.repeat(parseInt('$1'))
  105. text = text.replaceAll(/<.*?src="([^"]+).*?>/gm, '![]($1)'); // images
  106. text = text.replaceAll(/<code>|<\/code>/gm, '`'); // code/highlight
  107. text = text.replaceAll(/<li>/gm, '- '); // lists
  108. text = text.replace(/<[^>]*>?/gm, ''); // remove all other html
  109. text = text.replaceAll(/\n$/gm, ''); // prevent double newlines
  110. return text;
  111. }
  112. function getChatText() {
  113. const messageNodes = document.querySelectorAll(selector.chat.msgRow);
  114. const lastVisible = document.querySelector(selector.chat.msgLastVisible);
  115. let messages = [...messageNodes].slice(1).reverse();
  116. messages.push(lastVisible);
  117. let chatMessages = '';
  118. let speakers = [];
  119. messages.forEach(el => {
  120. if (el) {
  121. let textElement = el.querySelector(selector.chat.msgWrapper);
  122. let speakerElement = el.querySelector('span').childNodes[0];
  123. let text = parseChatHTML(textElement.innerHTML);
  124. let speaker = speakerElement.textContent;
  125. if (!speakers.includes(speaker) && !speaker.includes('@')) {
  126. speakers.push(speaker);
  127. }
  128. chatMessages += `\n${speaker}:\n${text}\n`;
  129. }
  130. });
  131. return [chatMessages, speakers];
  132. }
  133. function archiveChat() {
  134. let data = apiData.chatInfo;
  135. let chatTitle = document.querySelector('.chattitle').childNodes[1];
  136. let title = chatTitle.textContent;
  137. let date = new Date().toISOString().slice(0, 10);
  138. let time = new Date().toTimeString().slice(0, 8);
  139. let filename = `${title}_${date}_${time}`.replaceAll(':', '-');
  140. let chatText = getChatText();
  141. let speakers = data.participants.map(p => p.name);
  142. let created = data.created.split('.')[0].replace('T', '_');
  143. let lastInteract = data.last_interaction.split('.')[0].replace('T', '_');
  144. let header = `// Archived from ${window.location.href} on ${date} at ${time}
  145. // Conversation between: ${speakers.join(', ')}
  146. // Started ${created}, with last interaction on ${lastInteract}.\n`;
  147. const blob = new Blob([header, chatText[0]], {
  148. type: 'text/plain;charset=utf-8'
  149. });
  150. saveAs(blob, `[CAI]${filename.replaceAll(' ', '_')}.txt`);
  151. VM.showToast(`Saved current chat as ${filename}.txt`);
  152. }
  153. const firstMessageMarker = '\n@';
  154. function archiveAllChat() {
  155. const scroll = document.getElementById(ids.scrollbar);
  156. let finished = false;
  157. // Download when start of chat is reached
  158. const checkStart = () => {
  159. let msgs = [...displayedMessageRows].map(el => el.innerText);
  160. let first4 = msgs.splice(msgs.length - 4, msgs.length);
  161. if (first4.some(el => el.includes(firstMessageMarker)) && !finished) {
  162. finished = true;
  163. scrollObserver.disconnect();
  164. // attachChatObserver();
  165. setTimeout(() => {
  166. archiveChat();
  167. }, 500);
  168. }
  169. };
  170. const scrollObserver = new MutationObserver(m => {
  171. checkStart();
  172. scroll.scrollTo({
  173. top: -scroll.scrollHeight
  174. });
  175. });
  176. scrollObserver.observe(document.body, {
  177. subtree: true,
  178. childList: true
  179. });
  180. // detachChatObserver();
  181. scroll.scrollTo({
  182. top: -scroll.scrollHeight,
  183. behavior: 'smooth'
  184. });
  185. checkStart();
  186. }
  187.  
  188. function clickDropdownElement(buttonText, clickDropup = false) {
  189. let dropdown = document.querySelectorAll(selector.chat.dropdown);
  190. dropdown = clickDropup ? dropdown[1] : dropdown[0];
  191. dropdown.click();
  192. let button = [...document.querySelectorAll(selector.chat.dropdownButtons)].find(el => el.textContent === buttonText);
  193. button.click();
  194. }
  195.  
  196. function downloadHistory() {
  197. if (apiData.chatHistories.histories) {
  198. console.log('HISTORY', apiData.chatHistories.histories);
  199. // Remove chats with < 2 messages
  200. let filteredChats = apiData.chatHistories.histories.filter(chat => chat.msgs.length > 1);
  201. filteredChats.forEach(chat => {
  202. downloadSingleChat(chat);
  203. });
  204. VM.showToast(`Downloaded ${filteredChats.length} chats with ${apiData.chatCharInfo.participant__name}`);
  205. }
  206. }
  207. function downloadSingleChat(chat) {
  208. let filename = `${chat.msgs[0].src.name}_Chat-${chat.external_id}`;
  209. let participants = [chat.msgs[0].src.name, chat.msgs[0].tgt.name];
  210. let dDate = new Date().toISOString().slice(0, 10);
  211. let dTime = new Date().toTimeString().slice(0, 8);
  212. let created = chat.created.split('.')[0].replace('T', '_');
  213. let lastInteract = chat.last_interaction.split('.')[0].replace('T', '_');
  214. let chatText = [...chat.msgs.map(msg => `${msg.src.name}:\n` + msg.text)].join('\n\n');
  215. console.log('Downloading...', chat);
  216. let header = `// Source: https://beta.character.ai/chat?char=${apiData.chatCharInfo.external_id}&hist=${chat.external_id}
  217. // Archived on ${dDate} at ${dTime}.
  218. // Conversation between: ${participants.join(', ')}
  219. // Started ${created}, ${chat.msgs.length} messages with last interaction on ${lastInteract}.\n\n`;
  220. const blob = new Blob([header, chatText], {
  221. type: 'text/plain;charset=utf-8'
  222. });
  223. saveAs(blob, `[CAI]${filename.replaceAll(' ', '_')}.txt`);
  224. }
  225. function getChatByIndex(index) {
  226. if (apiData.chatHistories.histories) {
  227. let chats = apiData.chatHistories.histories;
  228. // Remove chats with < 2 messages
  229. let filteredChats = chats.filter(chat => chat.msgs.length > 1);
  230. console.log('FILTERED CHATS', filteredChats);
  231. let chat = filteredChats[index];
  232. downloadSingleChat(chat);
  233. }
  234. }
  235.  
  236. function createChatElements() {
  237. 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');
  238. button.addEventListener('click', () => archiveAllChat());
  239. document.body.appendChild(button);
  240. }
  241. function setupChatTools() {
  242. removeInjectedTools();
  243. createChatElements();
  244. VM.shortcut.register('alt-5', () => {
  245. archiveChat();
  246. });
  247. VM.shortcut.register('alt-6', () => {
  248. archiveAllChat();
  249. });
  250. VM.shortcut.register('alt-1', () => {
  251. clickDropdownElement('Regenerate Last Message', true);
  252. });
  253. VM.shortcut.register('alt-2', () => {
  254. clickDropdownElement('Remove Messages');
  255. });
  256. VM.shortcut.enable();
  257. }
  258. function removeInjectedTools() {
  259. const chatElements = document.querySelectorAll(`.${cls.Chat}`);
  260. chatElements.forEach(el => {
  261. el.remove();
  262. });
  263. }
  264. function setupEditTools() {
  265. VM.shortcut.register('alt-1', () => {
  266. [...document.querySelectorAll('button[type="submit"]')].find(el => el.textContent === 'Save ').click();
  267. });
  268. VM.shortcut.enable();
  269. }
  270. function createHistoryElements() {
  271. let button = uiCreateButton('▼', [cls.Chat, 'btn', 'border'], 'top:0;right:0;position:absolute;z-index: 1025');
  272. button.addEventListener('click', () => downloadHistory());
  273. document.body.appendChild(button);
  274. let conversations = document.getElementsByClassName('character-row');
  275. VM.observe(document.body, () => {
  276. if (document.querySelector('.character-row')) {
  277. [...conversations].forEach((conversation, i) => {
  278. let downloadChat = uiCreateButton('DOWNLOAD ▼', [cls.Chat, 'btn', 'border'], 'font-size: 0.8em;');
  279. // let recentMessages = conversation.querySelectorAll('.markdown-wrapper');
  280. // let searchText = recentMessages[1].innerHTML;
  281. conversation.appendChild(downloadChat);
  282. downloadChat.addEventListener('click', () => getChatByIndex(i));
  283. });
  284. return true;
  285. }
  286. });
  287. }
  288. function setupHistoryTools() {
  289. removeInjectedTools();
  290. createHistoryElements();
  291. }
  292.  
  293. let previousUrl = '';
  294. function checkUrlMutation(url) {
  295. // Chat pages
  296. if (url.indexOf('/chat?char=') > -1 || url.indexOf('/chat?hist=') > -1) {
  297. setupChatTools();
  298. } else {
  299. removeInjectedTools();
  300. }
  301. // Editing Page
  302. if (url.indexOf('/editing?char') > -1) {
  303. setupEditTools();
  304. }
  305. // History Page
  306. if (url.indexOf('/histories?char') > -1) {
  307. setupHistoryTools();
  308. }
  309. }
  310. const docObserver = new MutationObserver(m => {
  311. // Observe Url changes
  312. if (location.href != previousUrl) {
  313. previousUrl = location.href;
  314. console.log('NEW PAGE');
  315. checkUrlMutation(location.href);
  316. }
  317. });
  318. const config = {
  319. subtree: false,
  320. childList: true
  321. };
  322. function attachDocObserver() {
  323. console.log('Doc MutationObserver Enabled!');
  324. docObserver.observe(document.body, config);
  325. }
  326.  
  327. setRequestListeners();
  328. attachDocObserver();
  329.  
  330. })();
  331.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement