Chat2.0Recorder

Saves chat history. For Chat 2.0.

  1. // ==UserScript==
  2. // @name Chat2.0Recorder
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Saves chat history. For Chat 2.0.
  6. // @author bot_7420 [2937420]
  7. // @match https://www.torn.com/*
  8. // @run-at document-start
  9. // @grant GM_addStyle
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. let db = null;
  16.  
  17. window.onload = function () {
  18. initCSS();
  19. initControlPanel();
  20. };
  21.  
  22. // Hook fetch chat
  23. const originalFetch = unsafeWindow.fetch;
  24. unsafeWindow.fetch = async (...args) => {
  25. let [resource, config] = args;
  26. let response = await originalFetch(resource, config);
  27. if (response.url.indexOf("sendbird.com/v3/group_channels/") != -1 && response.url.indexOf("/messages?") != -1) {
  28. response.clone().json().then((data) => {
  29. if (Array.isArray(data.messages)) {
  30. // console.log(data.messages);
  31. dbWriteArray(data.messages);
  32. }
  33. });
  34. }
  35. return response;
  36. };
  37.  
  38. // Hook chat Websocket on receive message
  39. const originalSend = WebSocket.prototype.send;
  40. window.sockets = [];
  41. WebSocket.prototype.send = function (...args) {
  42. if (window.sockets.indexOf(this) === -1 && this.url.indexOf("sendbird.com") > -1) {
  43. console.log("ChatRecorder: found chat2.0 websocket");
  44. window.sockets.push(this);
  45. this.addEventListener("message", function (event) {
  46. if (event.data.startsWith("MESG")) {
  47. const messageObj = JSON.parse(event.data.substring(4));
  48. handleMessage(messageObj);
  49. }
  50. });
  51. }
  52. return originalSend.call(this, ...args);
  53. };
  54.  
  55. initIndexDB();
  56.  
  57. function handleMessage(message) {
  58. if (!message || !message.channel_url) {
  59. return;
  60. }
  61. dbWrite(message);
  62. }
  63.  
  64. function initIndexDB() {
  65. const openRequest = indexedDB.open("ScriptChat2.0RecorderDB", 2);
  66. openRequest.onupgradeneeded = function (e) {
  67. db = e.target.result;
  68. if (!db.objectStoreNames.contains("messageStore")) {
  69. console.log("ChatRecorder: initIndexDB open onupgradeneeded create store");
  70. const objectStore = db.createObjectStore("messageStore", { keyPath: "messageId", autoIncrement: false });
  71. objectStore.createIndex("targetPlayerId", "targetPlayerId", { unique: false });
  72. }
  73. };
  74. openRequest.onsuccess = function (e) {
  75. console.log("ChatRecorder: initIndexDB open onsuccess");
  76. db = e.target.result;
  77. };
  78. openRequest.onerror = function (e) {
  79. console.error("ChatRecorder: initIndexDB open onerror");
  80. console.dir(e);
  81. };
  82. }
  83.  
  84. function dbWrite(message) {
  85. if (!db) {
  86. console.error("ChatRecorder: dbWrite db is null");
  87. }
  88.  
  89. let msg = {};
  90. const targetPlayer = getTargetPlayerFromMessage(message);
  91. if (!targetPlayer) {
  92. return;
  93. }
  94. msg.targetPlayerId = targetPlayer.id;
  95. msg.targetPlayerName = targetPlayer.name;
  96. msg.senderPlayerId = message.user.guest_id;
  97. msg.senderPlayerName = message.user.name;
  98. msg.timestamp = message.ts;
  99. msg.messageText = message.message;
  100. msg.messageId = message.msg_id;
  101. // console.log(msg);
  102.  
  103. const transaction = db.transaction(["messageStore"], "readwrite");
  104. transaction.oncomplete = (event) => { };
  105. transaction.onerror = (event) => {
  106. console.error("ChatRecorder: dbWrite transaction onerror [" + msg.targetPlayerId + " " + msg.senderName + ": " + msg.messageText + "]");
  107. };
  108.  
  109. const store = transaction.objectStore("messageStore");
  110. const request = store.put(msg);
  111. request.onsuccess = (event) => { };
  112. }
  113.  
  114. function dbWriteArray(messageArray) {
  115. if (!db) {
  116. console.error("ChatRecorder: dbWriteArray db is null");
  117. }
  118.  
  119. const transaction = db.transaction(["messageStore"], "readwrite");
  120. transaction.oncomplete = (event) => { };
  121. transaction.onerror = (event) => {
  122. console.error("ChatRecorder: dbWrite transaction onerror [" + msg.targetPlayerId + " " + msg.senderName + ": " + msg.messageText + "]");
  123. };
  124.  
  125. const store = transaction.objectStore("messageStore");
  126.  
  127. for (const message of messageArray) {
  128. const targetPlayer = getTargetPlayerFromMessage(message);
  129. if (targetPlayer) {
  130. let msg = {};
  131. msg.targetPlayerId = targetPlayer.id;
  132. msg.targetPlayerName = targetPlayer.name;
  133. msg.senderPlayerId = message.user.user_id;
  134. msg.senderPlayerName = message.user.nickname;
  135. msg.timestamp = message.created_at;
  136. msg.messageText = message.message;
  137. msg.messageId = message.message_id;
  138. store.put(msg);
  139. }
  140. }
  141. }
  142.  
  143. function dbReadByTargetPlayerId(targetPlayerId) {
  144. if (!db) {
  145. console.error("ChatRecorder: dbReadByTargetPlayerId db is null");
  146. }
  147.  
  148. const transaction = db.transaction(["messageStore"], "readonly");
  149. transaction.oncomplete = (event) => { };
  150. transaction.onerror = (event) => {
  151. console.error("ChatRecorder: dbReadByTargetPlayerId transaction onerror [" + targetPlayerId + "]");
  152. };
  153.  
  154. const store = transaction.objectStore("messageStore");
  155. const index = store.index("targetPlayerId");
  156. const keyRange = IDBKeyRange.only(targetPlayerId);
  157.  
  158. return new Promise((resolve, reject) => {
  159. const resultList = [];
  160. index.openCursor(keyRange).onerror = (event) => {
  161. resolve(resultList);
  162. };
  163. index.openCursor(keyRange).onsuccess = (event) => {
  164. const cursor = event.target.result;
  165. if (cursor) {
  166. resultList.push(cursor.value);
  167. cursor.continue();
  168. } else {
  169. resolve(resultList);
  170. }
  171. };
  172. });
  173. }
  174.  
  175. function dbReadAllPlayerId() {
  176. if (!db) {
  177. console.error("ChatRecorder: dbReadAllPlayerId db is null");
  178. }
  179.  
  180. const transaction = db.transaction(["messageStore"], "readonly");
  181. transaction.oncomplete = (event) => { };
  182. transaction.onerror = (event) => {
  183. console.error("ChatRecorder: dbReadAllPlayerId transaction onerror");
  184. };
  185.  
  186. const store = transaction.objectStore("messageStore");
  187. const index = store.index("targetPlayerId");
  188. const keyRange = null;
  189.  
  190. return new Promise((resolve, reject) => {
  191. const resultList = [];
  192. index.openCursor(keyRange, "nextunique").onerror = (event) => {
  193. resolve(resultList);
  194. };
  195. index.openCursor(keyRange, "nextunique").onsuccess = (event) => {
  196. const cursor = event.target.result;
  197. if (cursor) {
  198. resultList.push(cursor.value);
  199. cursor.continue();
  200. } else {
  201. resolve(resultList);
  202. }
  203. };
  204. });
  205. }
  206.  
  207. function getTargetPlayerFromMessage(message) {
  208. if (message.channel_url.startsWith("public_")) {
  209. return null; // Ignore Globla, Trade, etc.
  210. } else if (message.channel_url.startsWith("faction-")) {
  211. return { id: "faction", name: "Faction" }; // Faction chat.
  212. }
  213.  
  214. // Private chats.
  215. const selfId = getSelfIdFromSession();
  216. const selfName = getSelfNameFromSession();
  217. if (!selfId || !selfName) {
  218. return { id: "others", name: "Other" };
  219. }
  220. const strList = message.channel_url.split("-");
  221. if (strList.length !== 3) {
  222. return { id: "others", name: "Other" };
  223. }
  224. let target = { id: "others", name: "Other" };
  225. if (parseInt(strList[1]) === selfId) {
  226. target.id = strList[2];
  227. target.name = strList[2];
  228. } else if (parseInt(strList[2]) === selfId) {
  229. target.id = strList[1];
  230. target.name = strList[1];
  231. } else {
  232. return { id: "others", name: "Other" };
  233. }
  234. return target;
  235. }
  236.  
  237. function getSelfIdFromSession() {
  238. let index = Object.keys(sessionStorage).findIndex((item) => item.startsWith("sidebarData"));
  239. if (index >= 0) {
  240. let sidebarData = JSON.parse(sessionStorage.getItem(sessionStorage.key(index)));
  241. let userID = sidebarData.user.userID;
  242. return userID;
  243. }
  244. return null;
  245. }
  246.  
  247. function getSelfNameFromSession() {
  248. let index = Object.keys(sessionStorage).findIndex((item) => item.startsWith("sidebarData"));
  249. if (index >= 0) {
  250. let sidebarData = JSON.parse(sessionStorage.getItem(sessionStorage.key(index)));
  251. let userID = sidebarData.user.name;
  252. return userID;
  253. }
  254. return null;
  255. }
  256.  
  257. function initCSS() {
  258. const isDarkmode = $("body").hasClass("dark-mode");
  259. GM_addStyle(`.chat-control-panel-popup {
  260. position: fixed;
  261. top: 10%;
  262. left: 15%;
  263. border-radius: 10px;
  264. padding: 10px;
  265. background: ${isDarkmode ? "#282828" : "#F0F0F0"};
  266. z-index: 1000;
  267. display: none;
  268. }
  269. .chat-control-panel-results {
  270. padding: 10px;
  271. }
  272. .chat-control-player {
  273. margin: 4px 4px 4px 4px !important;
  274. display: inline-block !important;
  275. }
  276. .chat-control-panel-overlay {
  277. position: fixed;
  278. top: 0;
  279. left: 0;
  280. background: ${isDarkmode ? "#404040" : "#B0B0B0"};
  281. width: 100%;
  282. height: 100%;
  283. opacity: 0.7;
  284. z-index: 900;
  285. display: none;
  286. }
  287. div#chat-player-list {
  288. overflow-y: scroll;
  289. height: 50px;
  290. }
  291. a.chat-history-search:hover {
  292. color: #318CE7 !important;
  293. }
  294. .chat-control-panel-item {
  295. display: inline-block;
  296. margin: 2px 2px 2px 2px;
  297. }`);
  298. }
  299.  
  300. function initControlPanel() {
  301. const $title = $("div#top-page-links-list");
  302. if ($title.length === 0) {
  303. console.error("ChatRecorder: nowhere to put control panel button");
  304. }
  305. const $controlBtn = $(`<a id="chatHistoryControl" class="t-clear h c-pointer right last">
  306. <span class="icon-wrap svg-icon-wrap">
  307. <span class="link-icon-svg">
  308. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 10.33"><defs><style>.cls-1{opacity:0.35;}.cls-2{fill:#fff;}.cls-3{fill:#777;}</style></defs><g id="Слой_2" data-name="Слой 2"><g id="icons"><g class="cls-1"><path class="cls-2" d="M10,5.67a2,2,0,0,1-4,0,1.61,1.61,0,0,1,0-.39A1.24,1.24,0,0,0,7.64,3.7a2.19,2.19,0,0,1,.36,0A2,2,0,0,1,10,5.67ZM8,1C3,1,0,5.37,0,5.37s3.22,5,8,5c5.16,0,8-5,8-5S13.14,1,8,1ZM8,9a3.34,3.34,0,1,1,3.33-3.33A3.33,3.33,0,0,1,8,9Z"></path></g><path class="cls-3" d="M10,4.67a2,2,0,0,1-4,0,1.61,1.61,0,0,1,0-.39A1.24,1.24,0,0,0,7.64,2.7a2.19,2.19,0,0,1,.36,0A2,2,0,0,1,10,4.67ZM8,0C3,0,0,4.37,0,4.37s3.22,5,8,5c5.16,0,8-5,8-5S13.14,0,8,0ZM8,8a3.34,3.34,0,1,1,3.33-3.33A3.33,3.33,0,0,1,8,8Z"></path></g></g></svg>
  309. </span>
  310. </span>
  311. <span>ChatRecorder</span>
  312. </a>`);
  313. $title.append($controlBtn);
  314.  
  315. const $controlPanelDiv = $(`<div id="chatControlPanel" class="chat-control-panel-popup">control</div>`);
  316. const $controlPanelOverlayDiv = $(`<div id="chatControlOverlayPanel" class="chat-control-panel-overlay"></div>`);
  317. $controlPanelDiv.html(`
  318. <input type="text" class="chat-control-panel-item" id="chat-target-id-input" placeholder="Player ID" size="10" />
  319. <button id="chat-search" class="chat-control-panel-item" style="cursor: pointer;">Search</button><br>
  320. <div id="chat-player-list"></div><br>
  321. <textarea readonly id="chat-results" cols="120" rows="30"></textarea>
  322. `);
  323.  
  324. // Control panel onClick listeners
  325. $controlPanelDiv.find("button#chat-search").click(function () {
  326. const inputId = $controlPanelDiv.find("input#chat-target-id-input").val();
  327. dbReadByTargetPlayerId(inputId).then((result) => {
  328. let text = "";
  329. for (const message of result) {
  330. const timeStr = formatDateString(new Date(message.timestamp));
  331. text += timeStr + " " + message.senderPlayerName + ": " + message.messageText + "\n";
  332. }
  333. text += "Found " + result.length + " records\n";
  334. const $textarea = $("textarea#chat-results");
  335. $textarea.val(text);
  336. $textarea.scrollTop($textarea[0].scrollHeight);
  337. });
  338. });
  339.  
  340. $title.append($controlPanelDiv);
  341. $title.append($controlPanelOverlayDiv);
  342.  
  343. $controlBtn.click(function () {
  344. dbReadAllPlayerId().then((result) => {
  345. const $playerListDiv = $controlPanelDiv.find("div#chat-player-list");
  346. $playerListDiv.empty();
  347. let num = 0;
  348. for (const message of result) {
  349. if (num == 8) {
  350. $playerListDiv.append($(`<br>`));
  351. num = -1;
  352. }
  353. num++;
  354. let a = $(`<a class="chat-control-player">${message.targetPlayerName}</a>`);
  355. a.click(() => {
  356. $controlPanelDiv.find("input#chat-target-id-input").val(message.targetPlayerId);
  357. $controlPanelDiv.find("button#chat-search").trigger("click");
  358. });
  359. $playerListDiv.append(a);
  360. }
  361.  
  362. $controlPanelDiv.fadeToggle(200);
  363. $controlPanelOverlayDiv.fadeToggle(200);
  364. });
  365. });
  366.  
  367. $controlPanelOverlayDiv.click(function () {
  368. $controlPanelDiv.fadeOut(200);
  369. $controlPanelOverlayDiv.fadeOut(200);
  370. });
  371. }
  372.  
  373. function formatDateString(date) {
  374. const pad = (v) => {
  375. return v < 10 ? "0" + v : v;
  376. };
  377. let year = date.getFullYear();
  378. let month = pad(date.getMonth() + 1);
  379. let day = pad(date.getDate());
  380. let hour = pad(date.getHours());
  381. let min = pad(date.getMinutes());
  382. let sec = pad(date.getSeconds());
  383. return year + "/" + month + "/" + day + " " + hour + ":" + min + ":" + sec;
  384. }
  385. })();