TORN: Dowload WarReport as CSV

Displays a button that allows users to download a csv version of their war report

  1. // ==UserScript==
  2. // @name TORN: Dowload WarReport as CSV
  3. // @namespace http://torn.city.com.dot.com.com
  4. // @version 1.0.3
  5. // @description Displays a button that allows users to download a csv version of their war report
  6. // @author Ironhydedragon[2428902]
  7. // @match https://www.torn.com/war.php?step=rankreport*
  8. // @license MIT
  9. // @run-at document-end
  10. // ==/UserScript==
  11.  
  12. //////// GLOBAL VARIABLES ////////
  13. const PDA_API_KEY = '###PDA-APIKEY###';
  14. function isPDA() {
  15. const PDATestRegex = !/^(###).+(###)$/.test(PDA_API_KEY);
  16.  
  17. return PDATestRegex;
  18. }
  19.  
  20. let GLOBAL_STATE = {
  21. // userId: USER_ID,
  22. factionId: undefined,
  23. reportId: undefined,
  24. };
  25.  
  26. //////// MODEL /////////
  27. function getGlobalState() {
  28. return GLOBAL_STATE;
  29. }
  30. function setGlobalState(newState) {
  31. GLOBAL_STATE = { ...getGlobalState(), ...newState };
  32. }
  33.  
  34. function getApiKey() {
  35. return localStorage.getItem('tornDownloadCsvApiKey');
  36. }
  37. function setApikey(apiKey) {
  38. localStorage.setItem('tornDownloadCsvApiKey', apiKey);
  39. }
  40.  
  41. // function getUserId() {
  42. // return getGlobalState().userId;
  43. // }
  44. // function setUserId(value, currentState) {
  45. // currentState = currentState || getGlobalState();
  46. // const newState = { ...currentState, userId: value };
  47. // return setGlobalState(newState);
  48. // }
  49.  
  50. function getFactionId() {
  51. return getGlobalState().factionId;
  52. }
  53. function setFactionId(value, currentState) {
  54. currentState = currentState || getGlobalState();
  55. const newState = { ...currentState, factionId: value };
  56. return setGlobalState(newState);
  57. }
  58.  
  59. function getReportId() {
  60. return getGlobalState().reportId;
  61. }
  62. function setReportId(value, currentState) {
  63. currentState = currentState || getGlobalState();
  64. const newState = { ...currentState, reportId: value[0] };
  65. return setGlobalState(newState);
  66. }
  67.  
  68. async function fetchPlayerData(apiKey) {
  69. try {
  70. const response = await fetch(`https://api.torn.com/user/?selections=profile&key=${apiKey}`);
  71. const data = await response.json();
  72.  
  73. if (data.error && (data.error.error === 'Incorrect key' || data.error.error === 'Access level of this key is not high enough')) {
  74. throw new Error(`Something went wrong: ${data.error.error}`);
  75. }
  76. return data;
  77. } catch (error) {
  78. console.error(error);
  79. }
  80. }
  81.  
  82. //////// UTIL FUNCITONS ////////
  83. async function requireElement(selectors, conditionsCallback) {
  84. try {
  85. await new Promise((res, rej) => {
  86. maxCycles = 500;
  87. let current = 1;
  88. const interval = setInterval(() => {
  89. if (document.querySelector(selectors)) {
  90. if (conditionsCallback === undefined) {
  91. clearInterval(interval);
  92. return res();
  93. }
  94. if (conditionsCallback(document.querySelector(selectors))) {
  95. clearInterval(interval);
  96. return res();
  97. }
  98. }
  99. if (current === maxCycles) {
  100. clearInterval(interval);
  101. rej('Timeout: Could not find element on page');
  102. }
  103. current++;
  104. }, 10);
  105. });
  106. } catch (err) {
  107. console.error(err);
  108. }
  109. }
  110.  
  111. //////// API FORM CODE ////////
  112. function submitFormCallback() {
  113. const inputEl = document.querySelector('#api-form__input');
  114. const submitBtnEl = document.querySelector('#api-form__submit');
  115.  
  116. const apiKey = inputEl.value;
  117. if (apiKey.length !== 16) {
  118. inputEl.style.border = `2px solid ${red}`;
  119. submitBtnEl.disabled = true;
  120. return;
  121. }
  122. setApikey(apiKey);
  123. dismountApiForm();
  124. window.location.reload();
  125. }
  126.  
  127. function inputValidatorCallback(event) {
  128. const inputEl = document.querySelector('#api-form__input');
  129. const submitBtnEl = document.querySelector('#api-form__submit');
  130. if (event.target.value.length === 16) {
  131. submitBtnEl.disabled = false;
  132. inputEl.style.border = '1px solid #444';
  133. }
  134. if (event.target.value.length !== 16) {
  135. submitBtnEl.disabled = true;
  136. }
  137. }
  138.  
  139. function renderApiFormStylesheet() {
  140. const apiFormStylesheetHTML = `
  141. <style>
  142. #api-form.header-wrapper-top {
  143. display: flex;
  144. }
  145. #api-form.header-wrapper-top .container {
  146. display: flex;
  147. justify-content: start;
  148. align-items: center;
  149. padding-left: 20px;
  150. }
  151.  
  152. #api-form.header-wrapper-top h2 {
  153. display: block;
  154. text-align: center;
  155. margin: 0;
  156. width: 172px;
  157. }
  158.  
  159. #api-form.header-wrapper-top input {
  160. background: linear-gradient(0deg,#111,#000);
  161. border-radius: 5px;
  162. box-shadow: 0 1px 0 hsla(0,0%,100%,.102);
  163. box-sizing: border-box;
  164. color: #9f9f9f;
  165. display: inline;
  166. font-weight: 400;
  167. height: 24px;
  168. width: clamp(170px, 50%, 250px);
  169. margin: 0 0 0 21px;
  170. outline: none;
  171. padding: 0 10px 0 10px;
  172. font-size: 12px;
  173. font-style: italic;
  174. vertical-align: middle;
  175. border: 0;
  176. text-shadow: none;
  177. z-index: 100;
  178. }
  179. #api-form.header-wrapper-top a {
  180. margin: 0 8px;
  181. }
  182. </style>`;
  183. document.head.insertAdjacentHTML('beforeend', apiFormStylesheetHTML);
  184. }
  185.  
  186. function renderApiForm() {
  187. const topHeaderBannerEl = document.querySelector('#topHeaderBanner');
  188. const apiFormHTML = `
  189. <div id="api-form" class="header-wrapper-top">
  190. <div class="container clear-fix">
  191. <h2>API Key</h2>
  192. <input
  193. id="api-form__input"
  194. type="text"
  195. placeholder="Enter a full-acces API key..."
  196. />
  197. <a href="#" id="api-form__submit" type="btn" disabled><span class="link-text">Submit</span</button>
  198. </div>
  199. </div>`;
  200.  
  201. topHeaderBannerEl.insertAdjacentHTML('afterbegin', apiFormHTML);
  202.  
  203. // set event liseners
  204. //// Event listeners
  205. document.querySelector('#api-form__submit').addEventListener('click', submitFormCallback);
  206. document.querySelector('#api-form__input').addEventListener('input', inputValidatorCallback);
  207. document.querySelector('#api-form__input').addEventListener('keyup', (event) => {
  208. if (event.key === 'Enter' || event.keyCode === 13) {
  209. submitFormCallback();
  210. }
  211. });
  212. }
  213. function dismountApiForm() {
  214. document.querySelector('#api-form').remove();
  215. }
  216.  
  217. function apiFormController() {
  218. renderApiFormStylesheet();
  219. renderApiForm();
  220. }
  221.  
  222. //////// CSV RELATED CODE ////////
  223. async function fetchRankedWarReport(reportID, apiKey) {
  224. const response = await fetch(`https://api.torn.com/torn/${reportID}?selections=rankedwarreport&key=${apiKey}`);
  225. return await response.json();
  226. }
  227.  
  228. function createWarReportContent(dataObject) {
  229. let rows = [];
  230.  
  231. dataObject = dataObject.rankedwarreport.factions;
  232.  
  233. for (const faction in dataObject) {
  234. const factionName = dataObject[faction].name;
  235. rows.push(factionName);
  236.  
  237. // const first = Object.keys(dataObject[faction].members)[0];
  238. // const headerRow = Object.keys(dataObject[faction].members[first]);
  239. const headerRow = ['Members', 'Level', 'Attacks', 'Score'];
  240. rows.push(headerRow);
  241.  
  242. for (const member in dataObject[faction].members) {
  243. // rows.push(Object.values(dataObject[faction].members[member]));
  244. const rawRow = Object.values(dataObject[faction].members[member]);
  245. const customRow = rawRow
  246. .filter((item, index) => index !== 1)
  247. .map((item, index) => {
  248. if (index === 0) {
  249. return `${item} [${member}]`;
  250. }
  251. return item;
  252. });
  253. rows.push(customRow);
  254. console.log(member, customRow); // TEST
  255. }
  256. }
  257.  
  258. return rows.map((row) => (Array.isArray(row) ? row.map((value) => `"${value}"`).join(';') : `"${row}"`)).join('\r\n');
  259. }
  260.  
  261. function downloadCsv(data, fileName) {
  262. const blob = new Blob([data], { type: 'text/csv' });
  263. const url = window.URL.createObjectURL(blob);
  264.  
  265. const a = document.createElement('a');
  266. a.href = url;
  267. a.download = `${fileName}.csv`;
  268. a.addEventListener('click', () => {});
  269. a.click();
  270. }
  271.  
  272. // async function copyToClipBoard(data) {
  273. // try {
  274. // console.log('copyCSV'); // TEST
  275.  
  276. // const blob = new Blob([data], { type: 'text/csv' });
  277. // // const clipboardItem = new ClipboardItem({
  278. // // 'text/plain': await new Promise((res) => {
  279. // // res(blob);
  280. // // }),
  281. // // });
  282. // navigator.clipboard.writeText([await blob.text()]);
  283. // } catch (error) {
  284. // console.error(error); // TEST
  285. // }
  286. // }
  287.  
  288. async function exportCsvClickHandler(e) {
  289. try {
  290. const warReportData = await fetchRankedWarReport(getReportId(), getApiKey());
  291. const warReportContent = createWarReportContent(warReportData);
  292.  
  293. downloadCsv(warReportContent, `Ranked War Report [${getReportId()}]`);
  294. // copyToClipBoard(warReportContent);
  295.  
  296. e.target.classList.add('disable');
  297. } catch (error) {
  298. console.error(error); // TEST
  299. }
  300. }
  301.  
  302. //////// VIEW ////////
  303.  
  304. function renderStylesheet() {
  305. const stylesheetHTML = `
  306. <style>
  307. #export-csv {
  308. float: right;
  309. display: flex;
  310. justify-content: center;
  311. align-items: center;
  312. margin-right: 10px
  313. }
  314. #export-csv:hover {
  315. cursor: pointer;
  316. }
  317. #export-csv.disable {
  318. color: #999;
  319. }
  320. #export-csv svg {
  321. padding-right: 2px
  322. fill: currentcolor;
  323. width: 15px;
  324. height: 16px;
  325. }
  326. #export-csv.disable csv {
  327. fill: #999;
  328. }
  329. </style>`;
  330. const headEl = document.querySelector('head');
  331. headEl.insertAdjacentHTML('beforeend', stylesheetHTML);
  332. }
  333.  
  334. function renderExportCsvEl() {
  335. const linkHTML = `
  336. <span id="export-csv">
  337. <svg
  338. viewBox="0 0 64 64"
  339. version="1.1"
  340. xmlns="http://www.w3.org/2000/svg"
  341. xmlns:xlink="http://www.w3.org/1999/xlink"
  342. xml:space="preserve"
  343. xmlns:serif="http://www.serif.com/"
  344. style="fill: currentcolor; /* fill-rule: evenodd; */ /* clip-rule: evenodd; */ /* stroke-linejoin: round; */ /* stroke-miterlimit: 2; */"
  345. stroke="currentcolor"
  346. >
  347. <g id="SVGRepo_iconCarrier">
  348. <rect id="Icons" x="-576" y="-128" width="1280" height="800" style="fill: none"></rect>
  349. <path id="download" d="M48.089,52.095l0,4l-32.049,0l0,-4l32.049,0Zm-16.025,-4l-16.024,-16l8.098,0l-0.049,-24l15.975,0l0.048,24l7.977,0l-16.025,16Z"></path>
  350. </g>
  351. </svg>
  352. Export CSV
  353. </span>`;
  354.  
  355. const titleContainerEl = document.querySelector('.war-report-wrap .title-black');
  356. titleContainerEl.insertAdjacentHTML('beforeend', linkHTML);
  357.  
  358. document.querySelector('#export-csv').addEventListener('click', exportCsvClickHandler);
  359. }
  360.  
  361. // function apiFormController() {} // TODO
  362.  
  363. //////// CONTROLLERS ////////
  364. async function initController() {
  365. try {
  366. renderStylesheet();
  367.  
  368. if (!getApiKey() && !isPDA()) {
  369. renderApiFormStylesheet();
  370. renderApiForm();
  371. return;
  372. }
  373.  
  374. if (isPDA()) {
  375. setApikey(PDA_API_KEY);
  376. }
  377.  
  378. const playerData = await fetchPlayerData(getApiKey());
  379. const factionId = playerData.faction.faction_id;
  380. setFactionId(factionId);
  381.  
  382. const urlParams = new URLSearchParams(window.location.href);
  383. const reportId = urlParams.get('rankID').match(/\d*/);
  384. setReportId(reportId);
  385. } catch (error) {
  386. console.error(error);
  387. }
  388. }
  389.  
  390. async function rankedWarCsvController() {
  391. try {
  392. await requireElement('.war-report-wrap .title-black');
  393. renderExportCsvEl();
  394. } catch (error) {
  395. console.error(error); // TEST
  396. }
  397. }
  398.  
  399. //// Promise race conditions
  400. // necessary as PDA scripts are inject after window.onload
  401. // const PDAPromise = new Promise((res, rej) => {
  402. // if (document.readyState === 'complete') res();
  403. // });
  404.  
  405. // const browserPromise = new Promise((res, rej) => {
  406. // window.addEventListener('load', () => res());
  407. // });
  408.  
  409. (async () => {
  410. try {
  411. console.log('🔫 WarReport CSV script is on!'); // TEST
  412. // await Promise.race([PDAPromise, browserPromise]);
  413. await initController();
  414. if (getApiKey()) {
  415. await rankedWarCsvController();
  416. }
  417. } catch (error) {
  418. console.error(error); // TEST
  419. }
  420. })();