Yeah! for Twitter

Adds Yeah! button to Twitter, essentially a public Like

  1. // ==UserScript==
  2. // @name Yeah! for Twitter
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.4
  5. // @description Adds Yeah! button to Twitter, essentially a public Like
  6. // @author dimden.dev
  7. // @match https://x.com/*
  8. // @match https://twitter.com/*
  9. // @icon https://dimden.dev/images/yeah_logo.png
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_registerMenuCommand
  12. // ==/UserScript==
  13.  
  14. // fetch polyfill
  15. function GM_fetch(url, options = {}) {
  16. return new Promise((resolve, reject) => {
  17. const method = options.method || 'GET';
  18. const headers = options.headers || {};
  19. const body = options.body || null;
  20.  
  21. GM_xmlhttpRequest({
  22. method,
  23. url,
  24. headers,
  25. data: body,
  26. onload(response) {
  27. const responseBody = response.responseText;
  28. const status = response.status;
  29. const statusText = response.statusText;
  30. const responseHeaders = parseHeaders(response.responseHeaders);
  31.  
  32. resolve(new Response(responseBody, {
  33. status,
  34. statusText,
  35. headers: responseHeaders
  36. }));
  37. },
  38. onerror(error) {
  39. reject(new Error('Network request failed'));
  40. },
  41. ontimeout() {
  42. reject(new Error('Network request timed out'));
  43. }
  44. });
  45. });
  46. }
  47.  
  48. function parseHeaders(headersString) {
  49. const headers = new Headers();
  50. const lines = headersString.trim().split(/[\r\n]+/);
  51. lines.forEach(line => {
  52. const parts = line.split(': ');
  53. const header = parts.shift();
  54. const value = parts.join(': ');
  55. headers.append(header, value);
  56. });
  57. return headers;
  58. }
  59.  
  60. class Response {
  61. constructor(body, options) {
  62. this.body = body;
  63. this.status = options.status;
  64. this.statusText = options.statusText;
  65. this.headers = options.headers;
  66. }
  67.  
  68. text() {
  69. return Promise.resolve(this.body);
  70. }
  71.  
  72. json() {
  73. return Promise.resolve(JSON.parse(this.body));
  74. }
  75. }
  76.  
  77. // chrome.storage.local polyfill
  78. window.chrome = window.chrome || {};
  79. chrome.runtime = chrome.runtime || {id: 'userscript'};
  80. chrome.storage = chrome.storage || {};
  81. chrome.storage.local = {
  82. storageKey: 'chromeStorage',
  83.  
  84. _getStorageObject: function () {
  85. const storage = localStorage.getItem(this.storageKey);
  86. return storage ? JSON.parse(storage) : {};
  87. },
  88.  
  89. _setStorageObject: function (obj) {
  90. localStorage.setItem(this.storageKey, JSON.stringify(obj));
  91. },
  92.  
  93. get: function (keys, callback) {
  94. const storageObj = this._getStorageObject();
  95. const result = {};
  96.  
  97. if (typeof keys === 'string') {
  98. result[keys] = storageObj[keys];
  99. } else if (Array.isArray(keys)) {
  100. keys.forEach(key => {
  101. result[key] = storageObj[key];
  102. });
  103. } else if (typeof keys === 'object') {
  104. Object.keys(keys).forEach(key => {
  105. result[key] = storageObj[key] !== undefined ? storageObj[key] : keys[key];
  106. });
  107. } else {
  108. Object.assign(result, storageObj);
  109. }
  110.  
  111. callback(result);
  112. },
  113.  
  114. set: function (items, callback) {
  115. const storageObj = this._getStorageObject();
  116.  
  117. Object.keys(items).forEach(key => {
  118. storageObj[key] = items[key];
  119. });
  120.  
  121. this._setStorageObject(storageObj);
  122.  
  123. if (callback) callback();
  124. },
  125.  
  126. remove: function (keys, callback) {
  127. const storageObj = this._getStorageObject();
  128.  
  129. if (typeof keys === 'string') {
  130. delete storageObj[keys];
  131. } else if (Array.isArray(keys)) {
  132. keys.forEach(key => {
  133. delete storageObj[key];
  134. });
  135. }
  136.  
  137. this._setStorageObject(storageObj);
  138.  
  139. if (callback) callback();
  140. },
  141.  
  142. clear: function (callback) {
  143. localStorage.removeItem(this.storageKey);
  144. if (callback) callback();
  145. }
  146. };
  147.  
  148. if(this.GM_registerMenuCommand) {
  149. GM_registerMenuCommand("Don't like tweet on Yeah", function () {
  150. chrome.storage.local.set({
  151. settings: {
  152. dontLike: true
  153. }
  154. });
  155. });
  156. GM_registerMenuCommand("Like tweet on Yeah", function () {
  157. chrome.storage.local.set({
  158. settings: {
  159. dontLike: false
  160. }
  161. });
  162. });
  163. GM_registerMenuCommand("Clear account tokens", function () {
  164. chrome.storage.local.remove(['yeahToken', 'yeahTokens']);
  165. });
  166. GM_registerMenuCommand("Reset popup settings", function () {
  167. chrome.storage.local.remove(['ignorePopup']);
  168. });
  169. }
  170.  
  171. let fontstyle = document.createElement('style');
  172. fontstyle.innerHTML = `
  173. @font-face {
  174. font-family: 'RosettaIcons2';
  175. src: url(data:application/x-font-woff;charset=utf-8;base64,d09GRk9UVE8AAF3UAAsAAAAAedgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAABCAAAWDEAAHCoSX0Qh0ZGVE0AAFk8AAAAGgAAABxtwomYR0RFRgAAWVgAAAAeAAAAIAC+AARPUy8yAABZeAAAAEMAAABgZVhdOGNtYXAAAFm8AAABEQAAAmLzogU8aGVhZAAAWtAAAAAxAAAANghvGzloaGVhAABbBAAAACAAAAAkCaYFU2htdHgAAFskAAABDwAAAiDSbAj/bWF4cAAAXDQAAAAGAAAABgCRUABuYW1lAABcPAAAAYEAAALN6g3E/3Bvc3QAAF3AAAAAEwAAACD/uAAyeJykvAl4FEX6Pz6dSc+U6AYladfddTN4HyjKtaCIoOAFKqIIIki4E5KQhNzJTGZ67pmunvs+cockJMgtAUFADnUJXuB6r+utK7ru+lVqhh42v7emUXe////vef7P8+/KdHVVv3W9VfXW532rOowiN1fBMMzl1ZU1a2trV966fnVlRc2tj68tritfWa1gchSMYk5qtCI1wKQGc1JblKkrcpdcoiwcpbioftFtGP/ycInaeeXZY1eyV5wrZa9kN4++UqG49ErVicuuVNxzZRk3RuGnOSHFaMXlij8qrlWMU0xU3KGYpXhA8ajiScUyxWpFqWKjolFhUNgULkVQkVB0KTYrtiuGFAcVxxUnFacV7ys+UXyt+F5xVnGeyWUuZsYwv2PGMjcw45kpzF3MbGYus4B5iili1jEbmFpGy5gYJ+NlIkwbs4nZwuxinmdeZF5hXmfeZv7KfM6cYX5g0jmKHHVOXg6Xc2XONTk350zImZYzM+f+nEdyFuYszVmVsz6nKqchR59jzRFzAjnxnM6c/pxtOXtyXsg5ljOccyrnvZyPc77K+UfOTzkZpVI5SnmZ8gqlRnm98lblZOV05b3Kh5SPKRcrlyvXKsuVNcpmpVHpUHqUYWWrskc5qNyp3Kc8rHxZ+ZryL8oPlZ8pv1FKuZfm/rauYv19f7r9DvDuv/3222VvguxNlL1JsjdZ9qbI3p9kb6rsTZM9OZcJci4T5FwmyLlMkJNPkJNPkJNPkJNPlCMnypETL0TKmU2Sc5kk5zJJrsskObNJcrpJcrpJcrpJcl0my5WYLKebLKebLKebLKebLKebIlNOkQuaIieYIieYIieYIieYciGBXMIUuYJ/kt9Nu+DJJNPkukyTKafJlHfIBd0hF3SHXNAdckF3yAXdIedyh5zLHXIud8i53JHNZYLcORPkzpkgd84EuXMmyB0wQe6ACXIHTJA7YMJEmURmz4TJU2VPDsksmCCzYILMggly2yfIjZ4wRU4w5UICuaA/yen+JKebKoemysmnyjWbKucydYrsyZlNlTObKucyTU43Tc5lmpx8mpx8mpxc5u4EmbsTZO5OkLk7QebuBJk9E+UhPFHm0kSZSxNlLk2UuTRRHsKTZMpJMuUkmXKSTDlJppz0M+WfZG+q7E2TvWx5k2ReT5J5PUnm9WQ568ly1pPlrKfIkVPkyCk/R06SvcmyN0X2/iR72fKmyCVMkUuYIpcwZYKcTp5VU+RZNUWeVVMmXEg3Tfay9Zwy8fbVlVVN1euLS2rHblhfU7O+ovj/VQJfiFTAxTB2xgFCTWAwIzIuxs14QMD5GD8TYIJMiAmDsIsyMSbOJJgk0wqCr53pYDqZLqab6QEh2Mv0Mf3MZmaAGQSB+CyzldnGbGd2MDtBOO5mnmP2MEPMXmYfCMr9zAHmBeYgc4g5DELzCHOUOcYcZ15iXgYB+mfmBDPMnGReZV4DYfoG8yZzijnNvMX8BQTrO8y7zHvM+8wHzIcgZD9i/sZ8zHzCfMp8BgL3C+ZL5ivma+bvzDcgfL9lvmP+wXzP/JP5Fwji/2F+ZH5izjKESTFp5hwjMRnmPPNvZgQENJOTk6PMyc1hc1QgrFHORTmjci7OuSTnNyC4R+dcmnNZzpic/JyCHE7hoytMrkKteD/n+pzNyqeV53PLcr9mrarLVCbV++oHkeOiKy4aumhkVNnFzMVv/OaG31TnTRx9+eh1lz5+aftlp8ecyg8XXMpdwTVefvXljt8+/NvaKzK/r/7D9it7/6goVGuuHPvbqwPXvHPthGtj1116nef6lhsUNwRuvPxG+83X3Lx3XN64LbcWjj9223KYs09POD3xnom2yczkoelj7yqbMX2G4e5b7/54FjOr/568e6ruzb13xr3bZ8+es/W+8fdtuz/vgZ0P3vHQzXPnzj0xb+68/Q/nPHzDw9semfvIpkcVjwYfJfOXze+a//1jXy947fHtT8x9wvNE38IZC+cvfO/Je548sWjhoncXr1x8+KlXlpQ8vfHpl58eWbpi2VXL2padeuaPz1if+fqZ1PLyoieL3l5x+YrHVjy38upVo1bFVl/371vyMrfkrSAF6SApYHDWz1emw+eu41Z1rUmuiBSFV3iX4tvxhJpZRTOLHppXdFfNGu0qwwrDCluRsAyJt3VP2jtz6O4/z/1g+c7a55qf458z7LPuxV/jz3reHXpn6MSJ/e+j9l3tu0N7QnuCexzPu7+u/GzFOyveeejVmXtRpk66gtvSva11e2R7ZJdvN/4Rf7vijfvfuH9onDBWXGVZza8zrGssri1F5x8htZy427sTCLcltvUMotRAZpAtqy1uKgai1dY1eCy+dWjOG3PeWPWt8CPKm0eOjjB/b9s7ovhq5YOMMKIwLImPKGZWdY8o7pxyNAXeHbf3QETzxyMKfqFWSRaeW8iNHXV+cuYzTodbIs5EM7limBQQ9evxQJstqAvqbI060/iFi6+JXeVqMuiaHCi9oY9zavUGLUZ2VbNbH3e1xn5a/D+3HdEGbW3BeDDe7o2bifqBE1JBs3QFyrzfx7ma9dpmJ5BrcUtYSPIf7Hz/zd2JIBSgpQVoTUsrNz7pfwKl1lg4Z3MLL2ftMiRcSf/RqheX9ukg60A8mICsTW+u2nmXcTqSnpEe4CyV1XXluBxXd1r60IOqFqwP2CO2qDsaCrg9rb34dXws8fzmzbFYItTqdbUnEh0YteOk1tWQtCaMUf3mDYll+HF0b8dGXCiIrM/qMWMTamppaSrMmzeiOMwrRnIMvIIsZIRzoZ+5Jc3LfMx9KT3JenW2phaj3tiEW0JImkEKpbulQrYFwvpQS8jW6o2jL8mT0p3k92xLyJr0JryJeDiJEXkAv0ruZL1xGkrgOO/VoVeB7AHMJmlI69Vam1uMtDvXkFxSkCqACuARxQt1R0dyrKOXwVPzJ3JtpNzp06VczfkCtZT7wXSSW5guUJHcDz4guZp0gZrkTv9Ayi3M1Eo/cjhBCoYLBwcHoUSUUJGChDQZa3BCW6bRLtWWAcvx0nHTpy89gwu1ZYP7BxMJrEmtyazhMj9iLWYx1p7Zn0gkziSGE/vLBrWDGFxi/+Cw9ow2od2v1Y7DGFGenZwFPOs4rCA3MAK5Ofs4ojgxS/Ez9zae38vV15fg+g4kXU+ulwqBZQ6HzWkXbdjudnjKn3e/1v8aksaRhezKlpXu8nYai73YI3rdbkQMxMB6PBDAXsHjdNk7ytyrWlYislAax77W/6pr/waPw23HNtHutDkciOYPpVzPdtTjkvp6lJf65l9M+toflOeCJMRdNWXKVYXnC1Rj8Z/eFwiLxNQ1rMy+C4zMMjdniMNP4aeeOoixhizA0rA0DPwoKd5WvG3dtu3QbnKUHMPSfawerz10EO+JFpIj6t7q7o2FeT/OI8ER5vxVD44wOR/tBY7ckMohIHRIwS9zL5i7uqZn58mHT8zVSAXS1SrcxGu1ZqvdLFiwERvdZh+S9pBVLB+0xHAcx/2xcBARGhMLt4kJc8Ic1uImJF1NoMOvIayUK127prp6jQaGT7YkTG8kOEYgT6ZhJOUfuFBy5q3zQW79hoEtmvwPycIH1HVNrZ0aHPaE/AFEppD5ZLI0n/WbPUbMo7qmprrCByVIrsLDeN68eZQVwcvzD0hAyFoC9hAOo87W1s7C10mBBETz8PDwCUoUUm0ZKC/VSAtfU3e2NtVpMG83WsyIcqZuJGfWm9+OKD4aXwCcKUir3wPG/8yXGefnclhr1GqtNqdVsGEeGz3mwD1vSZfuuy6KpNvVlatrKivKu/sKyT2z1Xps8JlDNn9TFB/HL7W3+vyBQNgbhq7RqH7A5J6TH/8tEml1x60Ja0iLteg9aRQ3nFi2dJl2yTxN3o9kIQkOw8WkC+BOxwZMrWuBk8DLwkxQRZ9hJl6rSQXV5FqSfS5MBVX0mdJoMrsvv2pUZqFUwIE3GbwyuKA/1WWDZYOFJKiCiTeogR6CYFl2WCwktKjhbN/A7KY1oL1EFipJEBhw9ahhCepQcL6AdmtQCnLzpOCwOl1weaZACqZgvhdAKdk4eHvhieZ7bdqSXdOCqSAJ/pyXNlPLacukydLnZWVaLcxlcHBPlA1Kk8nndDLTH/q5aT83qxCXactwGZIssA7No9fwvOHsBQ+aPNB1A+bIluCV6CUyMT2XzBqDSSMsPQugT8u3Fjw5olhHl6LOlQ8SIV/PX6hLfgsv3XN+B2cTrNjutUE/Ybz5xZ1f3Uym2ctailY+jYD2rtvflm4JNrphGtusDqvJ5RRNbmnlV+vINboj0SO7X/wbyY3thann8vncPuwTPDbR6p4fXRGs/5pcFCB/wCL2eTxel1f0Od02hIU3JfavUs7xGV0P4kcRvufeWVLOanTvlWcXc3a4NGvVdo/dU7hD5XV7PJodao/DYytsWckJoir/IO9yu7y2oODETqcD5b/A22wesyggQQ3vBMx7nQHBLbpdLo9PdEOlRBW2i04ggLZ6fdhPo0R1wMC67aIdO7DdYbc6qXgX3iG3SL8jqm/IOOkKovo7wcDCxwn7TepB8nj+O7+wbLQi0yr9nSur7hkc6KG/6nJNWXV1eWH+VJ4saOWuGZXZJwnwftOAJv9ixUDPJqDZWF5evRFITgNJ268kPUByjWJQzqasnOYiZwH10aY6SQfMRX3qMDH8PBOLMtdwIJwt2IalOdIfYOF14HvwfR0LtsOkF8L4FXysY9+2Bfjpnc2HUUAICgEcwREx7JkfWXYYDyOXus3Ybu3AJd01u/A+3O3virSfnP7dIpJjdYacIRzCb7388l8w2o63arurN3v7wr0Jvry+qkrb6+7xd4eaN9bWbmyk7CpJvUe0UL2i1Agp+mU5fuv8Ye5Bl2QnazFZjj9586+f4G/wwRsCBn/k0Pff4E/wG3fgKVhaLtmktQ9m2d6Y+pisgXxE4DRlPkvwL7klzjdw5RuBdxs3DRSmKtQDmzYNDGwCPpI1mQqOcpRyuZC8rf6ZhYWZKdLb9A0wk75Zq4Y+GhiEPgIQQW5KP0ruGwPwYeTfr8BSyEydpcj/WC4MFh+6cl/1yZSzdLnJf+OnTz75ia7bP0355KrCTO75Hzib2WQzwYhxiA6R91girig6QvbegFkztnhsPpvPGXAF0LvkHdYdCQciIO9uUwVwwOa1eC2iyW5GU6Ql5HrMBnEQhqzXjI02kL2D5O4fU6/8xJA5P5E5wIWxPylTr5y7j5NKpXvBlWrwpM13HpiLysrZje4qc30DkkZLo9n1JXjhsyXJ5i5TJ+7CncHWJCKjyaVse3ugz7EJDQyyJ5d8WPEZVKKU3EvugTvkR2h+5yfmSrdJ+SSf3AbuF1/Kh1iIp75mceoBzuhvwbzVYLUYsB5Jghrr/YaI1evwCTDHsV/0ehBZREazIb8/TAeZJWz0o7wfiGok59UF3zIjIyPd74woFvpAAM0NNo8ojp3fCBwvOUuW0Ahd9pWSxNJWrj3s32RoN3Rs9DXw0q0TpkozO6XZ7rpWHSBEr6rLnexwdDWRq18nGnLpaZTa/RbXEN5oNdQbGvwb23ly6ezXJU2TdLWjNlnf5YAUHbg17u7sJLOnkpnSrV+gTKlUJhfSYWiv9tXzWQwtKdz1rdoOEE4u7BLlUpDxrW2nh3eg1Ni3uPpwtdXQYKinZQyv3TbbeK9cgEtwQRooI+HuQDGiWPw/448gbeY+TnBXPit+g9+p//Pq3U3J+mhtwNlb21GFq3BdvWNjrblO39SEVq2qf0SciSdUFztseoO92af1NSeNrRh9oQqsfbZyN96Nn+0L7kCfq7rb2ro0OGILWYCtP54gF6f05NIpZNqYEYUCsLBihFekwvtOjygcvCL/WNqU3s7l7752VP5b143K5GfOcdePyj9ww6j8k+RYqpA7c3qcOnPJNu7GUXmlsXNTGHzuYuW5i8/N5qT2R6VlUhOIkuVXS7c9Il2rX1/ziLA2bA/b+3f072vr37Rzy+HXCfPhp5/HkQgN17waIFcfaf3rM83PmJa5MO7bPhR7t/v5zr2vxDv8iUDX0o3ztMUutG3Pwe4Xhv78ongIBO75TzKfc7WNbd0a8oX6+R079hWSYmhfY21NY2ONRiqWvlDBU21h6lLJR+m6uttauzTkSzIjy4WutsaaQmmG+pl1a5cV5knfp/uIClZVFTlDkJLsPPc77i48u+yppxYvLp0t3iU0eBo9jbBEXXAuE9o2g9016UVJsXdS1/JEVdDkNwWtPofPHgAo64dfwON3+3zBIEq0vfjDgX+i18jVbMee8CZ30J/0tnrbPG2uNrFdeL/89OJDiw7eNzgdptUE6SfOUF5XXdVY1VhiLMXrcFmoqq2yraFHNwArDGsJCmFHRPDS2SJ44ecT97c+B8hjYHfbAbwXDzUOlKOBsvaVwlLRigFVIUczKxhsvN2E8urOlTId50qVKbrmSH/A0uiFkyZNnPiEdCmmoT8clUZ/PumziWT0E7C60r/RRz///LPPjpLREELwe4JcOunziZ9Jo48APcqD6Q81JtMYcpIwZC5hIOc6TuqWqkgV6Qb3iy9VQSzEU1+TPp8LnTMTXLFUDHNpJikGN5NkfQhBvEa6LqPgNnf3bNa8qN5c07OhvKZ6Q+GT6g3d1SB+91w+8Mub8g2/vNlc+I10CYd9Tp/Na/fYXDZYzuyCzWGz37NYGoPh755DNo/NTfUQBC+ddrsNkCe2ItHCRiq3rN1SvWPD4eo38Zv4cM+OzVt6tuyI9MEkdmMXykt/RRjm3JgLbfy1bbStv7ZNyraVdGtSY1IGDv+pcsayh5H0MqnHqTEsyTYWGpf3E3mYqFKTYLyR64k6rSEqJax4b3KgKfpDcPliOIE++lz9n2GcsMaNIWNI5wNFbuJHKghHjWFjmIZB6bYaeSNv0QPwnTZRnQ3DZdFRHOhvgWTGmAW0ws+nqf4rTCTi5/6v1OSHzKRMrcqCLYLFYXFY7TY7kq6QYDR4WRy3xvgwH27JFq+38nBZW6B4MTOGxQY37zP7TAFz2A6CjYo2+MlPLox8Kj8OiIDq3H6314tICVmSqsFsD+4OJZOJRKgL96DUSjXuMXZpk82JmlA1DDbALWN+JGNAxgSVbekEFzYkaR1TB1UJnPSHI+FwICEkxaQ5wUf4SDPlUuagSoubrbyBN5i1YrPQHNCGeXSuYojTR3R+eIW1Fr0BZX7KnJWWkqdZPtLkozygSXR6OzRM0PqbIwZE7JJNwF4bK9ImCMjnYN1OjxPUU8ENs1CW3UHsdwXcAbfP4/ej9DXpa9hIxA91ExL2hC6C/r1B6uMihpilDRj7raoNx/yRSCTsS4hJIWlNGCKGcIu/ESPpW1UjbrFAnQ1WrdAsQksiPMozg3z+7agR5eF1JQweUXpB3U43pKZwnqgn5o1v/4fwj7V/rd9bvmN1PxJdbHx37b6V8RWJZQ0tq5DgYC2iBVvxXDy/bvFalHrxfIIzYIPXGETnN6U3sY1teHMbXJtxWyNKbzq/iW1rxBsaG00mg00Pcvah1Bvc49LYW+8WBN5kaRFaBJ2vJcR7bX6734FS/5bqWJh0IJIA6bkDXiS9KuVwx+uHO7e8gCTrZ6zlsWcfewkUc6mq5FwDk/qEKJXp34EIoshU82+1urynemAzAKrCc2r1QHXPhkJp/jzuCbxQt0i3RL+0ZRlej8sDFQmDTw/QgbeY9RQ6/AugQ0Afseys38M/TwH5UXwkfjj2Qmx/bB+so222VhtKTVfj2lBJYd4KMirdT0aBeB9Fup9NNZBRVAiu56SrsFTw+G23jR+/QOIwDV11XCr4+ravxpOCBeQqTP8Kjn/99VdfHada9lUgBK9aQLjbvh7/lVRwDOjRvwtzJQbfNDTn9OxTRf/ARIEJM/Td6dOnTn03RKA8Bn9XdHrOqdlDN2GJQVhSFN04Z87s2TcVQSp0d+qfnN1tzUoqUJUcNocdFAeHHfQmG8qo1ZiqPHa3zePwOkSBrpNidqi5YNhlAQYWRSRmPTcVVG5EkuQR1gMKjgscNanAzwtD1WOHfFBeEVGlt19Y5o6SBeQYSB5gRAl30yhJlXszvT1ctnadBjcMSGN7JZWnKW5oxUmciHhaNxHVABmL29H2sh2PFL6dMXKr1q9ftXL9s7ufe/bZ53Y/u35lIc0/lRhRLP7p6IjihVF3ySWl/j6iuGh7D8TMX/Zref++Ohck5v3k/qwk/cWX7s9K0qyvIfNS33BVyRJz80Yk3S0l5w2ztV3FuK62rg4Xd9YhopJUbJiP22Iw+4KugMfnpm13w+XyILeX9YcC0WAMhZPsK1/DEnU3xq2bzFs3tv4XJ6j/31x4I/fOD9i6Tiip7kJJH9w5fTpbV0ufO+u24c5O9OGHH9zJdmWfOzvxtroudOcHH37IZp87adraOhjxN32SDjM4HVGmGlNlHGkn7azf7/V7QFCIQRzGbeakIaKPNAcasQkbBbPDZLfYLBYktUntEqW2+C1es8fsNokm3ICbzAaD3mBuxo2Yd5kg2uyxBCyIUGr4sWab3eQw281OE2TXEGiOQuat5nbKHTEAhULRfkRrQVOwfovfFnQE7CEhiNtwMhCNRiKBJAZygUYHbEGLF+U9k7od+JS6lDLp0tSfuXGjUibpKHgZE+j4t8ihW0blna4iCjL245nkNyRELiJXEAVougpyGbmSbBxR1FBwuYtXEEX+3/i0PpXiPAOmDkPMENP6eIwsImsUtU6d0WyzmKx8Y1d9oiZYHaz0luEH8GMVK5aueHrjA/gulH+Ix/M7Fm5dvXXV/o1HcD/u920KIchyU6gn3tWWrO1pGuAHDNtsO+HdUf/BbQe3Pvfn7Ky8ciG5bMIXaMIX0mVHpCux9EcsXbpwAlxPSJfhbMRR6bIvJn4xkVz6BPkjPoj3+fZG0VD0uY7dfZFgMOoC54ibI7aAxWfxmD2AALOz1uZENifFEmX1Zdr1xvXGYttqmOZ43NCcN+e8ueJbfBaf3XPm1Cl06tSZPVCRs/jMilNzTs3ZMw6PxQ80r6qtqa2ptDZAf9WL2gjSRgzdgSHaTsm2lCsH8aVta24zbsIDeHd8R3dfV99gciu0rcuYaIo3BTfiSoRX6NfUVqLaig1NJbBQelI3kYJPyZjPyGVMahSZqUz/K/02B3PYB8tNxB+H6QwrJw/rDSxuIN13qug6yMN6Y6FLYrNfB68MCWsSo0m5/+sVLEXwygKv0jtVkCUsYeEwXeT+O8sZ0vscURJWasTs/8qhOZs5zeF/sMfDutxiVmz99TvO5ofB7IYh7TJjcE6LwwxD2QZzAYQBq4fVkMITH4xpry1gCzj89oAAijAgCQq0A15AEvtOYamNxc2xxmRLsqXLCNXCHaFkLBGLwPgG4d12cA77f0sdjfiSWZiV0EfQU2QJtEBi3XbWTXUzB3Lb/4ca77NNjviT/6vJeXHSRMaSJWQsAJTpyvTDKZZrh+BNmG3/z5nVZm41ROl8b4CV/g84Tm5h3e2WVp7Og1Cj2CA221r4FgOvFRpQs3SntDhLIrbbLpA0iI1iU5bEpHU0IC2QjMNsoywY9OYmEAy/THsKNP6IG6QlEtSiATfLJM0gRhoDTZQkCZIBAc7XSrewQoO3OdISbknybQJyyWsLbW+rj6KUQNzdhpLkTrIY64DY0eDPEif4dgHJC5GLEid90M5wMC7KxFCzNtwqN74VJEw7CDtofBMIO5TZcHkb8OcpWj827587yIwz1Kh5zqdM7QMF0hDWe1swOJuBR5klmdtYgJd6rBN0npawIcLHbHEcF2LeSAgN9p/NXM269QFj0Bw0R+1RAIXzVDEc9QQDwUAg5I6is6mrBypYny1gB5zi8AsAPwGsQbe7/NDx4Yg3BpnFrVFDGGX467kyXBFp6KhrN/ThQbw50tfZ0d7eH6X7If369oaO+mgl3oBwmb6yvqGhvsJQBl1Zmwso2scDdLZQrB73x0OhcCjmg/GEY1aA6UavDusQ1kLfAVJvgcGM8v5CvGQdtLlSeeLc7VxqtvRR6l7yUT0+/zJmz/1hKpdZlcmrx9RlVqXy8PlWFh5xRz3CuL6jsANQTkcH0RJtahizcqiDes/BujOWB6jY+SAsPR3KtDldzvFd+nZdm7atKdEIGreb9bc7O0xtprbmeH2g3l8nNJobzTqtuYGiRQE7sSCirHTD7IP44brFKxetXDm/albl3avnl61urm/R8Txq0rIWu8MKeoHVSbcyzKLFDc5j9dhknOx2UpSMPI7UCEBEN8R4LgAY5KGjK4pjoB4Hg74ojiPsxh4WuymyEfxCREjanxVfiAx7B4JbolvbDgRewPuF5017G3ejTM75Si5uizvieDPuDXUnw8EQrPNZqGkv91cmauLVXQ39AJp7OshSJv1YnzJ9daqJiwKMibfH28K9ngHPFkvSHLR77C6HaBedGGNk387aDlSfXHioqmdNbIkLOYELDnCsDtc76+x1thpjY/MiS8US7RJTta3W2YDs5SymRIJZMJltTbYmz7pwVbgq3pjg/Va/w49RtJxNrBpcuHdVV32PsQ8fxfteGHwh3hPpDHYg4pIsnD6wLrI0ujSyIL4oUlG6ob7SUKnfaKddvtFTGa2MbOio2IIWGRbolhqW6tcZ9GYkCOzqBn6VUCw0ubRuPfLWsqLd5aS8dus8GJ0bo/Jgd9ztcrtcLtGDvF2svxfvaNlmaW2JNYRR3iEy7mzqU6JgcNZTpr8kFo4P6wMt3hYPDFIBFQ2xPVs7d0R3R3cH9uAhTHKf+X72W/eevn6fpMTwd/0zs2ffe+/yG7CUi/Ci0KL2pe1L+5fvXLd9zd6K/Q37Gw4bD+O9eI9/d2xXbEfb1l60dznr0Lt0HnABEP/ozC1swBS2ROwRW8QRoWY+d8Qb9oT9wWBbW7AbgyZo6m5sQ1IXOcDpiutLK6sqK8vqSnTFurWW1XgNXusviRfHSzsr+6p667botiHdVssOvBN/+Ozbh+H6y5YP8Yf47dLDiw4v2jIL34nXWNbqSlDefliUfpeqIb9j4OH38HCFMv1VairnaRc7QTi34057G/r0ABsNBCLeiNsv+qlNUvDZ/cjYzVqjpojBB1qIS2tH46pZQ0KfbInrYtpYcxT1fsN6Eo6oLWoLU9mFasKsxW0DtQuElt1ostltNpsVLfkTaxUB7Yuo1N2MWQr5eGqlwDYBHNzRkk9Ya4QP6r0Wl1W0Yqp7mx2ohmf1diNvAxno0Ht0qHsc28w3t7S06LQtWoMW3byJNXhaBFDDQZM3tyBTDWvTg6yEyJAxYkYfP8XaG5x1sDrUi3WeejRlKdtiNvN23tYi6rx6FKxmPcagKWgJWKOOMEZulaxVII8qq3WI6NseNtwU09HmJvUJPdp4C2vTufR+YEiQj9pQDw8THKY71U2oju9VhQCRh3wAdf3eIDr4MUttVl5quRLoxi7lrIfGZI1l2c1e7BURYGJXQEQHprBeqE3Q7reGaH28dEyLHtElZmvVHWKjIPp9EV9UjDliaOMZNhlOxuKxeDyajCbRN1VZaeOGhCBW2DAOe4K+oA9qEkHBbtYNsh8KhaXCHLZ7bV6LB2X2XM1VRKuS9V11XU2bDP38ZusAFf7+gUh/29DAjh09O3v2xUDPBAQgJBwxe8IRt7xcte+ZrgXhCt8GXI4frl5YXryheHV1kQ7VWWvt1ViHm+mYd2vdOj+I5Nu155YzIzl585eNKL7b3qNMa9KjucffWfzumpca+7XdfCvfag1iEdk9rMMNUgnTfSSH0+50OgQH3ZiiUlngHfW2FU33r5uGGldWryxbZUlYOi39ln7bVsceYKjb4RFEJzg7dSDUBIcD0guCE3IQnHaKqSx6Y3NDSemTj91Vua5xnWktMq0V1wTX9N740Z0fPL6zYlvdoHawudXkpjgApLQoYCqxXdAFXldWtQK1ErnVIMqBycjpYh0+c4LvrT3c/HbgA8/OxLa+gf6t7YPR/ki/t9fd8brERm4xzUKmWaWPN6xDdivrsNPdDmwXaTsdoJzK5iEsj7l3+/48uG8n2rdz8574/vgB56HmY81HS19++jXg4Yd4eP9wevIZBp9JXXtG+cG5OdzSsqVlmvM96qWDZfsLU3NU+wcH92vSPWoILi3MTB7Hpa49PzlzrYr0jOMw1moLpVyVFlZljAcTGpKrgqcyTd77pCDV9QmDP07//RMl0eSWdldtKTx3tWpLd+8WTVqh3lLVW1r47ytVpVVVpZpzt2/kAj6rWZOxqS1Wi7nw/Ksqi8Xv16Rs6oDf5y9MPZg5yYWDRoPBZOI1z6h4UzASCYbCmrx3iOrYqY9TqmxRr32sPJXu5taUla3WZK5Wrxks21lIzuOiOdL1mNXq9Vpt1JDQpIKqRDSSSOijWg28KDoFJOzOwcGdmtTV6l1lg2sKWzKPchYLrcd2lZnWg3xD6xEoTG9XBfxQTxB8Cz8+9snRT459zOBPUq9+TK6GVj6SXshdN23addd9NO1fmtQjqhMHD5448dTBhzXSQWo4r6lta+jWkINQukFLTwNpMo+oFhYNHTkytPeIJtWt+vs7d99668y7b9VkVFlDe01bY7cmpVKRq4ga3FXkKgl86SpN3q5z6teYc2NeVaZvO3cNzLPyYLm/zF/mLsNz8cMtC2oWVC8uWbGiaEXJoprHqx/Xz8NzEZ4bm9v9ePfjWxcPrdhTdKjkeM3xmhMtJ/FJPBw73n28++DWPUNoz56th3qOdR+L0fiTLcM1x6qPlRxesae+o6a9OlmdqExURJD00C3cymZp+oQ1Mx6ykN/dTi6NEebm6PQ75twkPbFhTyVZvOD5mr8kj7BSwcvFIbbnb4dPv37o9YN/G/hSRKmCTISr2bn4Uwm9sODIoVOrDiwfim0/az1iZPeu2b60Z1H3oth8/CjVbBzUNdmaUZ7vzX+c/Efq6n8wqQnfK9OvEz3X29nZp8GkWJpE7pVW9KDtaojp7avrrNLcoKqqq6us6qzr1axTSyuqpUnSvVgqRpUQU5hSpBQcuR2TyyV1t3QzOq9QdZObyeXA3dsBQk/I3MNV1dZVVXZB6vRrqt6uzr7eWsg0T3phRHHzW9oRxakv1zEjCgU9Epawb1WSWefyuLNS7d5ONun3Ax5Hd5FprDnZ4NeakTQu8yr3rVRE7pQuIbdtQvHAWFK7vJ5tNjf4zQk0h9w8m9zMmhP+9mQA7e2QFKROZ2alUdcYpRWQDKW4Yo4opDrIPBHwt1sSkPkdQA/Kghnm7nj8RjrwPwz+QZka/wb3zVkSI2WYPIPIWmkWods1JdJMSSOtLcTSMyQilX0zFi1QNWp1jQ0JXZuG3KtqS8Tb27TxRs2E9M1ckeuphpL1jU21G/BqobK2thIjQVVR292vEV/qPf783qOH3x845eh3dtYIFUJldW2lCFW4eUTBrXxwRNnwlo7B3xMD/VOmuLSXk14jV7Gl8bXVuBhlRtS4KdyUNCHyqTSDDVs7cMSLyGfkVjYZiIAig9Kj1fjZVb0bWlBGITVxONMhGcjFOBVgU69djjNdkmEsTnWzeT+OKFbwih9Ixb9IxQ+MSGYpP0k/wXWLHcFEIp4ItQPQCTsj9qA9aAmb/UhUmwNGv9FjdPNug1Aj1hu1Oq3OVIdrcXWwIaFNaDtM3QIiq17gAqaYJSqi9O8FFpZAbzAUCPnDQkKMWmLmoCnQ4tcL6PzvRdYg6Oxmo8lo4UWtoPfrgmZqWvqOS+g6+G6xW+gIJ+LxeIju9kadIXvYHrYEzEEkqM1hqIfX6OFFg1gj1Ju1cJnqcTWuDdYldIi093BBU8QShzpcIbBxMQJ1CIZ8YRzHUWvMBHUw+AFKnr9CZPVCi90EdbDyoMAbfNk6XCvVcv+fGvt/ZVb6X7WcKWTw6EQoRGD1YovFZDaZrC2AuLQ+PmgMGiO2uADVE9m4EAFoGQz4oHKg4IdNIZS3iBSc+fYMsZ05cAZmyCF6SmnlPwpGGH6WYkRxeO/jSvIFqeUexvjbE6BQ3q/qwh2tni60n3zJevpqeytg/h3F9T+x9s76ZB1ogNepHsF43J8x+hF/nLmbXb5C/8CmJ7qXHCo9itGiPUWHDw8NHS4k16rw1J+KXnr55ZeKfpwKWRhSD3CpBrxesmWuw2yepE9v/5ohl31N3F8rU8dTezhymZpcctN30iXSJTfdJF1SKF22TL2ipGSFRvpGvWJryZ7CVK9qz9ate6jM31OydUVh5veXQ5o3jh59442jT9wP9Oq8s0RBxo8RzyUyT3XlHyaWVD1366j8jvGj0h3XccGgO4QBIdkA/qHHjxx8kZVjhLAjZA6YArzbSE9m2U0m9NSTC46yZj8dFLxgtJtNZrPNKELAYwyY0fGFi59kzSY7L0ICiDEFLDCcRMjKEwigQ0eOP8EGzCF7BIfEsMcfQNJWaTR326g8acaI4k/UOvgkr0jfmPWYC5vR8EjGjCgWfrR3RNFK+2XhJw/SCIhXph9Mv87d/Z6Uj++egaSrpKvYxiZ9vbGWr7FV4Sphg39DrBJ5jWxAF26JtvitPqvfRqHdJqs7u2GDvIIosn5vW6elzxm2hUwBJIiszW1wtwQtHpvX6uXDZhjKfB8ejPT5IoFIKIzoosYODZ3Ge4uGluPZRUVo1iw2WQUQsBz985/se+9hMmbGe0i6NTObwweatmu7rUEzsJFmbYE6WLwb2zdEy72gxQmYtdp0Ta4arPcBSKemfYy9gCzjRq/DY/fakc/CBo0+o9cQrsRlhkqrwWww8nJTlxfNxkVDRXvx6aEh9PbbbHMv328dQNdfz86YgaUx7814dwbJx+++J1e4tTXWEeoKd3t7cS8Imby3K881Sh8S/Rh8rjG/41xj6glO2G7e2ri5cXNNV1nc7XX5RC9yJ91JFlC5T/Sh/BXCoYbnS7eWbi3qeAqGA3S/0+g0OEx27UB9b2VbRVtpaB1GJXi9saqxsrG6Sl/Oe0xu3mVwmUQDfhovqy8rKSupXyYuRoIVgw4kXriLVpcNebRsha6qtnlD8wZDibhOKA6V0rW7p6ZHC7q5MwteAdQiq1p+EBZ1LBtcP7j++boDoDwGXRFX2BV0hT3Rsp6NfY2ot/FZfivejreE+9v62jo2JQaD9gg9iOQMO8P4IB7q3PIs2vJs+/PiYYo7xaz9St6cFC889Sc6u5P9KNEX2CJuQ1Ir+Q0n9Un9LPQRwFT5DgtO9ugaODMob2ZQ66gBxOa2eZF0MbmE5YP2GI4KUX8kEI4Ew74YjuEwSB/00VSWWImFDYTjnii0IGYP8mGTp0U0oPwOrLfozTysVnnPjiiXbu8ZOX9w/jKyfwweURbOUuR3pA6eG83pfVo7b0TSY9IC4pE8ODWRxXFTtMWHMmuf4XSxWqwD5RDXxlpQev75+azfErIHMahkHr8/HsPduhiStn3N5a+QDpGD9/7lFsw2NDc3aKSN6oZkc3shWYVJt9R9IRnVzDyumC9I7TR/V+dJV57+hMz49MynZAbg2E/J7z8hs/5G0I/KtJD6nuN7WzZpux/7dPwJSdFmxhaXWWwR9aIO9CFYCQSzYHGaQV4qGsc//NhkbU3LRr6quci0DC/BS4LLkkXhqtjGRM1Lk79+mCgaUQD7nQEhJkSFOF1ZxJgYEP2uACaKtq9PvPRpoju2KdyLkkPB5/EL+AXT881DKPMbMpUDZRHLzi30L25fEH2sxlEt1OAaXC3WuKMvtR/vPwR1qOWaV+qLTMvLAxWhivCWvUNbD3aiVM51XAVfYSw3m/bqh5qfO1g3VLJlOcockB7g8Dbfs5HB7dqd+l0mZz1o1PW43lXnql9tWqNfpzUMWp/F21Cqah7n6nB1UpOcs9PZsSu4M7o9ESnzrcfFuNi63lC2LrEmujqIpg5z1Kbt1Fa6K8UKXIErhUqHk9oMs86V6HP0Cf0Y9eM+sc/tyhrA0UzpGOcQqHqYVRHFikMNx/Uvdbt7xG7cjXuEbof+sYYFFYuRFCaXcf3h/tBAILg8WpRc+VRn0dbSvdn2JZ+LDgX3Dpj7jf186fKikqfq6CErWCbItL8xqXe7PiHjldmF4vZRqXf6uAmjJLvko9L6ZyLy4EcX6GLkTuW5QLqSy54qCqBpH7HaRBVoWtpmvDGhRRGDNBWztU2NtbWtTV0aMhUb9KxWS98FzCB46eLjhRXio4/o1hgE0PluyfX/KPjHVCv5w9vfMPgb4iJ/SC3+RkkuT7/GzV6wYI4ms0w95/hjpwvTPtXp4y+d0qSWqU8tOD678JbctoSuUYOlzdJKskJ6UToizSFzpFulWzH5kQU82VZItkrruO2bN2/fsWHzurUbNqxdt3nDDo18MqqMXPPfJ6PSBSMKnlfkvyKfi9p77aj8d389F7X3hlHk29RF3MRR0rfH6JmoH/9CEFn3DZlIrpxKJv+cz7/p7U1e8c0I8xh9pBl+CRlu5vI/skZA96fbtcgagidqw+DjpqDBa/DwLh3Mml2gu/JOg91gM5t4neBwOp0O0PBBzXfaQbt3OOAu2JF0vaoUl+hqa2trdGViMRIyS1mxOFrWXdtdM6jbhlHaq9qOB+Ld3d1dsUGQcEJqKZv/D3GbdrC2u7arNF4C2CQ/c4a7Hz/U/8zzzzxffkI8hfI/Ek4PnNj3/L7nX+1/A5/GJyr2L3t+We/D4mxhTtXDy5YtXfZwxWx6uiHFABPyv5QOzOeEt1gxYo2ag+ZAi9cA2MxgozDNrhcMSLz3LsxCnM1sNpssBlEPCFUfMAdg2Y1g9D4W7mVFg0cfBEQbsWXlpJda9H1RIYLEt96jBuSIjxr5PVERxKw9koWdXj0G4flQ/Yjyz4cVDB7J+TSoU6Z+TD/HRUxhU5AP8gHeb0DH32Wjbb4uR4e9g29rjDdHa7wNjnp7vVHfiBbMYOOOqB2qYY2Yoya04nHOEGzxUwNmi5t2QyrTyEbtMQeliJojZsjZGOJDvJ/36xFJpxpYexxewCs+og+izM2ZfZADLPSeFo/OBRWcIVnJGnI9C2UAkSliDBuCKC9VQJ4hSvI6yWE+yPqMMkVSd3LyCpU9UehCYoiVwwhT85kP+BJmBTcVdsgROrGVbv+7PXC56RcVsmXNI/gcXmRNsg6v0wPowuNw2ejBApGehbIKNgHd38RmbfaikLXdO91Ol8Nt9wJoogYyn80j26Ug1oWs7vY3WHrigNrsqQ0POXx2H+ugn2W4kdmtDVjcVDZZMUttmnanQ4Bx6kROJ+ukTw7ksJc8wkJJ+IITUdbSLoj4P2Ps2Alxv76n9niary3r2CwqwFk7PUvxlICRTHMhlHUCKv6zw88CaIBWQYvddvHncrObHLKZzSJAGmpeQ40WFiYQnUhwCXYhCz+cNsGaPdbtFB0oed/PtHYgYun0g2kIRBBjzl40LQsz0Qkz8b42FngsUguvVaBHO+z2C3nbkLmZFRxZeOPIWvdsQAhgyMq6bC7aRU7oBgG9XEyby16wANqp5M8+CwgYbIU3NF6UQ/KzE9nUFx6w9eESWl1aF4dDPlxCT8XJjIJ83Tbkb2KBO5BF1nAoIA/d8wQZlN1T8GTBshsjGHtiITUGu7EovtnKeiwAkWFw+AAso4AZkmS3IByuX+6o6X5WkDlncwELaMvc9FMfB/LZ2ywegZXPtKALR1suOIqxxEJPduyKdKRn7yK1/YrUIu1hszsbbrTtz3R9v2CfFCkkE5BP9csZswsPbvyzc2V/NIVs0nZlY34OuX8O4Z/fbX+EFR0wCxzZ5gj/cYRNoNtUbmoTvUD+a+1pTeUc6T3hZ70Apz3QEhedux4f64YZCejar6a1Edyo8Q2fJztf3aybfl/gllsIjj7Jr1jxQvOzmbg9rzf+yqj/zbgLzzL/kM/N/sI91wXuQdaiBwVaXQFZkLizZnmPcEE02EE0oMzlj3KiCjC81+yjzuI3BywBW9AWsIaoCu4ICyEh5AyKYYTDYtgVos4b9MaD0VAkFIG/QCQY88W9cU/cFccoqPJmD6xCKZRxAQf7n+eZkNvJyk/Zu9BuThjiurgu3OxvDDR6GnADrrfVmxqQTQc6EDiDUW8ymFssOrvO3uLUYkSPDICwoSoFOJg/Dqo4iOicjTRyLjX202OhTh/9AsTusbmzDltwdvqzdA46hAviwu5gqbiiH5blSVdIeeQKEK4wDEAEuUFAwbzD9CyWg6bNCjhL9uCWLTupndn9UIHu2wjZSWmXJyJNR8UNDCSQIDCi7OQWaQ7IS4fcbiHLATp4hJ83SFUXjnr9ug+THVpiNlLeBnFnzfHkVjInO0RcosblYrN9DQNYLf48LihpNh83eyGU99ePiWLb2W1EcYIotsIjUTDiWSV5OrWL24ufi2/ZtGVTz9bIbpQ6porjmJ+advwR+mmaJWYKmUItXlgEM8dUht3V20t7SntWJpbjhfjpupLi4uK6p+FxUeeSbcVbiw/UHQE4sCu3mx5X0Sa04VpcjWv4WmpMqjUCFsd1IW2iOWHswt0I94S6kolEojPUg1HKmDFx/5n9crxSW1pdWl29zrAKSSEVKBM2emiVHgrAOp+eHlqNWmGkkZAqsqpn3ZbqLdXPafdiaE5iS8+Wnp7t0Bx4F6d7zrQ50Wxzor825wnpGu4/KkRPo/5Sn/9oQA3C1b+2AOW5jp/7DT2RGeMVynO3pL7iLLCkG4IGACIwLiUvnk7CrCNqigDsCOgoOBFUUGeb0Ww08waHHk2XwkDFagEZgYZvsuhtesEiWOjWnwv0SZ/ex8fNiBRJY6ZILtaic7V49G590BA2AXKwU7Uo4o0EAGiSyaREmsx6YHb6wNEtrnYc90cjkYgvIbYJsoSTpY7D7/BPBR10BRnDRgPBsDfqpVtpcZS37tn0G4x4TqVMj0v/wElX4nukQl7Hx+8lhfSUVB6+UUJYGo2lXeQ76fAZ598x+ZK8GCS7MRl9I0HSb+jB8L1kDznN4CtzDyjJ5NRPXBFe3VQBV9NqXISLWlf3V/ZX7GoawkN4V2t/f19f6y48hPBQ0y6I71/dWgQD4PHLaaLK/0rU9/83Uf+viSp+TpT3feq3ZAmTHiZLlOeuT1Vzc0/evvsG6LrfkqdVOOIIW8CZoTsBNYoGwSDwdt6MMoXSAdZksVrsJpvJQU1kJjfvNXlMPlPQhlL0LUAnh9+ZdVkV2iuigJvNmhjEiD8Q8If8IXcERxB5mvxWDQO1afOGzRtaV8JYr4rUJJuSTT2GPtyLeyKtcIW6cS/C36/6cu5JYPDZneSJVD55giGT4OEUeUJJfiDNHHnijFo+oySfPcZBZ8ARsPvtPpsfSfOJmzUkQKLSI0BmgxZJC6Uga+HtRmiVwcV7wQVNIWvIGrYDAD8D2gauDlcnm5PNPTydlPmqXtwdTsIV7oEJ0st3w6tkTbgKFIqLye2cLWymFiufwW3AaByB5P9JkhmjqoLZ3wwXX4OrcFW4lmbdzfdSISEtHKcyYN5htBqtJpONt4GaI/KA/w1+E/32N8D6zX5r0B6wwbIDOmPQFfQGPH4f/ZR1IYG3QU9EjIhhZ8QWBvz/cHrCF9/9yOB0rTL1bRpz5mMVh1ZtW7Xtyc7H8GP4ybpVxauKK5aYH0NTs6Ika5+WRYk5aA1ao04QJelLQFxEXXTr2Beg4sKaFRf6rLh49PJAX7ynK9mV7Av3Y9DQ+a7mruZ4daASkdul2VzmZfxWZgPrrg80RQwRQ9xCz1r1q+iMpL0TaHV3QDW3Dg+nCoYHB4eH8fAYfJK8NUwKXsl/JxVKL+SG56nzXx/OFKiLKypK6GdkJf0V2+g3qVs39W/VpIKPqvPfGQYSaeEwJwXJwkxBJpguUOeT1OR53JlxuEwqwJgUsHnS7SdGcs5feSM1qy67aSRHqihQkhQoz9K6ZmmSND4mzfTdsf++N5ag9dJ9rF1PpanXHnDS7xp9FJGQUnIV245bcZubrGuDIXerjtzVQmZK88hoaV0Dyqsd/oDkDkM/5C6FBkBLhrN67YhiJq+A9kFU/t94Ekq/xRXjijKxDAlSgZSP2VJcViaUITEzh11KP6NNvUU/o92vEUkugdfZT2pTC+VPajNvSWs4cm3qLfoR7vAwAX+6WgpmarnUHIico87/np+XyuWkY5mF5Jg678n96SCDyXElKUgf46YvXTpPkwmqp++f90Fh6kfV8PDwB/Rb3+F5++cVZmZn/srh1Fjyg7SMzcu0pqaR2ymzZilGGDfI9ZQm9RJnCxq9RhAL4/E3pJz1BIUQgKGAyWuCuJkg0nkTCPMpEmGdJpfJB+g4YA9i9AOeL73NOloC+qjNTb8XoEY4rzeISDeo3N4gNbzDOLhlmFydKqWl5lxJDd2fHYZSC4BfUok0C1yJVELAJyXgZpGsDyGI1zRm0tlvCI1UNQKAofdZYp4oeo5MfZweMDF7QIXzO2G2oGNkPeuORoIwtb+npzNsXrPXLBrtJvSkdNHHmA3hkNUL7RF4mwnl/bRiJFcxo4qyYUl8JJd5YLsyZYxyAtWUAPQKSBBU9GTBr0EKn2w2qviASiTrHDRW0NAX7C9PQC6wVM+D13orSw8nAOwSVNkX2Wh63sEBlHbZpwhKQLasosbKVEgGVBS7ibLuRNGVrNI5ZdVHzOKtLPZC9PFnRRI0HWx3XwgBGoNUKAu7ABllEbQm2x5ZRwQMBnojQD/qYafb7nG6KBlgV41LDR6llrqIxDb0Wge8/d6+ZF9fsrdtwDtgH7D1atuqELlDqkrNSF/DtvX2JiDeM2jrbextTlZ6K70V1vKGKuj9Tw3nFjE49ZFyMLWbM9BjPgarwakXdKIO+tPg03v1ERNKjcpczIbproQtYos6ErgP90Va21vbo71Cnxi3R+3wwhQxBZH0IjlM4McGw3Rp90Q8USEu9ut7m9qb2irDlfSLHYfBprfxZp7/z/JTB6ECEd7HQ6EGarQVdIIeKqO36g0mlKEV4HmjGSCKzeDQ4kpYqBrbG9v1vWKfEPdEaVHBSBAGNxROK8GawqYwrZY9KsaF/mhvW3tbW2+4DydgYaWHpULmMK1A2YhCBeJCOYdXMGQXuUJJdhIdJ91D7mWrN63EG+HCqzZtRGSHtIvt3fgc7t6EgGQnG4tmj+ImLEl9VKZE0r3SPWLqOXZbxebidRs2FBfmnf2J3Eae/CtDfvNR6t/dylRtqp7D2GPyN8dKNhXtKGlr7jB14efxzuc27Y52hNr8SX/SHcQYhXEsFElGEsEuT4+7zxqzBBxZVcAhnxh0DLD2XQ1HntlT21EaWSki54WRyBpwk9Bob7Q1mrX6Ilv1Cv1KS7290dmMHNV2gbUKZotdZ9N6NgTrAnXRpqjJb/M5vVS0RC8YUekmGyyh099W/RL28tiAjTaLhW66YQOaNV1Nt+XMELZDGPMeHjQ0c8gexuiDWSr6eU8AtLaQLQKLZcgLcMPvDQHYePsDdQSHPNkwNbamr5MsnCioeX95cHV4dXhppChcXVHdDOjWWGdvxs24zlMbqglVJ6v7URG/1LCaX82Xm3gLFQClzcZSXA7jtMXNu3nRmp1SAlVnHCydMh5YRPXZs1su0FupLoplddTnDnu7xAHDgDXOR5qDF4zPZ8hNY8Rz/sz8rvyOn7cpveNH5X+cjpBcul/s4bN7kWZzdhts0YusOcB7jBjgl50aXuwUh8m7kX+dBkDY66TfvlDegvbpAj2c/jyI3E3uZgMBD/2PFjhsp/tz0ibpC2plXkkuIp+TUeSz7MdCF48wj/IKMiprqSVImR4BiXzbV9Llx6Wx2a+FChaMv238+MclTpDGitJVxyTu69u+Hk8KHqcfDo3F5PJjX32N6MdDnECuEsnY7LdDaNp5jps9++YiSYnH4huG5pyec2r5P0TCCESx9x+nTp869f3QWUyU+Nui03PQ6dl7bhQlhSAxK26cPQdJG8mXnEims0LCFufDhnCLrxlgizb7/Z9ZJ2phaa1nxWZvS4QP8zF6ip9MVSVB64mEw2FvTExCwlg2Yfbc+rvD6UMMPtesPNecbucazU2WBhs4e4OjPdKWTLTF2yKdge4GQ4OxzlxnrrXU2lBje22iOlIdrvBV4Cfw4tp1cNU+hR/HZtEsWrDZBXc01XNn15zts7c9fPCpE2atWWtvtjU76fH04uD6RGW8oqu6rxE1Y61b69V6QH8LINlex+KnW5/pL+kv3r3xeZNs7DI5jU4TtSNiW/ZALqjiRvpRossmRlZtKnm2+tnqXdq9QknVmroVuiLdcvNSjPQqc7Y62U0ScW/FC7WHtYe0R8wvYfIYJje89x3J/Y4oD5Hr6ZetfnXWVORGFO+w7fG2RLI12Zpoi7YD0r4o/Wj2m8PU3ek2LhyJBqO+qC/myR7sBslG7bdhA0rdmHmCDYPa5wNl0k03akoNpQ31DfWVLWWCFreAGKewio8g6R1AGa9LaZY36I16i95K7RdluDLa0NHQ0TKIt+CEO0HNJsFIOALl56XnyuV/TiZykMYMstjaYs9+9OnJ5hqAXDPXpBazPEh/Gz0YTSu3JTrY3tHe0Rej/w4o5oR60u9FDYicls6S0+QsC20JZdviSuBB3NfSXt9eHy3DpVjr0Np0Np3JwBtQnp/MJH88mCokM5kXzt2uJPPPTeVmPvroTM35QvXMVx59pzBdqHrnlVfe0aQL1e88+srMQmlvZitn19H/HgWrDNSH37WKPI1fRnu3dx84sK1meSGev1p62rAa0e/n15YP7Ng+MLBjx+bydWs3lK/V5HWRs8TA0JsSbmc5iVfPPbTo5KuHDp8sJDzh1ScPHzr56qJDczUShFQnFx9+aO7iRXMLJZ5SLlr80NzDi09q8rbANDYD4x5Rpv9CPudeffHwa68+efghzfnlqocWLXrowcOLXtXktZHJZ7L/EeXaM0ziA+UZEuRS15Ld6sRgYnCwLKHVSG+pyrRlZZrMGnVZgv6bl7dUv7wjn6fmcGXUKkD/4Qp4iBRIBZkeUOXLysq0QKNJ9WD6T1zobhcQYgzEsG5A7yQQWUNq2QRwXpug/6/iepA215N98k+Z7k69yVX2LcOVleiWW3DqVnZX6ZbVq0tLVxfizK23fMPK7yoq8DP9FYhUSpVsvKXV0obbcKs/HoMYUsn29+N9Ff2I3CUd4FaVla3S3KTGet5IDwbNVLXw4ZjmW/XussFVq0vLVhVK16sfPLLwtcJ0h+q1I0de05Dr1btLB1cX5u0iBSQ3NZnkMvhMOnhGCTduXtnSpZrMQvXSwaXDhemganhw/34K0/eX/Z/arj08iiLbMyQ93SQxKqHZb73SreiurstydwEJfKxGdHFBQQKuLDAkAQ0kIZAE8p7JYzI9j2Qqybw6L2aSTN7hkQSQd0Cj4bGAXGCv17ioPPzY77qyu15dv1s91MTcUz3kgXv9vvvPnSLd0+dUVVfV1Kk6derUD9ClKaQOT9pJMmnHyUzgyeA8fljG32rxt0Sm9ae1x99iGZYl6ejejH8QZnxlvoJ5DHM30w7zewd80KEdHRyhs/wOmNl3wsx+kBxkSkrMdFIyuPX1JaGYHF6MF6PgYSZl9/a+/d27+4ToOeiOkomjjn5JSx1zJ0yZOx0X4pX4NVzYmevfsSM3L4v8hjwBYQm94yfwb/ASrN7FM+h9w8lMbtUCxmq2mxySQ/JYQZHHDjvjQW6H0+VwV3nK3dx8XMaUe8wuE6iHMNqYy61mbs0LzKtvv+B/DubyQvIahEJSiOGOC8Vo3AVzy2w8RXMr8EXYrUABf6S354ignNEe6UnbuDEtbaMYPKPdmNpzRCRt4SovsFx7pBd4qcAbXn4/T3lmnBd8JsSLJkuv3WU0oOzMwL/EM8IU7TU+Ka3nqBjYyh7u6T16uC91kzC8lU1KSdsk4OPTIdFRUTnEUv+hIz2pG4XgITYJCiIEr0/fRHn4z5R3+CjlkT9TXpIQjT23A3a6LBCOjYTX9T4UFlgcOMbv7ErdtQVtQalFOzK5oC5oZFKTy173r/fr+rcNIO7uTO0AOrGnpd9/smww5SCMa/LEXDIeCns0vBK6Vl7/hj1rEPfdTO0apNuWp8tbX/N6bzKn0AzbM/uK9qP9qHdXexdQFCPTd7BmMPckF33gra/xm1e+voI3fTMVfY1nf5N2Ews3U/+BZ38dExwIrIb132W2K6ctIzM7O1P4LZHUp8yMHPUpjk1ITt4gDi9mNxzcfFwIdGuPHzxwQgwsZk8kH0wQhq3DVt4kyR5RCbI1Ho8sBJK1HlkqE4NBVjKZJCF4R1nNZ+S0dHe1tnaLMeSFfdru1hzIPjdDxKumj3F6fojcFSJnisFsEsfPjQDuvAg1Smtn91iUjKycnPujRH/y0dBQ0xCOG/poSIOGhoZw8mk8CPcwXBA4xG9KS9skvhDHqvfgfPbNnrTDtPCHe3oOiUNDLD23KyrPsofTejYJwYzwMslkFFbjJm2I8MGgttbjqRUCz8NdMopkVrCLV+MMP681SnKNqDwEDSLfi2GCBvmGzOLz9D6/3+fzizhZ6/fp8/L0+jwRD5HJfA5F4yA/0eYWNLWK+Em2tampRcA/0bY2FeSIwS7y2oS0otbvhbQGSBuNq6jD8sjI2Xs+YZk3p56+iQtuYi29xly+GwvL85jmlHdW9ixFy1D81pR1XExfhi9dTkNpKN2UoeeAeyp+3zLExVxeilampazl1o37OHu1dpPdhMo4Mvn2HDwZT759G08WYvqo7055HVdpZ3o7TqHeHRSAo7zUrh4/vy/mZVRjl+0y59W2NTW3thU0ZYvDaaSAX772nQtiTLPqWP3O2uXL165dLijydEqeSBMpUCWed/EOBJwTwqvEH36gxOBpYRfvpvEwu5hL4SMZkJ77bhqLDB4DtSl5JS9I1jQtDKke1aYEX+CZRT6J4gnUGdwGxA1/+N00PiAH5GGZfTXwO57MG4Z3UUixHP7nRL6jAo7NI/+uzGNfIfNCJGDe+xaNHwjM1OC/KFvDlF8EPHyJrK8qNhdJpmIoSvADFunl4l2Sq9xld9NDxtVuJ6dMwweYWo+7DjWgBnOd0cNF/3fgeY2yAs8Mg8u/8M9G4HeHtXCLvnhT+fKmBk+jtzBFnmDuoL7k0D3a8EL8CISF9E4eIQshqHfx92hdQ1I7N3CFccuVdVbZWlNGzxGTRDM1etisZqulwlQlccfJOqba6JFqK0L7gTLyOFxu7tQHzNmUywV/RNw/GVSiO/BZvF/zlWIJU/4tcJWfHxGtEgKtfxwlYMvwwzxdaUgevRd1en39p9ovoqvo6s6LulMGH+r0eX3eTuTTcxeW7YutnG2fvTV2+TKTyWijRZx28760nd2NnegIOlLYmdU9IW0ht2frfh2KQ3FbdNu2QlqrkVrW1aIF8FhJ3gs+zN+4QTWJToNP78tCej23IDb2BkO/G/R6PcryGbjrsQtiGb0BvuspJkct9bmhXpk3blyPZWSgqF44DtnjhQJQjQR/fmvwlkaZCVf40QJVvIqpg35O1uH1jN4Lr6HuPVlePYfXk3U/R8z4gQGdtrW5GeRbx7blN+UIuCIYx9+hiDttrQXN2SLRaXPy83PgzmY3F7RSuoDu4HVkPTNetHsF8XH3dxHcGSiFvKBjUCGjeWhDWD86NifUYVppXlAimpe19p5rkQ/y0ns5ZSpJUMcgWo/1eB1jUJvrXhPREkA9ckIHGWj51TLTerSCQMO8l4vDunB47I3rOGwqLM/wIhz+LeiOYTgs5l2cHZjDkwefeopEkwe/ego/KF5C53YfOs7hl8jvY6+/xRrKCiz5iFux7tT5P5w6dV7cg7pK/Dnc51dQFbPx5MrOV6Entk8PrgJlaxWLw+i3MDbmBhbpa8ii6/j38BxNqvHXd3An5jT4I/zANxgm3x8p9bzvQM0pdJTDESuvkMkkfNk8wgqIaAd+9nnilsUJr6xfPxdPWoSfSMf7ODJdey64Bf+WPH0io9PQWtnWcLAFs5i7iB/tRHsd+9zcf7L51oIKWBs+s/gyjjjTsntvd3PW70S0TUqDGqDdaz6Fn4VUn8PpJD3gDf4Jp0/Fb9zCdbdi1gbCA+l88CU8iYlZEIxgDc7C6gLEPbdCtzL+vO5P4sfoD/2VZzlUifB7uO4VJqae3GD1oUjPL9fFr7yg+/jjC/3nYMQ/PzLpxd5pI5NHNi2d6sHxeC8OR9DWn+P4mC/uTlWW8BXd2a3bYTj/WI+KzbBQaWRRu6FtpyvHlW8qKeZ8LOoobM9ycjFfpLi37UCpHC5lUSclwXMm2sqRv9tZS8sWrF2J1iLbGu/m7hWVO6veQCRqiMBaPx2/uA4vwI8i/GtuOAl38UZnaTm1OzxGHmMs1gpY5yKzw+ywcPgxIjAu9byngzrEVMvVHmpYwftYVCvVmBzc3UcsvOQprTZaTTaJYm0SYBk9Rpl6zEAiN6JAUZARFhgHqJdA8FS4LU4uet/fruKZVDe9CnPC44G5+PEw5degbSWgsu1lb1I3ryVoVVwieYYr28pk5KPNCHGPsWjDnreOlDQsH9RdRpfQ+8d2/UHd+Xzt/cRL6DIa7G+4cKR+z9voBKc8wp7RnVglBDvI4zyowqkseg8veVcISuHJ6XsP0kNGB/ekJydvS08WlY7pydvoM6UDLR1o0d3Q+yNx5FlchiOpvQS/dU6Jx5FADDh5us4Sg3PZZbDWEgI67aWBgUuiMpe9tGZgmTCsI5F8QBecO6zT3sYv82ZkqbI6SmrKG2vquds4kqmpb6yuL6svqy4oK+HmkAjGIkk2E0jJ09MtdO1/f9wmNW5VgWk0rmksrqXKAnErGuX7407Id2Lc+/KVfyAuuRbPryRPn2OVjOkrSdM5FhpiMtTef/sstEMF1D6BNsKUsUaAuXXgDWiEBGiE9z4QYa4NNUICbYSE+OEEbSCBBPjhBOyHOPEr+Tnkt7fZ6Fdb725p1SB6DYF2xUZ8VxxOWlSArRYIY3eSBdQWFTqlRYxW4j7V4Drsx7M/C1PaFCePH2CRV9pV5C5yGSpKQdFJJPMZowRTSgkqdZbWmjmcSGIZt1WGmcBJYfiqd7k9FIUKRw+wg8cSXs9tKvBTfe2rBdefAvm0XHsJx50ZmXRgYNLIyIEXJl2bigeu4aZrMRsCqUosj5ewe7M60wUyT5unL8zN9elBpYvS1somUCNnLGItMEWXm8slJLmsMGwRkXHYXOUqbFo1NdBNwSLjclaqe5cUsZmL2Rd8+mneaJJrRTzjE9YFs3i1u9qDPCAlJAJiS3IRkkwcmQJ5Wax2FRmFQrFx18gSbXpnFqzd52hbfY0tLXpfDlSgUMnUjGimNOwa0fz1XG+YclN5m9fhxxlbXamnFEGQbKWcjloZEVOMyuDJVuoslkHctQuBzNhKoaNIDrqPThusmu6SPXwbPzyHoQgANe4aj6PeVsdBllj7GSO7nA22OltdmaeYTuIpykINfltZExZgAlH8yyj5Zatk9Wy+8jLilq+LX0Geok6qJ0+C4P8CP3V+3dkV8JOyeLZG0VzFs6+GKRrldzxdMSbBqg9Wg0fpClLAOdMpbWMa0HroH6XlTacrxE0p8NzXc5iuGGEiy1RqNPj0R3f+485HVOvK4kHNzhWDS9k8r97v9/r8grKUbTH48gQcR37M5xoK80VymnJbVC4+zTYbGvME8iXR8qqKTlaraVtU7mrWb/Dlwnuu4Ac12NekJDbBa2gvnIWfIKcRQ9Us3mqRKqiTg1RldXFkM05kjCbJhPI5MgKDo7usRgWFpoOjp9rp4P7xV3wSMZ1+f6eIJRY1S7JR5qJfvSuHjGMh4Ygemdx4a+mIZqZwDEajWhCM2Z/iXSHuzyLwluFYPqewsWUw8fjr4hoSrUUGU3Gx2VIBAZmQqdoMJUnCsYzJbaGn0utdtbKHw0l4PlNX46tqMDeY5WJUxBGQia8+W/hTCplMMZjPXMO/WoV/dQ2fAb0AxOC5l/Bz1/BAzKkQ0F4ftQLAtQ9Ta0AfhEIIfSIGBpD6BBw/vIG32azlFIDVUm1zhLq0xYkonpgbFFYQgP5P8Awt9V8VSBQIla+lxdcIQjVPu7czK10kS66xHuSyUQQVu2SxcsFE8lM+PatzrxjzCV7ya1ZCFofNXe6p9LicIfnyyLuQxxSSPpcVSeVSuVmyWeBVny0iM7RG2VQr4AiQGX1urr4QZCbwAu6GZm1WSnDzaIvjB4L9fD/Ig6PUVlImlZlBVtwcefg2AUmgIk79o8zVtuoyj63eUcf148cJyIPJZW1w1DnqamCM4fDjCMiMAxYwKhaA5CjlIEsgMyBJ8OQotRabLNRM1qUsxJ9CGUB28NujZRguC19PQqJy6uwF8Qo68LJTcnoOXrmCuPOnqDgREKf1Yqi34C34X6/gX8I/vHm0Z5Dc4OcgUNQKAn+9qRvVE1Y4d/qCCPUSHcjCBZB0cOhLCHhwNN2B4dU8Xd7SZaugLGObfXT5qs8HuQkuG+eAvPhDHFVmTk/grB7nRAdEfB5e4lPM2DdWslnkPE+hx2eRJxhcSZ7kd+Tl7RDISTTzZzDOmUGEVA/WKqPTQrczMAhPk6e21kOPVSbC7+pW/YBAv4BfnQpZreQxIhVqFyozf+jml88O3cKD0G8boWJ/wShmyBjqtb1ED6GX9GK4414Iegi9IgYGkHqFmMhJRBf8Mb8j19/Z0eLv6PDn7RCzaOkoZPGzNRMW803a5pAhIF8MxpGlNEkHRTEeTabWKWaVET94gqcphIkpSBPt7n4KaRxH4tT60/SdkFaAxFlizHtG5aED/PcTqK+m9bz9Lr6MG8fHCWjVxnA8pYpMQyJCTXZbmm1rXjYMzChlVnx8ypdIKEZ5bbZ9tp4m6t6JXP/1vrvZ11q9CyZ+fXADX1ze2CGiJldzc63Kr6z3XPAONuxJ6ZVaUAvq9ezpGyw+b2iQONUhs9noyocW31FeWEzRgg9rAql4YGxMSg2u4MmMJ8ivyGohHr3hS+iWVVyifnTM29uVWLl5d8khVwVFS6hBcqVcxelqUg+jUw1mr81rz2oq3IP6UJOz0bPrnRUfb7pps6uoRtzp48dPC7tRZ0lTfquzRW6ulbIN+Xkl/upmiFqcry/ML+Ki78rT7s7ihYiw0qgpzVERKCoST4u4SA8quVxOufE4DouKwtpISnE0OGUc5e0LpEU9IERoR6KmPjppccykH9P/i0ScFDdp+yRpEtFM0fxI85wmS+PWHNJ8M/mXk1+ZfDHsjbC/hc8N3xJuCHeG7wv/golgHv0+KvJ9mMiR1LPO4DV4RXyI5GjpN/oRsMxSU7sQOQraTp05iIpiPg7UPg7gTsHcyZNC5HgfDI4boyLpMQvH6DEL6q9Mj1lIcqmzxGmsDB2zKEJGO90hv3fMAgJnKbOUUu8GCPS4ZyraWpSTnZ1dtLVyC2cPptADFmnt2W3Ze4r2Iy7gUA9YtLe1tdX3qAcsUpjIyv1F+3LastvU8xWR6tmK/oT+7ZcqP+Qi7R/uvnS8/3j/2NEK3Qld57LKF+0vZi3boNPp1KMVkUloU2Hm9oztBRsrE+2JTZu6t3dtP1xwlG5eN+2Gj/dw5THOfsxwZPvu7buTGzdAkh+S5FFBFiO/j7v3fVw+MRIZbSa682xSDRiDWiMyUeA5t02mY84gS5fsbvjIDgrIP6itRbIN2GaHCVY7o6nNJutYaicw/6+px3bq96eN7tQfONRxiIucuFevbtV7a701rdXtji5L/ehWPTe+V299u+D9hMM5/q21b1KPbWQXQhA/TCnSh7brzYaiJFt2Usmb5jxb4fe264vubdfr602cK7RfH2lyp8vJdM+8PknemZWtzzFmG/PLC5Ae5VXn1GbX7vRld3JJJl1J8tieuTZdX5aKMuxFlSXVxipjpVl1PakIbZmHfIHveQAHCif6/Fbe2z6vUrfQ3er2+e6x7fMfbKR/dmf4X9ro/6GJRluoro6apjpAclVzFLdgAQNRjeo2vdFj5q4voEhA98xPHjkUlyutw/MRQ805rdTME6lHJS4KNmutt3srvc5dtTW1NfUun+qSRx1/3UV2A1dpMJfQTfAiC0hw5JjpXLXFU9N5hhg5wZ4+TqTQFsLTWkNJnVcMaFmKtSH8XQW9AJ6r0FZaypF0UOAWhYxDItlAjU2tAl6EMNCZutJGm9flc9V4USMXeJ5FjWVevQvk7gf0vlG1T4zcmEq1jQkaR+T/AB8L9J4AAAB4nGNgYGBkAIKTnfmGIPqSTK8xjAYAPGUFUAAAeJxjYGRgYOADYgkGEGBiYATCCUDMAuYxAAAKegDDAAB4nGNgZt7GOIGBlYGDaSZTGgMDgw+EZkxjMGJ4COQDpbCDUO9wPwYHBoWvCswH/h9gOMvSC+QBNcIVKAAhIwBc6gxIAHiczdA7S0NRDAfwnD6Uy22Tw0WQgkNBRRwFh+IgFwd1cCgixYKDDkInKQ6KCOLi0M21H8DJb9LBTbdSxdemiadnqA+uafsBXDoYSMJ/+QUCAGkY9hQYnWB2NJlBzphY9wbEkIUcFF8CDjjiSZ7jeS5xzKtc4Srvc4ObkpFAIpmRkqzLpmxJVfakLsdyIg1pfmRd4KJuyo/7gi8miap9DdjyBM+qtqDaCpd5W7ULvhSjmh1oa1JWrSK7qh2qdq4aOHC2Cx687WvJQ6Fn0/RD3/ROb/RK99ShO7qlFl3TFS3TEi3SNFlK4Rd+Yg8dPuMTPmIbb7CWP8jXcqfhWXgU1ocfGF2ZMfiTHPHJf1e/4rd1hwAAAHicY2BkYGAA4plrngnH89t8ZeBmYQCBSzLvL8Lo//++HWGVYL4G5HIwMIFEAXMxDgIAAAB4nGNgZGBg6f1/gIGBVeL/v/+/WSUYgCIooB4ApE0HA3icbdC/L0RBEMDx2ZmVkCiukSjk/Auil+tVGv4AVBqFQifnL6AjcrlWISEkEhKR05FQ+A/k0GgUOuGe79vZJ8+Pl3zy5s3s7M4+ERFFTIrC1eJQewfylT89zs5ErPA1ts7eM2nd2Df1PtumdlHfJ+cNK3hAB/v4RB+jOMYtrun/wFzue/Kc9dAlv4oBseZZlvg+JGZ/feX7EU00fN5q5opSHxon/4I9rEmIzGucGd+pM59u+Ay2yb0XRcIWcXkPztTZ7MrPTPuXcz7/uO+0HZE7x4E0Ur3nPdZK9QXlXnEHu+Quf/+vMI8J4radyDLvSjsT7bp4l885LRWDsiZvchNGwqTch6kwLP8+X1foT/kAAABQAACRAAB4nH2RzUrDQBSFT/onXSi4EMRVEHEhdJimrf1zV+hSioJ0JaQhTULTpCTTYt/GJ/BBfAffxIVn0kHaIiZM+ObMvffcOwFwhg9Y2D3nuDFs4QRjwyXyq+Eymvg0XMGpdWm4igfr3XANF6U6I61KnbvrIkuzRa97wyXyo+EyZpgbruAK34areLNuDddwZ33BQ4oVtsgQIUAIBRtLcs43QkINXrraZlEQKnsZ5XmUUMqYlcNntILLbZr7ShGeqAVYI6aacesH69gljBmfMHaKET3coq5PpzYEZLEGRzUbdPeKrJx8WHeADhy00ONq8+ugS4s0UdNR6CaBb7eFFHJgumpEXprkDdPLoOO0eq12z+keT7HviINc4IVRWXEj+tzmH9M963eICZ6paN5XQ0YqU2/zmyHYqT5d0m/BmjpmTjVm5RnnEJxMLz2Zgz6N/SyP0sRuCinlcPJsS7nDMFVsbaMPRFcOl+7CT9VcxNHMER3R6bWc/n8DHl8p/rws/AA4On9RAAAAeJxjYGYAg/9bGYwYsAAALMIB6gA=);
  176. }
  177. `;
  178. document.head.appendChild(fontstyle);
  179.  
  180. let YEAH_images = {};
  181. YEAH_images['yeah_on32.png'] = 'data:image/png;charset=utf-8;base64,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'
  182. YEAH_images['yeah_off32.png'] = 'data:image/png;charset=utf-8;base64,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'
  183. YEAH_images['loading.svg'] = 'data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiBub25lOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyBhbmltYXRpb24tcGxheS1zdGF0ZTogcnVubmluZzsgYW5pbWF0aW9uLWRlbGF5OiAwczsiIHdpZHRoPSIyMDBweCIgaGVpZ2h0PSIyMDBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjY2FjYWNhIiBzdHJva2Utd2lkdGg9IjgiIHI9IjM1IiBzdHJva2UtZGFzaGFycmF5PSIxNjQuOTMzNjE0MzEzNDY0MTUgNTYuOTc3ODcxNDM3ODIxMzgiIHN0eWxlPSJhbmltYXRpb24tcGxheS1zdGF0ZTogcnVubmluZzsgYW5pbWF0aW9uLWRlbGF5OiAwczsiPgogIDxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgZHVyPSIwLjgxOTY3MjEzMTE0NzU0MXMiIHZhbHVlcz0iMCA1MCA1MDszNjAgNTAgNTAiIGtleVRpbWVzPSIwOzEiIHN0eWxlPSJhbmltYXRpb24tcGxheS1zdGF0ZTogcnVubmluZzsgYW5pbWF0aW9uLWRlbGF5OiAwczsiPjwvYW5pbWF0ZVRyYW5zZm9ybT4KPC9jaXJjbGU+CjwhLS0gW2xkaW9dIGdlbmVyYXRlZCBieSBodHRwczovL2xvYWRpbmcuaW8vIC0tPjwvc3ZnPg=='
  184.  
  185. // scripts/purify.min.js
  186. /*! @license DOMPurify 3.1.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.3/LICENSE */
  187. !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=S(Array.prototype.forEach),m=S(Array.prototype.pop),p=S(Array.prototype.push),f=S(String.prototype.toLowerCase),d=S(String.prototype.toString),h=S(String.prototype.match),g=S(String.prototype.replace),_=S(String.prototype.indexOf),T=S(String.prototype.trim),y=S(Object.prototype.hasOwnProperty),E=S(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(N,t)});var N;const b=S(Number.isNaN);function S(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function R(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function w(e){for(let t=0;t<e.length;t++){y(e,t)||(e[t]=null)}return e}function C(t){const n=l(null);for(const[o,r]of e(t)){y(t,o)&&(Array.isArray(r)?n[o]=w(r):r&&"object"==typeof r&&r.constructor===Object?n[o]=C(r):n[o]=r)}return n}function v(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return S(n.get);if("function"==typeof n.value)return S(n.value)}e=o(e)}return function(){return null}}const L=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),D=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),O=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),x=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),k=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),M=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),I=i(["#text"]),U=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),P=i(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),F=i(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),H=i(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),z=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),B=a(/<%[\w\W]*|[\w\W]*%>/gm),W=a(/\${[\w\W]*}/gm),G=a(/^data-[\-\w.\u00B7-\uFFFF]/),Y=a(/^aria-[\-\w]+$/),j=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),X=a(/^(?:\w+script|data):/i),q=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),$=a(/^html$/i),K=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var V=Object.freeze({__proto__:null,MUSTACHE_EXPR:z,ERB_EXPR:B,TMPLIT_EXPR:W,DATA_ATTR:G,ARIA_ATTR:Y,IS_ALLOWED_URI:j,IS_SCRIPT_OR_DATA:X,ATTR_WHITESPACE:q,DOCTYPE_NAME:$,CUSTOM_ELEMENT:K});const Z=1,J=3,Q=7,ee=8,te=9,ne=function(){return"undefined"==typeof window?null:window},oe=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ne();const o=e=>t(e);if(o.version="3.1.3",o.removed=[],!n||!n.document||n.document.nodeType!==te)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:S,Element:w,NodeFilter:z,NamedNodeMap:B=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:W,DOMParser:G,trustedTypes:Y}=n,X=w.prototype,q=v(X,"cloneNode"),K=v(X,"nextSibling"),re=v(X,"childNodes"),ie=v(X,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ae,le="";const{implementation:ce,createNodeIterator:se,createDocumentFragment:ue,getElementsByTagName:me}=r,{importNode:pe}=a;let fe={};o.isSupported="function"==typeof e&&"function"==typeof ie&&ce&&void 0!==ce.createHTMLDocument;const{MUSTACHE_EXPR:de,ERB_EXPR:he,TMPLIT_EXPR:ge,DATA_ATTR:_e,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:Ae}=V;let{IS_ALLOWED_URI:Ne}=V,be=null;const Se=R({},[...L,...D,...O,...k,...I]);let Re=null;const we=R({},[...U,...P,...F,...H]);let Ce=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,De=!0,Oe=!0,xe=!1,ke=!0,Me=!1,Ie=!0,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!1,We=!0,Ge=!1;const Ye="user-content-";let je=!0,Xe=!1,qe={},$e=null;const Ke=R({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=R({},["audio","video","img","source","image","track"]);let Je=null;const Qe=R({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=R({},[et,tt,nt],d);let lt=null;const ct=["application/xhtml+xml","text/html"],st="text/html";let ut=null,mt=null;const pt=255,ft=r.createElement("form"),dt=function(e){return e instanceof RegExp||e instanceof Function},ht=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!mt||mt!==e){if(e&&"object"==typeof e||(e={}),e=C(e),lt=-1===ct.indexOf(e.PARSER_MEDIA_TYPE)?st:e.PARSER_MEDIA_TYPE,ut="application/xhtml+xml"===lt?d:f,be=y(e,"ALLOWED_TAGS")?R({},e.ALLOWED_TAGS,ut):Se,Re=y(e,"ALLOWED_ATTR")?R({},e.ALLOWED_ATTR,ut):we,it=y(e,"ALLOWED_NAMESPACES")?R({},e.ALLOWED_NAMESPACES,d):at,Je=y(e,"ADD_URI_SAFE_ATTR")?R(C(Qe),e.ADD_URI_SAFE_ATTR,ut):Qe,Ve=y(e,"ADD_DATA_URI_TAGS")?R(C(Ze),e.ADD_DATA_URI_TAGS,ut):Ze,$e=y(e,"FORBID_CONTENTS")?R({},e.FORBID_CONTENTS,ut):Ke,ve=y(e,"FORBID_TAGS")?R({},e.FORBID_TAGS,ut):{},Le=y(e,"FORBID_ATTR")?R({},e.FORBID_ATTR,ut):{},qe=!!y(e,"USE_PROFILES")&&e.USE_PROFILES,De=!1!==e.ALLOW_ARIA_ATTR,Oe=!1!==e.ALLOW_DATA_ATTR,xe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,ke=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Me=e.SAFE_FOR_TEMPLATES||!1,Ie=!1!==e.SAFE_FOR_XML,Ue=e.WHOLE_DOCUMENT||!1,He=e.RETURN_DOM||!1,ze=e.RETURN_DOM_FRAGMENT||!1,Be=e.RETURN_TRUSTED_TYPE||!1,Fe=e.FORCE_BODY||!1,We=!1!==e.SANITIZE_DOM,Ge=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,Ne=e.ALLOWED_URI_REGEXP||j,ot=e.NAMESPACE||nt,Ce=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&dt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ce.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&dt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ce.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ce.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Me&&(Oe=!1),ze&&(He=!0),qe&&(be=R({},I),Re=[],!0===qe.html&&(R(be,L),R(Re,U)),!0===qe.svg&&(R(be,D),R(Re,P),R(Re,H)),!0===qe.svgFilters&&(R(be,O),R(Re,P),R(Re,H)),!0===qe.mathMl&&(R(be,k),R(Re,F),R(Re,H))),e.ADD_TAGS&&(be===Se&&(be=C(be)),R(be,e.ADD_TAGS,ut)),e.ADD_ATTR&&(Re===we&&(Re=C(Re)),R(Re,e.ADD_ATTR,ut)),e.ADD_URI_SAFE_ATTR&&R(Je,e.ADD_URI_SAFE_ATTR,ut),e.FORBID_CONTENTS&&($e===Ke&&($e=C($e)),R($e,e.FORBID_CONTENTS,ut)),je&&(be["#text"]=!0),Ue&&R(be,["html","head","body"]),be.table&&(R(be,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ae=e.TRUSTED_TYPES_POLICY,le=ae.createHTML("")}else void 0===ae&&(ae=oe(Y,c)),null!==ae&&"string"==typeof le&&(le=ae.createHTML(""));i&&i(e),mt=e}},gt=R({},["mi","mo","mn","ms","mtext"]),_t=R({},["foreignobject","annotation-xml"]),Tt=R({},["title","style","font","a","script"]),yt=R({},[...D,...O,...x]),Et=R({},[...k,...M]),At=function(e){let t=ie(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||gt[o]):Boolean(yt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&_t[o]:Boolean(Et[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!_t[o])&&(!(t.namespaceURI===et&&!gt[o])&&(!Et[n]&&(Tt[n]||!yt[n]))):!("application/xhtml+xml"!==lt||!it[e.namespaceURI]))},Nt=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},bt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Re[e])if(He||ze)try{Nt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},St=function(e){let t=null,n=null;if(Fe)e="<remove></remove>"+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===lt&&ot===nt&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=ae?ae.createHTML(e):e;if(ot===nt)try{t=(new G).parseFromString(o,lt)}catch(e){}if(!t||!t.documentElement){t=ce.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?le:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?me.call(t,Ue?"html":"body")[0]:Ue?t.documentElement:i},Rt=function(e){return se.call(e.ownerDocument||e,e,z.SHOW_ELEMENT|z.SHOW_COMMENT|z.SHOW_TEXT|z.SHOW_PROCESSING_INSTRUCTION|z.SHOW_CDATA_SECTION,null)},wt=function(e){return e instanceof W&&(void 0!==e.__depth&&"number"!=typeof e.__depth||void 0!==e.__removalCount&&"number"!=typeof e.__removalCount||"string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof B)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Ct=function(e){return"function"==typeof S&&e instanceof S},vt=function(e,t,n){fe[e]&&u(fe[e],(e=>{e.call(o,t,n,mt)}))},Lt=function(e){let t=null;if(vt("beforeSanitizeElements",e,null),wt(e))return Nt(e),!0;const n=ut(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:n,allowedTags:be}),e.hasChildNodes()&&!Ct(e.firstElementChild)&&E(/<[/\w]/g,e.innerHTML)&&E(/<[/\w]/g,e.textContent))return Nt(e),!0;if(e.nodeType===Q)return Nt(e),!0;if(Ie&&e.nodeType===ee&&E(/<[/\w]/g,e.data))return Nt(e),!0;if(!be[n]||ve[n]){if(!ve[n]&&Ot(n)){if(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,n))return!1;if(Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ie(e)||e.parentNode,n=re(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=q(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,K(e))}}}return Nt(e),!0}return e instanceof w&&!At(e)?(Nt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!E(/<\/no(script|embed|frames)/i,e.innerHTML)?(Me&&e.nodeType===J&&(t=e.textContent,u([de,he,ge],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),vt("afterSanitizeElements",e,null),!1):(Nt(e),!0)},Dt=function(e,t,n){if(We&&("id"===t||"name"===t)&&(n in r||n in ft||"__depth"===n||"__removalCount"===n))return!1;if(Oe&&!Le[t]&&E(_e,t));else if(De&&E(Te,t));else if(!Re[t]||Le[t]){if(!(Ot(e)&&(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,e)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(e))&&(Ce.attributeNameCheck instanceof RegExp&&E(Ce.attributeNameCheck,t)||Ce.attributeNameCheck instanceof Function&&Ce.attributeNameCheck(t))||"is"===t&&Ce.allowCustomizedBuiltInElements&&(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,n)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))))return!1}else if(Je[t]);else if(E(Ne,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==_(n,"data:")||!Ve[e]){if(xe&&!E(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},Ot=function(e){return"annotation-xml"!==e&&h(e,Ae)},xt=function(e){vt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Re};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=ut(a);let p="value"===a?c:T(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,vt("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(bt(a,e),!n.keepAttr)continue;if(!ke&&E(/\/>/i,p)){bt(a,e);continue}if(Ie&&E(/((--!?|])>)|<\/(style|title)/i,p)){bt(a,e);continue}Me&&u([de,he,ge],(e=>{p=g(p,e," ")}));const f=ut(e.nodeName);if(Dt(f,s,p)){if(!Ge||"id"!==s&&"name"!==s||(bt(a,e),p=Ye+p),ae&&"object"==typeof Y&&"function"==typeof Y.getAttributeType)if(l);else switch(Y.getAttributeType(f,s)){case"TrustedHTML":p=ae.createHTML(p);break;case"TrustedScriptURL":p=ae.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),wt(e)?Nt(e):m(o.removed)}catch(e){}}}vt("afterSanitizeAttributes",e,null)},kt=function e(t){let n=null;const o=Rt(t);for(vt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();){if(vt("uponSanitizeShadowNode",n,null),Lt(n))continue;const t=ie(n);n.nodeType===Z&&(t&&t.__depth?n.__depth=(n.__removalCount||0)+t.__depth+1:n.__depth=1),(n.__depth>=pt||n.__depth<0||b(n.__depth))&&Nt(n),n.content instanceof s&&(n.content.__depth=n.__depth,e(n.content)),xt(n)}vt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Ct(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||ht(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=ut(e.nodeName);if(!be[t]||ve[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof S)n=St("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===Z&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!He&&!Me&&!Ue&&-1===e.indexOf("<"))return ae&&Be?ae.createHTML(e):e;if(n=St(e),!n)return He?null:Be?le:""}n&&Fe&&Nt(n.firstChild);const c=Rt(Xe?e:n);for(;i=c.nextNode();){if(Lt(i))continue;const e=ie(i);i.nodeType===Z&&(e&&e.__depth?i.__depth=(i.__removalCount||0)+e.__depth+1:i.__depth=1),(i.__depth>=pt||i.__depth<0||b(i.__depth))&&Nt(i),i.content instanceof s&&(i.content.__depth=i.__depth,kt(i.content)),xt(i)}if(Xe)return e;if(He){if(ze)for(l=ue.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Re.shadowroot||Re.shadowrootmode)&&(l=pe.call(a,l,!0)),l}let m=Ue?n.outerHTML:n.innerHTML;return Ue&&be["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&E($,n.ownerDocument.doctype.name)&&(m="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+m),Me&&u([de,he,ge],(e=>{m=g(m,e," ")})),ae&&Be?ae.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ht(e),Pe=!0},o.clearConfig=function(){mt=null,Pe=!1},o.isValidAttribute=function(e,t,n){mt||ht({});const o=ut(e),r=ut(t);return Dt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(fe[e]=fe[e]||[],p(fe[e],t))},o.removeHook=function(e){if(fe[e])return m(fe[e])},o.removeHooks=function(e){fe[e]&&(fe[e]=[])},o.removeAllHooks=function(){fe={}},o}();return re}));
  188. //# sourceMappingURL=purify.min.js.map
  189.  
  190. // scripts/api.js
  191. const publicToken = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
  192. function getCsrf() {
  193. let csrf = document.cookie.match(/(?:^|;\s*)ct0=([0-9a-f]+)\s*(?:;|$)/);
  194. return csrf ? csrf[1] : "";
  195. }
  196.  
  197. function debugLog(...args) {
  198. if(typeof vars === "object" && vars.developerMode) {
  199. if(args[0] === 'notifications.get' && !document.querySelector('.notifications-modal') && !location.pathname.startsWith('/notifications')) return;
  200. if(vars.extensiveLogging) {
  201. console.trace(...args);
  202. } else {
  203. console.log(...args, new Error().stack.split("\n")[2].trim()); // genius
  204. }
  205. }
  206. }
  207.  
  208. // extract full text and url entities from "note_tweet"
  209. function parseNoteTweet(result) {
  210. let text, entities;
  211. if(result.note_tweet.note_tweet_results.result) {
  212. text = result.note_tweet.note_tweet_results.result.text;
  213. entities = result.note_tweet.note_tweet_results.result.entity_set;
  214. if(result.note_tweet.note_tweet_results.result.richtext?.richtext_tags.length) {
  215. entities.richtext = result.note_tweet.note_tweet_results.result.richtext.richtext_tags // logically, richtext is an entity, right?
  216. }
  217. } else {
  218. text = result.note_tweet.note_tweet_results.text;
  219. entities = result.note_tweet.note_tweet_results.entity_set;
  220. }
  221. return {text, entities};
  222. }
  223.  
  224.  
  225. // transform ugly useless twitter api reply to usable legacy tweet
  226. function parseTweet(res) {
  227. if(typeof res !== "object") return;
  228. if(res.limitedActionResults) {
  229. let limitation = res.limitedActionResults.limited_actions.find(l => l.action === "Reply");
  230. if(limitation) {
  231. res.tweet.legacy.limited_actions_text = limitation.prompt ? limitation.prompt.subtext.text : LOC.limited_tweet.message;
  232. }
  233. res = res.tweet;
  234. }
  235. if(!res.legacy && res.tweet) res = res.tweet;
  236. let tweet = res.legacy;
  237. if(!res.core) return;
  238. tweet.user = res.core.user_results.result.legacy;
  239. tweet.user.id_str = tweet.user_id_str;
  240. if(res.core.user_results.result.is_blue_verified && !res.core.user_results.result.legacy.verified_type) {
  241. tweet.user.verified = true;
  242. tweet.user.verified_type = "Blue";
  243. }
  244. if(tweet.retweeted_status_result) {
  245. let result = tweet.retweeted_status_result.result;
  246. if(result.limitedActionResults && result.tweet && result.tweet.legacy) {
  247. let limitation = result.limitedActionResults.limited_actions.find(l => l.action === "Reply");
  248. if(limitation) {
  249. result.tweet.legacy.limited_actions_text = limitation.prompt ? limitation.prompt.subtext.text : LOC.limited_tweet.message;
  250. }
  251. }
  252. if(result.tweet) result = result.tweet;
  253. if(
  254. result.quoted_status_result &&
  255. result.quoted_status_result.result &&
  256. result.quoted_status_result.result.legacy &&
  257. result.quoted_status_result.result.core &&
  258. result.quoted_status_result.result.core.user_results.result.legacy
  259. ) {
  260. result.legacy.quoted_status = result.quoted_status_result.result.legacy;
  261. if(result.legacy.quoted_status) {
  262. result.legacy.quoted_status.user = result.quoted_status_result.result.core.user_results.result.legacy;
  263. result.legacy.quoted_status.user.id_str = result.legacy.quoted_status.user_id_str;
  264. if(result.quoted_status_result.result.core.user_results.result.is_blue_verified && !result.quoted_status_result.result.core.user_results.result.legacy.verified_type) {
  265. result.legacy.quoted_status.user.verified = true;
  266. result.legacy.quoted_status.user.verified_type = "Blue";
  267. }
  268. tweetStorage[result.legacy.quoted_status.id_str] = result.legacy.quoted_status;
  269. tweetStorage[result.legacy.quoted_status.id_str].cacheDate = Date.now();
  270. userStorage[result.legacy.quoted_status.user.id_str] = result.legacy.quoted_status.user;
  271. userStorage[result.legacy.quoted_status.user.id_str].cacheDate = Date.now();
  272. } else {
  273. console.warn("No retweeted quoted status", result);
  274. }
  275. } else if(
  276. result.quoted_status_result &&
  277. result.quoted_status_result.result &&
  278. result.quoted_status_result.result.tweet &&
  279. result.quoted_status_result.result.tweet.legacy &&
  280. result.quoted_status_result.result.tweet.core &&
  281. result.quoted_status_result.result.tweet.core.user_results.result.legacy
  282. ) {
  283. result.legacy.quoted_status = result.quoted_status_result.result.tweet.legacy;
  284. if(result.legacy.quoted_status) {
  285. result.legacy.quoted_status.user = result.quoted_status_result.result.tweet.core.user_results.result.legacy;
  286. result.legacy.quoted_status.user.id_str = result.legacy.quoted_status.user_id_str;
  287. if(result.quoted_status_result.result.tweet.core.user_results.result.is_blue_verified && !result.core.user_results.result.verified_type) {
  288. result.legacy.quoted_status.user.verified = true;
  289. result.legacy.quoted_status.user.verified_type = "Blue";
  290. }
  291. tweetStorage[result.legacy.quoted_status.id_str] = result.legacy.quoted_status;
  292. tweetStorage[result.legacy.quoted_status.id_str].cacheDate = Date.now();
  293. userStorage[result.legacy.quoted_status.user.id_str] = result.legacy.quoted_status.user;
  294. userStorage[result.legacy.quoted_status.user.id_str].cacheDate = Date.now();
  295. } else {
  296. console.warn("No retweeted quoted status", result);
  297. }
  298. }
  299. tweet.retweeted_status = result.legacy;
  300. if(tweet.retweeted_status && result.core.user_results.result.legacy) {
  301. tweet.retweeted_status.user = result.core.user_results.result.legacy;
  302. tweet.retweeted_status.user.id_str = tweet.retweeted_status.user_id_str;
  303. if(result.core.user_results.result.is_blue_verified && !result.core.user_results.result.legacy.verified_type) {
  304. tweet.retweeted_status.user.verified = true;
  305. tweet.retweeted_status.user.verified_type = "Blue";
  306. }
  307. tweet.retweeted_status.ext = {};
  308. if(result.views) {
  309. tweet.retweeted_status.ext.views = {r: {ok: {count: +result.views.count}}};
  310. }
  311. tweet.retweeted_status.res = res;
  312. if(res.card && res.card.legacy && res.card.legacy.binding_values) {
  313. tweet.retweeted_status.card = res.card.legacy;
  314. }
  315. tweetStorage[tweet.retweeted_status.id_str] = tweet.retweeted_status;
  316. tweetStorage[tweet.retweeted_status.id_str].cacheDate = Date.now();
  317. userStorage[tweet.retweeted_status.user.id_str] = tweet.retweeted_status.user;
  318. userStorage[tweet.retweeted_status.user.id_str].cacheDate = Date.now();
  319. } else {
  320. console.warn("No retweeted status", result);
  321. }
  322. if(result.note_tweet && result.note_tweet.note_tweet_results) {
  323. let note = parseNoteTweet(result);
  324. tweet.retweeted_status.full_text = note.text;
  325. tweet.retweeted_status.entities = note.entities;
  326. tweet.retweeted_status.display_text_range = undefined; // no text range for long tweets
  327. }
  328. }
  329.  
  330. if(res.quoted_status_result) {
  331. tweet.quoted_status_result = res.quoted_status_result;
  332. }
  333. if(res.note_tweet && res.note_tweet.note_tweet_results) {
  334. let note = parseNoteTweet(res);
  335. tweet.full_text = note.text;
  336. tweet.entities = note.entities;
  337. tweet.display_text_range = undefined; // no text range for long tweets
  338. }
  339. if(tweet.quoted_status_result && tweet.quoted_status_result.result) {
  340. let result = tweet.quoted_status_result.result;
  341. if(!result.core && result.tweet) result = result.tweet;
  342. if(result.limitedActionResults) {
  343. let limitation = result.limitedActionResults.limited_actions.find(l => l.action === "Reply");
  344. if(limitation) {
  345. result.tweet.legacy.limited_actions_text = limitation.prompt ? limitation.prompt.subtext.text : LOC.limited_tweet.message;
  346. }
  347. result = result.tweet;
  348. }
  349. tweet.quoted_status = result.legacy;
  350. if(tweet.quoted_status) {
  351. tweet.quoted_status.user = result.core.user_results.result.legacy;
  352. if(!tweet.quoted_status.user) {
  353. delete tweet.quoted_status;
  354. } else {
  355. tweet.quoted_status.user.id_str = tweet.quoted_status.user_id_str;
  356. if(result.core.user_results.result.is_blue_verified && !result.core.user_results.result.legacy.verified_type) {
  357. tweet.quoted_status.user.verified = true;
  358. tweet.quoted_status.user.verified_type = "Blue";
  359. }
  360. tweet.quoted_status.ext = {};
  361. if(result.views) {
  362. tweet.quoted_status.ext.views = {r: {ok: {count: +result.views.count}}};
  363. }
  364. tweetStorage[tweet.quoted_status.id_str] = tweet.quoted_status;
  365. tweetStorage[tweet.quoted_status.id_str].cacheDate = Date.now();
  366. userStorage[tweet.quoted_status.user.id_str] = tweet.quoted_status.user;
  367. userStorage[tweet.quoted_status.user.id_str].cacheDate = Date.now();
  368. }
  369. } else {
  370. console.warn("No quoted status", result);
  371. }
  372. }
  373. if(res.card && res.card.legacy) {
  374. tweet.card = res.card.legacy;
  375. let bvo = {};
  376. for(let i = 0; i < tweet.card.binding_values.length; i++) {
  377. let bv = tweet.card.binding_values[i];
  378. bvo[bv.key] = bv.value;
  379. }
  380. tweet.card.binding_values = bvo;
  381. }
  382. if(res.views) {
  383. if(!tweet.ext) tweet.ext = {};
  384. tweet.ext.views = {r: {ok: {count: +res.views.count}}};
  385. }
  386. if(res.source) {
  387. tweet.source = res.source;
  388. }
  389. if(res.birdwatch_pivot) { // community notes
  390. tweet.birdwatch = res.birdwatch_pivot;
  391. }
  392. if(res.trusted_friends_info_result && res.trusted_friends_info_result.owner_results && res.trusted_friends_info_result.owner_results.result && res.trusted_friends_info_result.owner_results.result.legacy) {
  393. tweet.trusted_circle_owner = res.trusted_friends_info_result.owner_results.result.legacy.screen_name;
  394. }
  395.  
  396. if(tweet.favorited && tweet.favorite_count === 0) {
  397. tweet.favorite_count = 1;
  398. }
  399. if(tweet.retweeted && tweet.retweet_count === 0) {
  400. tweet.retweet_count = 1;
  401. }
  402.  
  403. tweet.res = res;
  404.  
  405. tweetStorage[tweet.id_str] = tweet;
  406. tweetStorage[tweet.id_str].cacheDate = Date.now();
  407. userStorage[tweet.user.id_str] = tweet.user;
  408. userStorage[tweet.user.id_str].cacheDate = Date.now();
  409. return tweet;
  410. }
  411.  
  412.  
  413. const API = {
  414. account: {
  415. verifyCredentials: () => {
  416. return new Promise((resolve, reject) => {
  417. GM_fetch(`https://api.${location.hostname}/1.1/account/verify_credentials.json`, {
  418. headers: {
  419. "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAG5LOQEAAAAAbEKsIYYIhrfOQqm4H8u7xcahRkU%3Dz98HKmzbeXdKqBfUDmElcqYl0cmmKY9KdS2UoNIz3Phapgsowi",
  420. "x-csrf-token": getCsrf(),
  421. "x-twitter-auth-type": "OAuth2Session"
  422. },
  423. credentials: "include"
  424. }).then(response => response.json()).then(data => {
  425. if (data.errors && data.errors[0]) {
  426. return reject(data.errors[0].message);
  427. }
  428. resolve(data);
  429. }).catch(e => {
  430. reject(e);
  431. });
  432. });
  433. },
  434. },
  435. user: {
  436. get: (val, byId = true) => {
  437. return new Promise((resolve, reject) => {
  438. GM_fetch(`https://api.${location.hostname}/1.1/users/show.json?${byId ? `user_id=${val}` : `screen_name=${val}`}`, {
  439. headers: {
  440. "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAG5LOQEAAAAAbEKsIYYIhrfOQqm4H8u7xcahRkU%3Dz98HKmzbeXdKqBfUDmElcqYl0cmmKY9KdS2UoNIz3Phapgsowi",
  441. "x-csrf-token": getCsrf(),
  442. "x-twitter-auth-type": "OAuth2Session",
  443. "x-twitter-client-language": window.LANGUAGE ? window.LANGUAGE : navigator.language ? navigator.language : "en"
  444. },
  445. credentials: "include"
  446. }).then(i => {
  447. if(i.status === 401) {
  448. setTimeout(() => {
  449. location.href = `/i/flow/login?newtwitter=true`;
  450. }, 50);
  451. }
  452. return i.json();
  453. }).then(data => {
  454. debugLog('user.get', {val, byId, data});
  455. if (data.errors && data.errors[0]) {
  456. return reject(data.errors[0].message);
  457. }
  458. resolve(data);
  459. }).catch(e => {
  460. reject(e);
  461. });
  462. });
  463. },
  464. getV2: name => {
  465. return new Promise((resolve, reject) => {
  466. GM_fetch(`/i/api/graphql/sLVLhk0bGj3MVFEKTdax1w/UserByScreenName?variables=%7B%22screen_name%22%3A%22${name}%22%2C%22withSafetyModeUserFields%22%3Atrue%2C%22withSuperFollowsUserFields%22%3Atrue%7D&features=${encodeURIComponent(JSON.stringify({"blue_business_profile_image_shape_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true}))}`, {
  467. headers: {
  468. "authorization": publicToken,
  469. "x-csrf-token": getCsrf(),
  470. "x-twitter-auth-type": "OAuth2Session",
  471. "content-type": "application/json",
  472. "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
  473. },
  474. credentials: "include"
  475. }).then(i => i.json()).then(data => {
  476. debugLog('user.getV2', 'start', {name, data});
  477. if (data.errors && data.errors[0]) {
  478. return reject(data.errors[0].message);
  479. }
  480. if(data.data.user.result.unavailable_message) {
  481. return reject(data.data.user.result.unavailable_message.text);
  482. }
  483.  
  484. let result = data.data.user.result;
  485. result.legacy.id_str = result.rest_id;
  486. if(result.legacy_extended_profile.birthdate) {
  487. result.legacy.birthdate = result.legacy_extended_profile.birthdate;
  488. }
  489. if(result.professional) {
  490. result.legacy.professional = result.professional;
  491. }
  492. if(result.affiliates_highlighted_label && result.affiliates_highlighted_label.label) {
  493. result.legacy.affiliates_highlighted_label = result.affiliates_highlighted_label.label;
  494. }
  495. if(result.is_blue_verified && !result.legacy.verified_type) {
  496. result.legacy.verified_type = "Blue";
  497. }
  498. debugLog('user.getV2', 'end', result.legacy);
  499. resolve(result.legacy);
  500. }).catch(e => {
  501. reject(e);
  502. });
  503. });
  504. },
  505. follow: screen_name => {
  506. return new Promise((resolve, reject) => {
  507. GM_fetch(`https://api.${location.hostname}/1.1/friendships/create.json`, {
  508. method: 'POST',
  509. headers: {
  510. "authorization": publicToken,
  511. "x-csrf-token": getCsrf(),
  512. "x-twitter-auth-type": "OAuth2Session",
  513. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  514. },
  515. credentials: "include",
  516. body: `screen_name=${screen_name}`
  517. }).then(i => i.json()).then(data => {
  518. if (data.errors && data.errors[0]) {
  519. return reject(data.errors[0].message);
  520. }
  521. resolve(data);
  522. }).catch(e => {
  523. reject(e);
  524. });
  525. });
  526. },
  527. unfollow: screen_name => {
  528. return new Promise((resolve, reject) => {
  529. GM_fetch(`https://api.${location.hostname}/1.1/friendships/destroy.json`, {
  530. method: 'POST',
  531. headers: {
  532. "authorization": publicToken,
  533. "x-csrf-token": getCsrf(),
  534. "x-twitter-auth-type": "OAuth2Session",
  535. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  536. },
  537. credentials: "include",
  538. body: `screen_name=${screen_name}`
  539. }).then(i => i.json()).then(data => {
  540. if (data.errors && data.errors[0]) {
  541. return reject(data.errors[0].message);
  542. }
  543. resolve(data);
  544. }).catch(e => {
  545. reject(e);
  546. });
  547. });
  548. },
  549. receiveNotifications: (id, receive = false) => {
  550. return new Promise((resolve, reject) => {
  551. GM_fetch(`/i/api/1.1/friendships/update.json`, {
  552. headers: {
  553. "authorization": publicToken,
  554. "x-csrf-token": getCsrf(),
  555. "x-twitter-auth-type": "OAuth2Session",
  556. "content-type": "application/x-www-form-urlencoded"
  557. },
  558. credentials: "include",
  559. method: 'post',
  560. body: `include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_has_nft_avatar=1&skip_status=1&cursor=-1&id=${id}&device=${receive}`
  561. }).then(i => i.json()).then(data => {
  562. if (data.errors && data.errors[0].code === 32) {
  563. return reject("Not logged in");
  564. }
  565. if (data.errors && data.errors[0]) {
  566. return reject(data.errors[0].message);
  567. }
  568. resolve(data);
  569. }).catch(e => {
  570. reject(e);
  571. });
  572. });
  573. },
  574. block: id => {
  575. return new Promise((resolve, reject) => {
  576. GM_fetch(`/i/api/1.1/blocks/create.json`, {
  577. headers: {
  578. "authorization": publicToken,
  579. "x-csrf-token": getCsrf(),
  580. "x-twitter-auth-type": "OAuth2Session",
  581. "content-type": "application/x-www-form-urlencoded"
  582. },
  583. credentials: "include",
  584. method: 'post',
  585. body: `user_id=${id}`
  586. }).then(i => i.json()).then(data => {
  587. if (data.errors && data.errors[0].code === 32) {
  588. return reject("Not logged in");
  589. }
  590. if (data.errors && data.errors[0]) {
  591. return reject(data.errors[0].message);
  592. }
  593. resolve(data);
  594. }).catch(e => {
  595. reject(e);
  596. });
  597. });
  598. },
  599. unblock: id => {
  600. return new Promise((resolve, reject) => {
  601. GM_fetch(`/i/api/1.1/blocks/destroy.json`, {
  602. headers: {
  603. "authorization": publicToken,
  604. "x-csrf-token": getCsrf(),
  605. "x-twitter-auth-type": "OAuth2Session",
  606. "content-type": "application/x-www-form-urlencoded"
  607. },
  608. credentials: "include",
  609. method: 'post',
  610. body: `user_id=${id}`
  611. }).then(i => i.json()).then(data => {
  612. if (data.errors && data.errors[0].code === 32) {
  613. return reject("Not logged in");
  614. }
  615. if (data.errors && data.errors[0]) {
  616. return reject(data.errors[0].message);
  617. }
  618.  
  619. resolve(data);
  620. }).catch(e => {
  621. reject(e);
  622. });
  623. });
  624. },
  625. mute: id => {
  626. return new Promise((resolve, reject) => {
  627. GM_fetch(`/i/api/1.1/mutes/users/create.json`, {
  628. headers: {
  629. "authorization": publicToken,
  630. "x-csrf-token": getCsrf(),
  631. "x-twitter-auth-type": "OAuth2Session",
  632. "content-type": "application/x-www-form-urlencoded"
  633. },
  634. credentials: "include",
  635. method: 'post',
  636. body: `user_id=${id}`
  637. }).then(i => i.json()).then(data => {
  638. if (data.errors && data.errors[0].code === 32) {
  639. return reject("Not logged in");
  640. }
  641. if (data.errors && data.errors[0]) {
  642. return reject(data.errors[0].message);
  643. }
  644. resolve(data);
  645. }).catch(e => {
  646. reject(e);
  647. });
  648. });
  649. },
  650. unmute: id => {
  651. return new Promise((resolve, reject) => {
  652. GM_fetch(`/i/api/1.1/mutes/users/destroy.json`, {
  653. headers: {
  654. "authorization": publicToken,
  655. "x-csrf-token": getCsrf(),
  656. "x-twitter-auth-type": "OAuth2Session",
  657. "content-type": "application/x-www-form-urlencoded"
  658. },
  659. credentials: "include",
  660. method: 'post',
  661. body: `user_id=${id}`
  662. }).then(i => i.json()).then(data => {
  663. if (data.errors && data.errors[0].code === 32) {
  664. return reject("Not logged in");
  665. }
  666. if (data.errors && data.errors[0]) {
  667. return reject(data.errors[0].message);
  668. }
  669.  
  670. resolve(data);
  671. }).catch(e => {
  672. reject(e);
  673. });
  674. });
  675. },
  676. removeFollower: id => {
  677. return new Promise((resolve, reject) => {
  678. GM_fetch(`/i/api/graphql/QpNfg0kpPRfjROQ_9eOLXA/RemoveFollower`, {
  679. headers: {
  680. "authorization": publicToken,
  681. "x-csrf-token": getCsrf(),
  682. "x-twitter-auth-type": "OAuth2Session",
  683. "content-type": "application/json"
  684. },
  685. credentials: "include",
  686. method: 'post',
  687. body: JSON.stringify({"variables":{"target_user_id":id},"queryId":"QpNfg0kpPRfjROQ_9eOLXA"})
  688. }).then(i => i.json()).then(data => {
  689. if (data.errors && data.errors[0].code === 32) {
  690. return reject("Not logged in");
  691. }
  692. if (data.errors && data.errors[0]) {
  693. return reject(data.errors[0].message);
  694. }
  695. resolve(data);
  696. }).catch(e => {
  697. reject(e);
  698. });
  699. });
  700. },
  701. lookup: ids => {
  702. return new Promise((resolve, reject) => {
  703. GM_fetch(`https://api.${location.hostname}/1.1/users/lookup.json?user_id=${ids.join(",")}`, {
  704. headers: {
  705. "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAG5LOQEAAAAAbEKsIYYIhrfOQqm4H8u7xcahRkU%3Dz98HKmzbeXdKqBfUDmElcqYl0cmmKY9KdS2UoNIz3Phapgsowi",
  706. "x-csrf-token": getCsrf(),
  707. "x-twitter-auth-type": "OAuth2Session",
  708. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  709. },
  710. credentials: "include"
  711. }).then(i => i.json()).then(data => {
  712. if (data.errors && data.errors[0]) {
  713. return reject(data.errors[0].message);
  714. }
  715. resolve(data);
  716. }).catch(e => {
  717. reject(e);
  718. });
  719. });
  720. },
  721. getFollowersYouFollow: (id, cursor) => {
  722. return new Promise((resolve, reject) => {
  723. let obj = {
  724. "userId": id,
  725. "count": 50,
  726. "includePromotedContent": false
  727. };
  728. if(cursor) obj.cursor = cursor;
  729. GM_fetch(`/i/api/graphql/m8AXvuS9H0aAI09J3ISOrw/FollowersYouKnow?variables=${encodeURIComponent(JSON.stringify(obj))}&features=${encodeURIComponent(JSON.stringify({"rweb_lists_timeline_redesign_enabled":false,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_enhance_cards_enabled":false}))}`, {
  730. headers: {
  731. "authorization": publicToken,
  732. "x-csrf-token": getCsrf(),
  733. "x-twitter-auth-type": "OAuth2Session",
  734. "content-type": "application/json"
  735. },
  736. credentials: "include"
  737. }).then(i => i.json()).then(data => {
  738. debugLog('user.getFollowersYouFollow', 'start', {id, cursor, data});
  739. if (data.errors && data.errors[0].code === 32) {
  740. return reject("Not logged in");
  741. }
  742. if (data.errors && data.errors[0]) {
  743. return reject(data.errors[0].message);
  744. }
  745. let list = data.data.user.result.timeline.timeline.instructions.find(i => i.type === 'TimelineAddEntries').entries;
  746. const out = {
  747. list: list.filter(e => e.entryId.startsWith('user-')).map(e => {
  748. let user = e.content.itemContent.user_results.result;
  749. user.legacy.id_str = user.rest_id;
  750. if(user.is_blue_verified && !user.legacy.verified_type) {
  751. user.legacy.verified = true;
  752. user.legacy.verified_type = "Blue";
  753. }
  754. return user.legacy;
  755. }),
  756. cursor: list.find(e => e.entryId.startsWith('cursor-bottom-')).content.value
  757. };
  758. debugLog('user.getFollowersYouFollow', 'end', out);
  759. resolve(out);
  760. }).catch(e => {
  761. reject(e);
  762. });
  763. });
  764. },
  765. switchRetweetsVisibility: (user_id, see) => {
  766. return new Promise((resolve, reject) => {
  767. GM_fetch(`/i/api/1.1/friendships/update.json`, {
  768. headers: {
  769. "authorization": publicToken,
  770. "x-csrf-token": getCsrf(),
  771. "x-twitter-auth-type": "OAuth2Session",
  772. "content-type": "application/x-www-form-urlencoded"
  773. },
  774. credentials: "include",
  775. method: 'post',
  776. body: `id=${user_id}&retweets=${see}`
  777. }).then(i => i.json()).then(data => {
  778. if (data.errors && data.errors[0].code === 32) {
  779. return reject("Not logged in");
  780. }
  781. if (data.errors && data.errors[0]) {
  782. return reject(data.errors[0].message);
  783. }
  784. resolve(data);
  785. }).catch(e => {
  786. reject(e);
  787. });
  788. });
  789. },
  790. getFollowRequests: (cursor = -1) => {
  791. return new Promise((resolve, reject) => {
  792. GM_fetch(`/i/api/1.1/friendships/incoming.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_has_nft_avatar=1&skip_status=1&cursor=${cursor}&stringify_ids=true&count=100`, {
  793. headers: {
  794. "authorization": publicToken,
  795. "x-csrf-token": getCsrf(),
  796. "x-twitter-auth-type": "OAuth2Session"
  797. },
  798. credentials: "include"
  799. }).then(i => i.json()).then(data => {
  800. if (data.errors && data.errors[0].code === 32) {
  801. return reject("Not logged in");
  802. }
  803. if (data.errors && data.errors[0]) {
  804. return reject(data.errors[0].message);
  805. }
  806. resolve(data);
  807. }).catch(e => {
  808. reject(e);
  809. });
  810. });
  811. },
  812. acceptFollowRequest: user_id => {
  813. return new Promise((resolve, reject) => {
  814. GM_fetch(`/i/api/1.1/friendships/accept.json`, {
  815. headers: {
  816. "authorization": publicToken,
  817. "x-csrf-token": getCsrf(),
  818. "x-twitter-auth-type": "OAuth2Session",
  819. "content-type": "application/x-www-form-urlencoded"
  820. },
  821. credentials: "include",
  822. method: 'post',
  823. body: `user_id=${user_id}`
  824. }).then(i => i.json()).then(data => {
  825. if (data.errors && data.errors[0].code === 32) {
  826. return reject("Not logged in");
  827. }
  828. if (data.errors && data.errors[0]) {
  829. return reject(data.errors[0].message);
  830. }
  831. resolve(data);
  832. }).catch(e => {
  833. reject(e);
  834. });
  835. });
  836. },
  837. declineFollowRequest: user_id => {
  838. return new Promise((resolve, reject) => {
  839. GM_fetch(`/i/api/1.1/friendships/deny.json`, {
  840. headers: {
  841. "authorization": publicToken,
  842. "x-csrf-token": getCsrf(),
  843. "x-twitter-auth-type": "OAuth2Session",
  844. "content-type": "application/x-www-form-urlencoded"
  845. },
  846. credentials: "include",
  847. method: 'post',
  848. body: `user_id=${user_id}`
  849. }).then(i => i.json()).then(data => {
  850. if (data.errors && data.errors[0].code === 32) {
  851. return reject("Not logged in");
  852. }
  853. if (data.errors && data.errors[0]) {
  854. return reject(data.errors[0].message);
  855. }
  856. resolve(data);
  857. }).catch(e => {
  858. reject(e);
  859. });
  860. });
  861. },
  862. },
  863. tweet: {
  864. post: data => { // deprecated
  865. return new Promise((resolve, reject) => {
  866. GM_fetch(`https://api.${location.hostname}/1.1/statuses/update.json`, {
  867. method: 'POST',
  868. headers: {
  869. "authorization": publicToken,
  870. "x-csrf-token": getCsrf(),
  871. "x-twitter-auth-type": "OAuth2Session",
  872. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  873. },
  874. body: new URLSearchParams(data).toString(),
  875. credentials: "include"
  876. }).then(i => i.json()).then(data => {
  877. if (data.errors && data.errors[0]) {
  878. return reject(data.errors[0].message);
  879. }
  880. resolve(data);
  881. }).catch(e => {
  882. reject(e);
  883. });
  884. });
  885. },
  886. /*
  887. text | tweet_text | status - tweet text
  888. media | media_ids - media ids
  889. card_uri - card uri
  890. sensitive - sensitive media
  891. in_reply_to_status_id | in_reply_to_tweet_id - reply to tweet id
  892. exclude_reply_user_ids - exclude mentions
  893. attachment_url - quote tweet url
  894. circle - circle id
  895. conversation_control - conversation control (follows | mentions)
  896. */
  897. postV2: tweet => {
  898. return new Promise((resolve, reject) => {
  899. let text;
  900. if(tweet.text) {
  901. text = tweet.text;
  902. } else if(tweet.tweet_text) {
  903. text = tweet.tweet_text;
  904. } else if(tweet.status) {
  905. text = tweet.status;
  906. } else {
  907. text = "";
  908. }
  909. let variables = {
  910. "tweet_text": text,
  911. "media": {
  912. "media_entities": [],
  913. "possibly_sensitive": false
  914. },
  915. "semantic_annotation_ids": [],
  916. "dark_request": false
  917. };
  918. if(tweet.card_uri) {
  919. variables.card_uri = tweet.card_uri;
  920. }
  921. if(tweet.media_ids) {
  922. if(typeof tweet.media_ids === "string") {
  923. tweet.media = tweet.media_ids.split(",");
  924. } else {
  925. tweet.media = tweet.media_ids;
  926. }
  927. }
  928. if(tweet.media) {
  929. variables.media.media_entities = tweet.media.map(i => ({media_id: i, tagged_users: []}));
  930. if(tweet.sensitive) {
  931. variables.media.possibly_sensitive = true;
  932. }
  933. }
  934. if(tweet.conversation_control === 'follows') {
  935. variables.conversation_control = { mode: 'Community' };
  936. } else if(tweet.conversation_control === 'mentions') {
  937. variables.conversation_control = { mode: 'ByInvitation' };
  938. }
  939. if(tweet.circle) {
  940. variables.trusted_friends_control_options = { "trusted_friends_list_id": tweet.circle };
  941. }
  942. if(tweet.in_reply_to_status_id) {
  943. tweet.in_reply_to_tweet_id = tweet.in_reply_to_status_id;
  944. delete tweet.in_reply_to_status_id;
  945. }
  946. if(tweet.in_reply_to_tweet_id) {
  947. variables.reply = {
  948. in_reply_to_tweet_id: tweet.in_reply_to_tweet_id,
  949. exclude_reply_user_ids: []
  950. }
  951. if(tweet.exclude_reply_user_ids) {
  952. if(typeof tweet.exclude_reply_user_ids === "string") {
  953. tweet.exclude_reply_user_ids = tweet.exclude_reply_user_ids.split(",");
  954. }
  955. variables.reply.exclude_reply_user_ids = tweet.exclude_reply_user_ids;
  956. }
  957. }
  958. if(tweet.attachment_url) {
  959. variables.attachment_url = tweet.attachment_url;
  960. }
  961. debugLog('tweet.postV2', 'init', {tweet, variables});
  962. let parsedTweet = twttr.txt.parseTweet(text);
  963. GM_fetch(`/i/api/graphql/${parsedTweet.weightedLength > 280 ? 'cuvrhmg0s4pGaLWV68NNnQ/CreateNoteTweet' : 'I_J3_LvnnihD0Gjbq5pD2g/CreateTweet'}`, {
  964. method: 'POST',
  965. headers: {
  966. "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw",
  967. "x-csrf-token": getCsrf(),
  968. "x-twitter-auth-type": "OAuth2Session",
  969. "content-type": "application/json; charset=utf-8",
  970. "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
  971. },
  972. credentials: "include",
  973. body: JSON.stringify({
  974. variables,
  975. "features": {"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"responsive_web_home_pinned_timelines_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false},
  976. "queryId": parsedTweet.weightedLength > 280 ? 'cuvrhmg0s4pGaLWV68NNnQ' : 'I_J3_LvnnihD0Gjbq5pD2g'
  977. })
  978. }).then(i => i.json()).then(data => {
  979. debugLog('tweet.postV2', 'start', data);
  980. if (data.errors && data.errors[0]) {
  981. return reject(data.errors[0].message);
  982. }
  983. let ct = data.data.create_tweet ? data.data.create_tweet : data.data.notetweet_create;
  984. let result = ct.tweet_results.result;
  985. let tweet = parseTweet(result);
  986. if(result.trusted_friends_info_result && !tweet.limited_actions) {
  987. tweet.limited_actions = 'limit_trusted_friends_tweet';
  988. }
  989. debugLog('tweet.postV2', 'end', tweet);
  990. resolve(tweet);
  991. }).catch(e => {
  992. reject(e);
  993. });
  994. });
  995. },
  996. favorite: id => {
  997. return new Promise((resolve, reject) => {
  998. GM_fetch(`/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet`, {
  999. method: 'POST',
  1000. headers: {
  1001. "authorization": publicToken,
  1002. "x-csrf-token": getCsrf(),
  1003. "x-twitter-auth-type": "OAuth2Session",
  1004. "content-type": "application/json; charset=utf-8"
  1005. },
  1006. credentials: "include",
  1007. body: JSON.stringify({"variables":{"tweet_id":id},"queryId":"lI07N6Otwv1PhnEgXILM7A"})
  1008. }).then(i => i.json()).then(data => {
  1009. if (data.errors && data.errors[0]) {
  1010. return reject(data.errors[0].message);
  1011. }
  1012. resolve(data);
  1013. }).catch(e => {
  1014. reject(e);
  1015. });
  1016. });
  1017. },
  1018. unfavorite: id => {
  1019. return new Promise((resolve, reject) => {
  1020. GM_fetch(`/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet`, {
  1021. method: 'POST',
  1022. headers: {
  1023. "authorization": publicToken,
  1024. "x-csrf-token": getCsrf(),
  1025. "x-twitter-auth-type": "OAuth2Session",
  1026. "content-type": "application/json; charset=utf-8"
  1027. },
  1028. credentials: "include",
  1029. body: JSON.stringify({"variables":{"tweet_id":id},"queryId":"ZYKSe-w7KEslx3JhSIk5LA"})
  1030. }).then(i => i.json()).then(data => {
  1031. if (data.errors && data.errors[0]) {
  1032. return reject(data.errors[0].message);
  1033. }
  1034. resolve(data);
  1035. }).catch(e => {
  1036. reject(e);
  1037. });
  1038. });
  1039. },
  1040. retweet: id => {
  1041. return new Promise((resolve, reject) => {
  1042. GM_fetch(`/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet`, {
  1043. method: 'POST',
  1044. headers: {
  1045. "authorization": publicToken,
  1046. "x-csrf-token": getCsrf(),
  1047. "x-twitter-auth-type": "OAuth2Session",
  1048. "content-type": "application/json; charset=utf-8"
  1049. },
  1050. credentials: "include",
  1051. body: JSON.stringify({"variables":{"tweet_id":id,"dark_request":false},"queryId":"ojPdsZsimiJrUGLR1sjUtA"})
  1052. }).then(i => i.json()).then(data => {
  1053. debugLog('tweet.retweet', id, data);
  1054. if (data.errors && data.errors[0]) {
  1055. return reject(data.errors[0].message);
  1056. }
  1057. resolve(data);
  1058. }).catch(e => {
  1059. reject(e);
  1060. });
  1061. });
  1062. },
  1063. unretweet: id => {
  1064. return new Promise((resolve, reject) => {
  1065. GM_fetch(`/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet`, {
  1066. method: 'POST',
  1067. headers: {
  1068. "authorization": publicToken,
  1069. "x-csrf-token": getCsrf(),
  1070. "x-twitter-auth-type": "OAuth2Session",
  1071. "content-type": "application/json; charset=utf-8"
  1072. },
  1073. credentials: "include",
  1074. body: JSON.stringify({"variables":{"source_tweet_id":id,"dark_request":false},"queryId":"iQtK4dl5hBmXewYZuEOKVw"})
  1075. }).then(i => i.json()).then(data => {
  1076. debugLog('tweet.unretweet', id, data);
  1077. if (data.errors && data.errors[0]) {
  1078. return reject(data.errors[0].message);
  1079. }
  1080. resolve(data);
  1081. }).catch(e => {
  1082. reject(e);
  1083. });
  1084. });
  1085. },
  1086. delete: id => {
  1087. return new Promise((resolve, reject) => {
  1088. GM_fetch(`/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet`, {
  1089. method: 'POST',
  1090. headers: {
  1091. "authorization": publicToken,
  1092. "x-csrf-token": getCsrf(),
  1093. "x-twitter-auth-type": "OAuth2Session",
  1094. "content-type": "application/json; charset=utf-8"
  1095. },
  1096. credentials: "include",
  1097. body: JSON.stringify({"variables":{"tweet_id":id,"dark_request":false},"queryId":"VaenaVgh5q5ih7kvyVjgtg"})
  1098. }).then(i => i.json()).then(data => {
  1099. debugLog('tweet.delete', id, data);
  1100. if (data.errors && data.errors[0]) {
  1101. return reject(data.errors[0].message);
  1102. }
  1103. resolve(data);
  1104. }).catch(e => {
  1105. reject(e);
  1106. });
  1107. });
  1108. },
  1109. get: id => { // deprecated
  1110. return new Promise((resolve, reject) => {
  1111. GM_fetch(`https://api.${location.hostname}/1.1/statuses/show.json?id=${id}&include_my_retweet=1&cards_platform=Web13&include_entities=1&include_user_entities=1&include_cards=1&send_error_codes=1&tweet_mode=extended&include_ext_alt_text=true&include_reply_count=true&ext=views%2CmediaStats%2CverifiedType%2CisBlueVerified`, {
  1112. headers: {
  1113. "authorization": publicToken,
  1114. "x-csrf-token": getCsrf(),
  1115. "x-twitter-auth-type": "OAuth2Session",
  1116. "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
  1117. },
  1118. credentials: "include"
  1119. }).then(i => i.json()).then(data => {
  1120. if (data.errors && data.errors[0]) {
  1121. return reject(data.errors[0].message);
  1122. }
  1123. resolve(data);
  1124. }).catch(e => {
  1125. reject(e);
  1126. });
  1127. });
  1128. },
  1129. vote: (api, tweet_id, card_uri, card_name, selected_choice) => {
  1130. return new Promise((resolve, reject) => {
  1131. GM_fetch(`https://caps.${location.hostname}/v2/capi/${api.split('//')[1]}`, {
  1132. headers: {
  1133. "authorization": publicToken,
  1134. "x-csrf-token": getCsrf(),
  1135. "x-twitter-auth-type": "OAuth2Session",
  1136. "content-type": "application/x-www-form-urlencoded"
  1137. },
  1138. credentials: "include",
  1139. method: 'post',
  1140. body: `twitter%3Astring%3Acard_uri=${encodeURIComponent(card_uri)}&twitter%3Along%3Aoriginal_tweet_id=${tweet_id}&twitter%3Astring%3Aresponse_card_name=${card_name}&twitter%3Astring%3Acards_platform=Web-12&twitter%3Astring%3Aselected_choice=${selected_choice}`
  1141. }).then(response => response.json()).then(data => {
  1142. if (data.errors && data.errors[0].code === 32) {
  1143. return reject("Not logged in");
  1144. }
  1145. if (data.errors && data.errors[0]) {
  1146. return reject(data.errors[0].message);
  1147. }
  1148. resolve(data);
  1149. }).catch(e => {
  1150. reject(e);
  1151. });
  1152. })
  1153. },
  1154. createCard: card_data => {
  1155. return new Promise((resolve, reject) => {
  1156. GM_fetch(`https://caps.${location.hostname}/v2/cards/create.json`, {
  1157. headers: {
  1158. "authorization": publicToken,
  1159. "x-csrf-token": getCsrf(),
  1160. "x-twitter-auth-type": "OAuth2Session",
  1161. "content-type": "application/x-www-form-urlencoded"
  1162. },
  1163. credentials: "include",
  1164. method: 'post',
  1165. body: `card_data=${encodeURIComponent(JSON.stringify(card_data))}`
  1166. }).then(response => response.json()).then(data => {
  1167. if (data.errors && data.errors[0].code === 32) {
  1168. return reject("Not logged in");
  1169. }
  1170. if (data.errors && data.errors[0]) {
  1171. return reject(data.errors[0].message);
  1172. }
  1173. resolve(data);
  1174. }).catch(e => {
  1175. reject(e);
  1176. });
  1177. })
  1178. },
  1179. mute: id => {
  1180. return new Promise((resolve, reject) => {
  1181. GM_fetch(`/i/api/1.1/mutes/conversations/create.json`, {
  1182. headers: {
  1183. "authorization": publicToken,
  1184. "x-csrf-token": getCsrf(),
  1185. "x-twitter-auth-type": "OAuth2Session",
  1186. "content-type": "application/x-www-form-urlencoded"
  1187. },
  1188. credentials: "include",
  1189. method: 'post',
  1190. body: `tweet_id=${id}`
  1191. }).then(i => i.json()).then(data => {
  1192. if (data.errors && data.errors[0].code === 32) {
  1193. return reject("Not logged in");
  1194. }
  1195. if (data.errors && data.errors[0]) {
  1196. return reject(data.errors[0].message);
  1197. }
  1198. resolve(data);
  1199. }).catch(e => {
  1200. reject(e);
  1201. });
  1202. });
  1203. },
  1204. unmute: id => {
  1205. return new Promise((resolve, reject) => {
  1206. GM_fetch(`/i/api/1.1/mutes/conversations/destroy.json`, {
  1207. headers: {
  1208. "authorization": publicToken,
  1209. "x-csrf-token": getCsrf(),
  1210. "x-twitter-auth-type": "OAuth2Session",
  1211. "content-type": "application/x-www-form-urlencoded"
  1212. },
  1213. credentials: "include",
  1214. method: 'post',
  1215. body: `tweet_id=${id}`
  1216. }).then(i => i.json()).then(data => {
  1217. if (data.errors && data.errors[0].code === 32) {
  1218. return reject("Not logged in");
  1219. }
  1220. if (data.errors && data.errors[0]) {
  1221. return reject(data.errors[0].message);
  1222. }
  1223. resolve(data);
  1224. }).catch(e => {
  1225. reject(e);
  1226. });
  1227. });
  1228. },
  1229. lookup: ids => {
  1230. return new Promise((resolve, reject) => {
  1231. GM_fetch(`https://api.${location.hostname}/1.1/statuses/lookup.json?id=${ids.join(',')}&include_entities=true&include_ext_alt_text=true&include_card_uri=true&tweet_mode=extended&include_reply_count=true&ext=views%2CmediaStats`, {
  1232. headers: {
  1233. "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
  1234. "x-csrf-token": getCsrf(),
  1235. "x-twitter-auth-type": "OAuth2Session",
  1236. "x-twitter-client-language": navigator.language ? navigator.language : "en"
  1237. },
  1238. credentials: "include"
  1239. }).then(i => i.json()).then(data => {
  1240. if (data.errors && data.errors[0].code === 32) {
  1241. return reject("Not logged in");
  1242. }
  1243. if (data.errors && data.errors[0]) {
  1244. return reject(data.errors[0].message);
  1245. }
  1246. resolve(data);
  1247. }).catch(e => {
  1248. reject(e);
  1249. });
  1250. });
  1251. },
  1252. pin: id => {
  1253. return new Promise((resolve, reject) => {
  1254. GM_fetch(`/i/api/1.1/account/pin_tweet.json`, {
  1255. headers: {
  1256. "authorization": publicToken,
  1257. "x-csrf-token": getCsrf(),
  1258. "x-twitter-auth-type": "OAuth2Session",
  1259. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  1260. },
  1261. credentials: "include",
  1262. method: 'post',
  1263. body: `id=${id}`
  1264. }).then(i => i.text()).then(data => {
  1265. resolve(true);
  1266. }).catch(e => {
  1267. reject(e);
  1268. });
  1269. });
  1270. },
  1271. unpin: id => {
  1272. return new Promise((resolve, reject) => {
  1273. GM_fetch(`/i/api/1.1/account/unpin_tweet.json`, {
  1274. headers: {
  1275. "authorization": publicToken,
  1276. "x-csrf-token": getCsrf(),
  1277. "x-twitter-auth-type": "OAuth2Session",
  1278. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  1279. },
  1280. credentials: "include",
  1281. method: 'post',
  1282. body: `id=${id}`
  1283. }).then(i => i.text()).then(data => {
  1284. resolve(true);
  1285. }).catch(e => {
  1286. reject(e);
  1287. });
  1288. });
  1289. },
  1290. moderate: id => {
  1291. return new Promise((resolve, reject) => {
  1292. GM_fetch(`/i/api/graphql/pjFnHGVqCjTcZol0xcBJjw/ModerateTweet`, {
  1293. method: 'POST',
  1294. headers: {
  1295. "authorization": publicToken,
  1296. "x-csrf-token": getCsrf(),
  1297. "x-twitter-auth-type": "OAuth2Session",
  1298. "content-type": "application/json; charset=utf-8"
  1299. },
  1300. credentials: "include",
  1301. body: JSON.stringify({"variables":{"tweetId":id},"queryId":"pjFnHGVqCjTcZol0xcBJjw"})
  1302. }).then(i => i.json()).then(data => {
  1303. debugLog('tweet.moderate', id, data);
  1304. if (data.errors && data.errors[0]) {
  1305. return reject(data.errors[0].message);
  1306. }
  1307. resolve(data);
  1308. }).catch(e => {
  1309. reject(e);
  1310. });
  1311. });
  1312. },
  1313. unmoderate: id => {
  1314. return new Promise((resolve, reject) => {
  1315. GM_fetch(`/i/api/graphql/pVSyu6PA57TLvIE4nN2tsA/UnmoderateTweet`, {
  1316. method: 'POST',
  1317. headers: {
  1318. "authorization": publicToken,
  1319. "x-csrf-token": getCsrf(),
  1320. "x-twitter-auth-type": "OAuth2Session",
  1321. "content-type": "application/json; charset=utf-8"
  1322. },
  1323. credentials: "include",
  1324. body: JSON.stringify({"variables":{"tweetId":"1683331680751308802"},"queryId":"pVSyu6PA57TLvIE4nN2tsA"})
  1325. }).then(i => i.json()).then(data => {
  1326. debugLog('tweet.unmoderate', id, data);
  1327. if (data.errors && data.errors[0]) {
  1328. return reject(data.errors[0].message);
  1329. }
  1330. resolve(data);
  1331. }).catch(e => {
  1332. reject(e);
  1333. });
  1334. });
  1335. },
  1336. getModeratedReplies: (id, cursor) => {
  1337. return new Promise((resolve, reject) => {
  1338. let variables = {"rootTweetId":id,"count":20,"includePromotedContent":false};
  1339. if(cursor) variables.cursor = cursor;
  1340. GM_fetch(`/i/api/graphql/SiKS1_3937rb72ytFnDHmA/ModeratedTimeline?variables=${encodeURIComponent(JSON.stringify(variables))}&features=${encodeURIComponent(JSON.stringify({"rweb_lists_timeline_redesign_enabled":false,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_enhance_cards_enabled":false}))}`, {
  1341. method: 'POST',
  1342. headers: {
  1343. "authorization": publicToken,
  1344. "x-csrf-token": getCsrf(),
  1345. "x-twitter-auth-type": "OAuth2Session",
  1346. "content-type": "application/x-www-form-urlencoded",
  1347. "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
  1348. },
  1349. credentials: "include"
  1350. }).then(i => i.json()).then(data => {
  1351. debugLog('tweet.getModeratedReplies', 'start', id, data);
  1352. if (data.errors && data.errors[0]) {
  1353. return reject(data.errors[0].message);
  1354. }
  1355. let entries = data.data.tweet.result.timeline_response.timeline.instructions.find(i => i.entries);
  1356. if(!entries) return resolve({
  1357. list: [],
  1358. cursor: undefined
  1359. });
  1360. entries = entries.entries;
  1361. let list = entries.filter(e => e.entryId.startsWith('tweet-'));
  1362. let cursor = entries.find(e => e.entryId.startsWith('cursor-bottom'));
  1363. if(!cursor) {
  1364. let entries = data.data.tweet.result.timeline_response.timeline.instructions.find(i => i.replaceEntry && i.replaceEntry.entryIdToReplace.includes('cursor-bottom'));
  1365. if(entries) {
  1366. cursor = entries.replaceEntry.entry.content.operation.cursor.value;
  1367. }
  1368. } else {
  1369. cursor = cursor.content.operation.cursor.value;
  1370. }
  1371. let out = {
  1372. list: list.map(e => {
  1373. let tweet = parseTweet(e.content.itemContent.tweet_results.result);
  1374. if(!tweet) return;
  1375. tweet.moderated = true;
  1376. return tweet;
  1377. }).filter(e => e),
  1378. cursor
  1379. };
  1380. debugLog('tweet.getModeratedReplies', 'end', id, out);
  1381. resolve(data);
  1382. }).catch(e => {
  1383. reject(e);
  1384. });
  1385. });
  1386. }
  1387. },
  1388. };
  1389.  
  1390. // scripts/helpers.js
  1391. function createModal(html, className, onclose, canclose) {
  1392. let modal = document.createElement('div');
  1393. modal.classList.add('yeah-modal');
  1394. let modal_content = document.createElement('div');
  1395. modal_content.classList.add('yeah-modal-content');
  1396. if(className) modal_content.classList.add(className);
  1397. modal_content.innerHTML = html;
  1398. modal.appendChild(modal_content);
  1399. let close = document.createElement('span');
  1400. close.classList.add('yeah-modal-close');
  1401. close.title = "ESC";
  1402. close.innerHTML = '&times;';
  1403. document.body.style.overflowY = 'hidden';
  1404. function removeModal() {
  1405. modal.remove();
  1406. let event = new Event('findActiveTweet');
  1407. document.dispatchEvent(event);
  1408. document.removeEventListener('keydown', escapeEvent);
  1409. if(onclose) onclose();
  1410. let modals = document.getElementsByClassName('modal');
  1411. if(modals.length === 0) {
  1412. document.body.style.overflowY = '';
  1413. }
  1414. }
  1415. modal.removeModal = removeModal;
  1416. function escapeEvent(e) {
  1417. if(document.querySelector('.viewer-in')) return;
  1418. if(e.key === 'Escape' || (e.altKey && e.keyCode === 78)) {
  1419. if(!canclose || canclose()) removeModal();
  1420. }
  1421. }
  1422. close.addEventListener('click', removeModal);
  1423. let isHoldingMouseFromContent = false;
  1424. modal_content.addEventListener('mousedown', () => {
  1425. isHoldingMouseFromContent = true;
  1426. });
  1427. document.addEventListener('mouseup', () => {
  1428. setTimeout(() => isHoldingMouseFromContent = false, 10);
  1429. });
  1430. modal.addEventListener('click', e => {
  1431. if(e.target === modal && !isHoldingMouseFromContent) {
  1432. if(!canclose || canclose()) removeModal();
  1433. }
  1434. });
  1435. document.addEventListener('keydown', escapeEvent);
  1436. modal_content.appendChild(close);
  1437. document.body.appendChild(modal);
  1438. return modal;
  1439. }
  1440.  
  1441. async function callTwitterApi(method = 'GET', path, headers = {}, body) {
  1442. if(typeof body === 'object' && !headers['Content-Type']) {
  1443. body = JSON.stringify(body);
  1444. headers['Content-Type'] = 'application/json';
  1445. }
  1446. if(headers['Content-Type'] === 'application/x-www-form-urlencoded') {
  1447. body = new URLSearchParams(body).toString();
  1448. }
  1449. if(!headers['Authorization']) {
  1450. headers['Authorization'] = `Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA`;
  1451. }
  1452. if(!headers['x-csrf-token']) {
  1453. let csrf = document.cookie.match(/(?:^|;\s*)ct0=([0-9a-f]+)\s*(?:;|$)/);
  1454. headers['x-csrf-token'] = csrf ? csrf[1] : "";
  1455. }
  1456. headers['x-twitter-auth-type'] = 'OAuth2Session';
  1457. headers['x-twitter-active-user'] = 'yes';
  1458. headers['x-twitter-client-language'] = 'en';
  1459.  
  1460. let res = await GM_fetch(`https://${location.hostname}/i/api${path}`, {
  1461. method,
  1462. headers,
  1463. body
  1464. }).then(res => res.json());
  1465.  
  1466. if(res.errors) {
  1467. throw new Error(res.errors[0].message);
  1468. }
  1469.  
  1470. return res;
  1471. };
  1472.  
  1473. async function callYeahApi(path, body = {}) {
  1474. let token = await getYeahToken();
  1475. if(token) body.key = token;
  1476.  
  1477. const res = await GM_fetch(API_URL + path, {
  1478. method: 'POST',
  1479. headers: {
  1480. 'Content-Type': 'application/json'
  1481. },
  1482. body: JSON.stringify(body)
  1483. });
  1484. let result = await res.text();
  1485.  
  1486. if(result === 'Invalid key') {
  1487. chrome.storage.local.remove('yeahToken');
  1488. chrome.storage.local.get('yeahTokens', async result => {
  1489. if(result.yeahTokens) {
  1490. let userId = await getUserId();
  1491. delete result.yeahTokens[userId];
  1492. chrome.storage.local.set(result);
  1493. }
  1494. });
  1495. throw new Error('Invalid key');
  1496. }
  1497.  
  1498. return result;
  1499. }
  1500.  
  1501. let _userId;
  1502. async function getUserId() {
  1503. if(!_userId) {
  1504. let user = await API.account.verifyCredentials();
  1505. _userId = user.id_str;
  1506. }
  1507. return _userId;
  1508. }
  1509.  
  1510. function getYeahToken() {
  1511. return new Promise(async (resolve, reject) => {
  1512. chrome.storage.local.get(['yeahToken', 'yeahTokens'], async result => {
  1513. if(result) {
  1514. let userId = await getUserId();
  1515. if(result.yeahTokens && result.yeahTokens[userId]) {
  1516. resolve(result.yeahTokens[userId]);
  1517. } else {
  1518. resolve(result.yeahToken);
  1519. }
  1520. } else {
  1521. resolve(null);
  1522. }
  1523. });
  1524. });
  1525. }
  1526.  
  1527. function getYeahSettings() {
  1528. return new Promise((resolve, reject) => {
  1529. chrome.storage.local.get('settings', result => {
  1530. if(result && result.settings) {
  1531. resolve(result.settings);
  1532. } else {
  1533. resolve({});
  1534. }
  1535. });
  1536. });
  1537. }
  1538.  
  1539. function formatLargeNumber(n) {
  1540. let option = {notation: 'compact', compactDisplay: 'short', maximumFractionDigits: 1, minimumFractionDigits: 1};
  1541. if (n >= 1e3) {
  1542. return Number(n).toLocaleString('en-US', option);
  1543. }
  1544. else return Number(n).toLocaleString();
  1545. }
  1546.  
  1547. function escapeHTML(unsafe) {
  1548. if(typeof unsafe === 'undefined' || unsafe === null) {
  1549. return '';
  1550. }
  1551. return DOMPurify.sanitize(String(unsafe));
  1552. }
  1553.  
  1554. async function appendUser(u, container, label) {
  1555. let userElement = document.createElement('div');
  1556. userElement.classList.add('user-item');
  1557.  
  1558. userElement.innerHTML = /*html*/`
  1559. <div>
  1560. <a href="/${u.screen_name}" class="user-item-link" target="_blank">
  1561. <img src="${u.profile_image_url_https}" alt="${u.screen_name}" class="user-item-avatar tweet-avatar" width="48" height="48">
  1562. <div class="user-item-text">
  1563. <span class="yeah-name user-item-name${u.protected ? ' user-protected' : ''}${u.muting ? ' user-muted' : ''}${u.verified || u.verified_type ? ' user-verified' : u.id_str === '1708130407663759360' ? ' user-verified user-verified-dimden' : ''} ${u.verified_type === 'Government' ? 'user-verified-gray' : u.verified_type === 'Business' ? 'user-verified-yellow' : u.verified_type === 'Blue' ? 'user-verified-blue' : ''}">${escapeHTML(u.name)}</span><br>
  1564. <span class="yeah-handle">@${u.screen_name}</span>
  1565. ${u.followed_by ? `<span class="follows-you-label">Follows you</span>` : ''}
  1566. ${label ? `<br><span class="user-item-additional">${escapeHTML(label)}</span>` : ''}
  1567. </div>
  1568. </a>
  1569. </div>
  1570. <button class="user-yeah-item-btn nice-yeah-button ${u.following ? 'yeah-following' : 'yeah-follow'}">${u.following ? "Following" : "Follow"}</button>
  1571. `;
  1572.  
  1573. let followButton = userElement.querySelector('.user-yeah-item-btn');
  1574. followButton.addEventListener('click', async () => {
  1575. if (followButton.classList.contains('yeah-following')) {
  1576. try {
  1577. await API.user.unfollow(u.screen_name);
  1578. } catch(e) {
  1579. console.error(e);
  1580. alert(e);
  1581. return;
  1582. }
  1583. followButton.classList.remove('yeah-following');
  1584. followButton.classList.add('yeah-follow');
  1585. followButton.innerText = "Follow";
  1586. } else {
  1587. try {
  1588. await API.user.follow(u.screen_name);
  1589. } catch(e) {
  1590. console.error(e);
  1591. alert(e);
  1592. return;
  1593. }
  1594. followButton.classList.remove('yeah-follow');
  1595. followButton.classList.add('yeah-following');
  1596. followButton.innerText = "Following";
  1597. }
  1598. });
  1599.  
  1600. container.appendChild(userElement);
  1601. }
  1602.  
  1603.  
  1604. // scripts/tweetrenderer.js
  1605.  
  1606. let lastTweetErrorDate = 0;
  1607. const mediaClasses = [
  1608. undefined,
  1609. 'tweet-media-element-one',
  1610. 'tweet-media-element-two',
  1611. 'tweet-media-element-three',
  1612. 'tweet-media-element-two',
  1613. ];
  1614.  
  1615. function calculateSize(x, y, max_x, max_y) {
  1616. let ratio = x / y;
  1617. let iw = innerWidth;
  1618. if(iw < 590) max_x = iw - 120;
  1619. if(x > max_x) {
  1620. x = max_x;
  1621. y = x / ratio;
  1622. }
  1623. if(y > max_y) {
  1624. y = max_y;
  1625. x = y * ratio;
  1626. }
  1627. return [parseInt(x), parseInt(y)];
  1628. }
  1629.  
  1630. const sizeFunctions = [
  1631. undefined,
  1632. (w, h) => calculateSize(w, h, 450, 500),
  1633. (w, h) => calculateSize(w, h, 225, 400),
  1634. (w, h) => innerWidth < 590 ? calculateSize(w, h, 225, 400) : calculateSize(w, h, 150, 250),
  1635. (w, h) => calculateSize(w, h, 225, 400),
  1636. (w, h) => calculateSize(w, h, 225, 400),
  1637. (w, h) => calculateSize(w, h, 225, 400),
  1638. (w, h) => calculateSize(w, h, 225, 400),
  1639. (w, h) => calculateSize(w, h, 225, 400)
  1640. ];
  1641.  
  1642. const quoteSizeFunctions = [
  1643. undefined,
  1644. (w, h) => calculateSize(w, h, 400, 400),
  1645. (w, h) => calculateSize(w, h, 200, 400),
  1646. (w, h) => calculateSize(w, h, 125, 200),
  1647. (w, h) => calculateSize(w, h, 100, 150),
  1648. (w, h) => calculateSize(w, h, 100, 150),
  1649. (w, h) => calculateSize(w, h, 100, 150),
  1650. (w, h) => calculateSize(w, h, 100, 150),
  1651. (w, h) => calculateSize(w, h, 100, 150)
  1652. ];
  1653.  
  1654. function html(strings, ...values) {
  1655. let str = '';
  1656. strings.forEach((string, i) => {
  1657. str += string + escapeHTML(values[i]);
  1658. });
  1659. return str;
  1660. }
  1661.  
  1662. async function handleFiles(files, mediaArray, mediaContainer, is_dm = false) {
  1663. let images = [];
  1664. let videos = [];
  1665. let gifs = [];
  1666. for (let i = 0; i < files.length; i++) {
  1667. let file = files[i];
  1668. if (file.type.includes('gif')) {
  1669. // max 15 mb
  1670. if (file.size > 15000000) {
  1671. return alert("Gifs max size is 15mb");
  1672. }
  1673. gifs.push(file);
  1674. } else if (file.type.includes('video')) {
  1675. // max 500 mb
  1676. if (file.size > 500000000) {
  1677. return alert("Videos max size is 500mb");
  1678. }
  1679. videos.push(file);
  1680. } else if (file.type.includes('image')) {
  1681. // max 5 mb
  1682. if (
  1683. file.size > 5000000 ||
  1684. (window.navigator && navigator.connection && navigator.connection.type === 'cellular')
  1685. ) {
  1686. // convert png to jpeg
  1687. let toBreak = false, i = 0;
  1688. while(file.size > 5000000) {
  1689. await new Promise(resolve => {
  1690. let canvas = document.createElement('canvas');
  1691. let ctx = canvas.getContext('2d');
  1692. let img = new Image();
  1693. img.onload = function () {
  1694. canvas.width = img.width;
  1695. canvas.height = img.height;
  1696. ctx.drawImage(img, 0, 0);
  1697. let dataURL = canvas.toDataURL('image/jpeg', (window.navigator && navigator.connection && navigator.connection.type === 'cellular') ? (0.5 - i*0.1) : (0.9 - i*0.1));
  1698. let blobBin = atob(dataURL.split(',')[1]);
  1699. let array = [];
  1700. for (let i = 0; i < blobBin.length; i++) {
  1701. array.push(blobBin.charCodeAt(i));
  1702. }
  1703. let newFile = new Blob([new Uint8Array(array)], { type: 'image/jpeg' });
  1704. if(newFile.size > file.size) {
  1705. toBreak = true;
  1706. } else {
  1707. file = newFile;
  1708. }
  1709. resolve();
  1710. };
  1711. img.src = URL.createObjectURL(file);
  1712. });
  1713. if(toBreak || i++ > 5) break;
  1714. }
  1715. if(file.size > 5000000) {
  1716. return alert("Images max size is 5mb");
  1717. }
  1718. }
  1719. images.push(file);
  1720. }
  1721. }
  1722. // either up to 4 images or 1 video or 1 gif
  1723. if (images.length > 0) {
  1724. if (images.length > 4) {
  1725. images = images.slice(0, 4);
  1726. }
  1727. if (videos.length > 0 || gifs.length > 0) {
  1728. return alert("Images and videos max count is 4");
  1729. }
  1730. }
  1731. if (videos.length > 0) {
  1732. if (images.length > 0 || gifs.length > 0 || videos.length > 1) {
  1733. return alert("Videos max count is 1");
  1734. }
  1735. }
  1736. if (gifs.length > 0) {
  1737. if (images.length > 0 || videos.length > 0 || gifs.length > 1) {
  1738. return alert("Gifs max count is 1");
  1739. }
  1740. }
  1741. // get base64 data
  1742. let media = [...images, ...videos, ...gifs];
  1743. let base64Data = [];
  1744. for (let i = 0; i < media.length; i++) {
  1745. let file = media[i];
  1746. let reader = new FileReader();
  1747. reader.readAsArrayBuffer(file);
  1748. reader.onload = () => {
  1749. base64Data.push(reader.result);
  1750. if (base64Data.length === media.length) {
  1751. while (mediaArray.length >= 4) {
  1752. mediaArray.pop();
  1753. mediaContainer.lastChild.remove();
  1754. }
  1755. base64Data.forEach(data => {
  1756. let div = document.createElement('div');
  1757. let img = document.createElement('img');
  1758. div.title = file.name;
  1759. div.id = `new-tweet-media-img-${Date.now()}${Math.random()}`.replace('.', '-');
  1760. div.className = "new-tweet-media-img-div";
  1761. img.className = "new-tweet-media-img";
  1762. let progress = document.createElement('span');
  1763. progress.hidden = true;
  1764. progress.className = "new-tweet-media-img-progress";
  1765. let remove = document.createElement('span');
  1766. remove.className = "new-tweet-media-img-remove";
  1767. let alt;
  1768. if (!file.type.includes('video')) {
  1769. alt = document.createElement('span');
  1770. alt.className = "new-tweet-media-img-alt";
  1771. alt.innerText = "ALT";
  1772. alt.addEventListener('click', () => {
  1773. mediaObject.alt = prompt("Alt text", mediaObject.alt || '');
  1774. });
  1775. }
  1776. let cw = document.createElement('span');
  1777. cw.className = "new-tweet-media-img-cw";
  1778. cw.innerText = "CW";
  1779. cw.addEventListener('click', () => {
  1780. createModal(`
  1781. <div class="cw-modal" style="color:var(--almost-black)">
  1782. <h2 class="nice-header">Content warnings</h2>
  1783. <br>
  1784. <input type="checkbox" id="cw-modal-graphic_violence"${mediaObject.cw.includes('graphic_violence') ? ' checked' : ''}> <label for="cw-modal-graphic_violence">Graphic violence</label><br>
  1785. <input type="checkbox" id="cw-modal-adult_content"${mediaObject.cw.includes('adult_content') ? ' checked' : ''}> <label for="cw-modal-adult_content">Adult content</label><br>
  1786. <input type="checkbox" id="cw-modal-other"${mediaObject.cw.includes('other') ? ' checked' : ''}> <label for="cw-modal-other">Sensitive content</label><br>
  1787. </div>
  1788. `);
  1789. let graphic_violence = document.getElementById('cw-modal-graphic_violence');
  1790. let adult_content = document.getElementById('cw-modal-adult_content');
  1791. let sensitive_content = document.getElementById('cw-modal-other');
  1792. [graphic_violence, adult_content, sensitive_content].forEach(checkbox => {
  1793. checkbox.addEventListener('change', () => {
  1794. if (checkbox.checked) {
  1795. mediaObject.cw.push(checkbox.id.slice(9));
  1796. } else {
  1797. let index = mediaObject.cw.indexOf(checkbox.id.slice(9));
  1798. if (index > -1) {
  1799. mediaObject.cw.splice(index, 1);
  1800. }
  1801. }
  1802. });
  1803. });
  1804. });
  1805.  
  1806. let mediaObject = {
  1807. div, img,
  1808. id: div.id,
  1809. data: data,
  1810. type: file.type,
  1811. cw: [],
  1812. category: file.type.includes('gif') ? (is_dm ? 'dm_gif' : 'tweet_gif') : file.type.includes('video') ? (is_dm ? 'dm_video' : 'tweet_video') : (is_dm ? 'dm_image' : 'tweet_image')
  1813. };
  1814. mediaArray.push(mediaObject);
  1815. if(file.type.includes('video')) {
  1816. img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAWUSURBVHhe7Z1pqG5THMbPNV1jul1TJEOZuqYMRZEpoRARvlw+uIjwASlRFIkMHwzJ8AVfZMhYOGRKESlDkciQyJhknj3PXu9b3nP2sPba9x3Wfp5f/dpr77p1zl7Ped+11l77f5fMz8/PGV3WGByNKA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOG3eC1gGl4ammXF+h9+HZj0xAdgC3gwPhw5AHjAAL8Kz4Re8UEVTANaCT8HDijOTGy9B9t1fxVkJTWOAneAhoWky5ADIPqykKQCbQA8U84V9xz6spKlzlwyOJl9q+9B/3eI4AOI0zQIOhs+H5iJeh3fBP4qzcjaDF8DNizPTls/gDfCH4qycDeBZcLfibDEcxL8QmotJDQA7fVf4QXFWz8nwvtA0LTkJPhCatewM34LrFGej1AYg9SvgF/hNaDby8eBo2vPp4NjEl5B90hqPAcRxAMRxAMRxAMRxAMRxAMRJDcCaA2NYe3A07Ym9d236Y4TUAGwET4VlCw//Z124MjRNAmfADUOzEnb8iZB90pouS8H/QC5A1C0FMwDcUWTS4YLbz6FZCgOwFaz6Yx7LUrDJh7EsBZue0KcA/Av/Dk0TS18CwIcm/KjbEV4Nf4Qmgr4E4ErIbdAfwUvhXvB+WLkb1gS6BICzAG5Y+KTG2EfGXVn42PRDeAo8AnLjSs5wplV2b4dy3z/7IokuATgHbtfg9vBuOA04JngOHgjPhJ/D3Lgdlt3XhV4Ek0gNAL9jH4RNg66f4J2hOTX4lgx/hj3gdbBuTj1r3At/C81KuA5zD0wa96QGgB0fO+L+c3CcNt/Bi+G+8BGYw4wh9t616Y8R+jIIbMN78AR4NHyTF5RRDADhoInvPO4Pz4NfQUlUAzCE36+3wN0h34D+FUqhHoAhX8Pz4X7wSZg8rcoNB2CUt+Ex8Hj4Li/0HQdgMRxNPwY5W+D8+lvYW1IDsD6Mfc6/zeCYG3zRgq9lcf3gDsj1hEnDRZ4YNoXsk9Z02Q/wDuRKVd3CysbwQrh1cTY+WL7m2dAcG/vAa+ChcFKvzXN2ciPkGKUK7spaBfmJVYbEhpBJBICwZA7HB1dBPnnMAW8IWY3w6SJf1twb3soLueMApMFnHJfBqFJss4wDkE4vyuc4AGlwqzafLLJ4ZtY4AO0Y7sF/A57OC7nTZRYwSyViJjEL4MDvWjjJaaBLxEQyzgBsCS+Hp8FJl8p1iZgpwpU1LmLxxnJL2TTqJLtEzBTg9/yx8DV4PayttJk7DsAo3BfwOHwYruCFvuMABDhYvQm+Co+CMvdFPQB8e/lcyH0A3Bq2HpRCNQD8vY+Er0BuBZOtZKoYgF3gQ/AJuCcvKJMaAI6UaQyzUiJmOeTyLRewjoOxP/80cYmY1QDn7yy1wvk8t3hx5SwXXCImkrKVQC7XchWMu3iqdsvkwFhLxHQZA/Dfcpl02xonVR9o4d65HSCXn5+GOXc+4X6/sns7lNvtkvuxSwBmiSsgV+/4QIQFIvi0juvo3MJlauhLAPhJ9CjkfP4SmPR9qEhfAmAScQDE6RKAWSoR02dcIkYYl4gRxyVixHGJGDNeHABxHABxHABxHABxHABxUgOgUCJmFuAiTwzyJWL6ikvEmM6MbUeQ6QEOgDhNAeB/umDyprYPmwLAKpkydXN7CPuuttJpUwDehy+HpskQDuDZh5U0zQIIN1zeBg+C0yiSYNrDsrbPQL7wyh1FlcQEYAgrYjkAecAARNUwbBMA00M8DRTHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHAZBmbu4/x6swK3hIFr4AAAAASUVORK5CYII=';
  1817. } else {
  1818. let dataBase64 = arrayBufferToBase64(data);
  1819. img.src = `data:${file.type};base64,${dataBase64}`;
  1820. }
  1821. remove.addEventListener('click', () => {
  1822. div.remove();
  1823. for (let i = mediaArray.length - 1; i >= 0; i--) {
  1824. let m = mediaArray[i];
  1825. if (m.id === div.id) mediaArray.splice(i, 1);
  1826. }
  1827. });
  1828. div.append(img, progress, remove);
  1829. if (!file.type.includes('video')) {
  1830. img.addEventListener('click', () => {
  1831. new Viewer(mediaContainer, {
  1832. transition: false,
  1833. zoomRatio: 0.3
  1834. });
  1835. });
  1836. div.append(alt);
  1837. } else {
  1838. cw.style.marginLeft = '-53px';
  1839. }
  1840. div.append(cw);
  1841. mediaContainer.append(div);
  1842. });
  1843. setTimeout(() => {
  1844. let messageModalElement = document.getElementsByClassName('messages-container')[0];
  1845. let inboxModalElement = document.getElementsByClassName('inbox-modal')[0];
  1846. if(messageModalElement) inboxModalElement.scrollTop = inboxModalElement.scrollHeight;
  1847. }, 10);
  1848. }
  1849. }
  1850. }
  1851. }
  1852. let isURL = (str) => {
  1853. try {
  1854. new URL(str);
  1855. return true;
  1856. } catch (_) {
  1857. return false;
  1858. }
  1859. }
  1860. function handleDrop(event, mediaArray, mediaContainer) {
  1861. let text = event.dataTransfer.getData("Text").trim();
  1862. if(text.length <= 1) {
  1863. event.stopPropagation();
  1864. event.preventDefault();
  1865. let files = event.dataTransfer.files;
  1866. handleFiles(files, mediaArray, mediaContainer);
  1867. }
  1868. }
  1869. function getMedia(mediaArray, mediaContainer, is_dm = false) {
  1870. let input = document.createElement('input');
  1871. input.type = 'file';
  1872. input.multiple = true;
  1873. input.accept = 'image/jpeg,image/png,image/webp,image/gif,video/mp4,video/quicktime';
  1874. input.addEventListener('change', () => {
  1875. handleFiles(input.files, mediaArray, mediaContainer, is_dm);
  1876. });
  1877. input.click();
  1878. };
  1879. function timeElapsed(targetTimestamp) {
  1880. let currentDate = new Date();
  1881. let currentTimeInms = currentDate.getTime();
  1882. let targetDate = new Date(targetTimestamp);
  1883. let targetTimeInms = targetDate.getTime();
  1884. let elapsed = Math.floor((currentTimeInms - targetTimeInms) / 1000);
  1885.  
  1886. if (elapsed < 1) {
  1887. return 'now';
  1888. }
  1889. if (elapsed < 60) { //< 60 sec
  1890. return `${elapsed}s`;
  1891. }
  1892. if (elapsed < 3600) { //< 60 minutes
  1893. return `${Math.floor(elapsed / (60))}m`;
  1894. }
  1895. if (elapsed < 86400) { //< 24 hours
  1896. return `${Math.floor(elapsed / (3600))}h`;
  1897. }
  1898. if (elapsed < 604800) { //<7 days
  1899. return `${Math.floor(elapsed / (86400))}d`;
  1900. }
  1901. if (targetDate.getFullYear() == currentDate.getFullYear()) { // same years
  1902. return targetDate.toLocaleDateString("en-US", { month: 'long', day: 'numeric' });
  1903. }
  1904. //more than last years
  1905. return targetDate.toLocaleDateString("en-US", { year: 'numeric', month: 'long', day: 'numeric' });
  1906. }
  1907.  
  1908.  
  1909. async function renderTweetBodyHTML(t, is_quoted) {
  1910. let result = "",
  1911. last_pos = 0,
  1912. index_map = {}; // {start_position: [end_position, replacer_func]}
  1913. hashflags = [];
  1914.  
  1915. if(is_quoted) t = t.quoted_status;
  1916.  
  1917. full_text_array = Array.from(t.full_text);
  1918.  
  1919. if (t.entities.richtext) {
  1920. t.entities.richtext.forEach(snippet => {
  1921. //if i felt like it, id write a long-winded series of comments on how much i hate emojis. but i'll refrain
  1922. //and this *still* doesnt work properly with flag emojis
  1923. //im just glad it works at all
  1924.  
  1925. let textBeforeSnippet = t.full_text.slice(0, snippet.from_index);
  1926. let emojisBeforeSnippet = textBeforeSnippet.match(/\p{Extended_Pictographic}/gu);
  1927. emojisBeforeSnippet = emojisBeforeSnippet ? emojisBeforeSnippet.length : 0;
  1928.  
  1929. let fromIndex = snippet.from_index - emojisBeforeSnippet;
  1930. let toIndex = snippet.to_index - emojisBeforeSnippet;
  1931.  
  1932. index_map[fromIndex] = [
  1933. toIndex,
  1934. text => {
  1935. let snippetText = escapeHTML(full_text_array.slice(fromIndex, toIndex).join(''));
  1936. let startingTags = `${snippet.richtext_types.includes('Bold') ? '<b>' : ''}${snippet.richtext_types.includes('Italic') ? '<i>' : ''}`;
  1937. let endingTags = `${snippet.richtext_types.includes('Bold') ? '</b>' : ''}${snippet.richtext_types.includes('Italic') ? '</i>' : ''}`;
  1938.  
  1939. return `${startingTags}${snippetText}${endingTags}`;
  1940. }
  1941. ];
  1942. });
  1943. }
  1944.  
  1945. if (is_quoted) { // for quoted tweet we need only hashflags and readable urls
  1946. if (t.entities.hashtags) {
  1947. t.entities.hashtags.forEach(hashtag => {
  1948. let hashflag = hashflags.find(h => h.hashtag.toLowerCase() === hashtag.text.toLowerCase());
  1949. index_map[hashtag.indices[0]] = [hashtag.indices[1], text =>
  1950. `#${escapeHTML(hashtag.text)}`+
  1951. `${hashflag ? `<img src="${hashflag.asset_url}" class="hashflag">` : ''}`];
  1952. });
  1953. };
  1954.  
  1955. if (t.entities.urls) {
  1956. t.entities.urls.forEach(url => {
  1957. index_map[url.indices[0]] = [url.indices[1], text => `${escapeHTML(url.display_url)}`];
  1958. });
  1959. };
  1960. } else {
  1961. if (t.entities.hashtags) {
  1962. t.entities.hashtags.forEach(hashtag => {
  1963. let hashflag = hashflags.find(h => h.hashtag.toLowerCase() === hashtag.text.toLowerCase());
  1964. index_map[hashtag.indices[0]] = [hashtag.indices[1], text => `<a href="/hashtag/${escapeHTML(hashtag.text)}">`+
  1965. `#${escapeHTML(hashtag.text)}`+
  1966. `${hashflag ? `<img src="${hashflag.asset_url}" class="hashflag">` : ''}`+
  1967. `</a>`];
  1968. });
  1969. };
  1970.  
  1971. if (t.entities.symbols) {
  1972. t.entities.symbols.forEach(symbol => {
  1973. index_map[symbol.indices[0]] = [symbol.indices[1], text => `<a href="/search?q=%24${escapeHTML(symbol.text)}">`+
  1974. `$${escapeHTML(symbol.text)}`+
  1975. `</a>`];
  1976. });
  1977. }
  1978.  
  1979. if (t.entities.urls) {
  1980. t.entities.urls.forEach(url => {
  1981. index_map[url.indices[0]] = [url.indices[1], text =>
  1982. `<a href="${escapeHTML(url.expanded_url)}" title="${escapeHTML(url.expanded_url)}" target="_blank" rel="noopener noreferrer">`+
  1983. `${escapeHTML(url.display_url)}</a>`];
  1984. });
  1985. };
  1986.  
  1987. if (t.entities.user_mentions) {
  1988. t.entities.user_mentions.forEach(user => {
  1989. index_map[user.indices[0]] = [user.indices[1], text => `<a href="/${escapeHTML(user.screen_name)}">${escapeHTML(text)}</a>`];
  1990. });
  1991. };
  1992.  
  1993. if(t.entities.media) {
  1994. t.entities.media.forEach(media => {
  1995. index_map[media.indices[0]] = [media.indices[1], text => ``];
  1996. });
  1997. }
  1998. };
  1999.  
  2000. let display_start = t.display_text_range !== undefined ? t.display_text_range[0] : 0;
  2001. let display_end = t.display_text_range !== undefined ? t.display_text_range[1] : full_text_array.length;
  2002. for (let [current_pos, _] of full_text_array.entries()) {
  2003. if (current_pos < display_start) { // do not render first part of message
  2004. last_pos = current_pos + 1; // to start copy from next symbol
  2005. continue;
  2006. }
  2007. if (current_pos == display_end || // reached the end of visible part
  2008. current_pos == full_text_array.length - 1) { // reached the end of tweet itself
  2009. if (display_end == full_text_array.length) current_pos++; // dirty hack to include last element of slice
  2010. result += escapeHTML(full_text_array.slice(last_pos, current_pos).join(''));
  2011. break;
  2012. }
  2013. if (current_pos > display_end) {
  2014. break; // do not render last part of message
  2015. }
  2016.  
  2017. if (current_pos in index_map) {
  2018. let [end, func] = index_map[current_pos];
  2019. if (current_pos > last_pos) {
  2020. result += escapeHTML(full_text_array.slice(last_pos, current_pos).join('')); // store chunk of untouched text
  2021. }
  2022. result += func(full_text_array.slice(current_pos, end).join('')); // run replacer func on corresponding range
  2023. last_pos = end;
  2024. }
  2025. }
  2026. return result
  2027. }
  2028. function arrayInsert(arr, index, value) {
  2029. return [...arr.slice(0, index), value, ...arr.slice(index)];
  2030. }
  2031. function generatePoll(tweet, tweetElement, user) {
  2032. let pollElement = tweetElement.getElementsByClassName('tweet-card')[0];
  2033. pollElement.innerHTML = '';
  2034. let poll = tweet.card.binding_values;
  2035. let choices = Object.keys(poll).filter(key => key.endsWith('label')).map((key, i) => ({
  2036. label: poll[key].string_value,
  2037. count: poll[key.replace('label', 'count')] ? +poll[key.replace('label', 'count')].string_value : 0,
  2038. id: parseInt(key.replace(/[^0-9]/g, ''))
  2039. }));
  2040. choices.sort((a, b) => a.id - b.id);
  2041. let voteCount = choices.reduce((acc, cur) => acc + cur.count, 0);
  2042. if(poll.selected_choice || user.id_str === tweet.user.id_str || (poll.counts_are_final && poll.counts_are_final.boolean_value)) {
  2043. for(let i in choices) {
  2044. let choice = choices[i];
  2045. if(user.id_str !== tweet.user.id_str && poll.selected_choice && choice.id === +poll.selected_choice.string_value) {
  2046. choice.selected = true;
  2047. }
  2048. choice.percentage = Math.round(choice.count / voteCount * 100) || 0;
  2049. let choiceElement = document.createElement('div');
  2050. choiceElement.classList.add('choice');
  2051. choiceElement.innerHTML = html`
  2052. <div class="choice-bg" style="width:${choice.percentage}%" data-percentage="${choice.percentage}"></div>
  2053. <div class="choice-label">
  2054. <span>${escapeHTML(choice.label)}</span>
  2055. ${choice.selected ? `<span class="choice-selected"></span>` : ''}
  2056. </div>
  2057. ${isFinite(choice.percentage) ? `<div class="choice-count">${choice.count} (${choice.percentage}%)</div>` : '<div class="choice-count">0</div>'}
  2058. `;
  2059. pollElement.append(choiceElement);
  2060. }
  2061. } else {
  2062. for(let i in choices) {
  2063. let choice = choices[i];
  2064. let choiceElement = document.createElement('div');
  2065. choiceElement.classList.add('choice', 'choice-unselected');
  2066. choiceElement.classList.add('tweet-button');
  2067. choiceElement.innerHTML = html`
  2068. <div class="choice-bg" style="width:100%"></div>
  2069. <div class="choice-label">${escapeHTML(choice.label)}</div>
  2070. `;
  2071. choiceElement.addEventListener('click', async () => {
  2072. let newCard = await API.tweet.vote(poll.api.string_value, tweet.id_str, tweet.card.url, tweet.card.name, choice.id);
  2073. tweet.card = newCard.card;
  2074. generateCard(tweet, tweetElement, user);
  2075. });
  2076. pollElement.append(choiceElement);
  2077. }
  2078. }
  2079. if(tweet.card.url.startsWith('card://')) {
  2080. let footer = document.createElement('span');
  2081. footer.classList.add('poll-footer');
  2082. let endsAtMessage = `Ends at: ${new Date(poll.end_datetime_utc.string_value).toLocaleString()}`;
  2083. footer.innerHTML = html`${voteCount} ${voteCount === 1 ? 'vote' : 'votes'}${(!poll.counts_are_final || !poll.counts_are_final.boolean_value) && poll.end_datetime_utc ? ` ${endsAtMessage}` : ''}`;
  2084. pollElement.append(footer);
  2085. }
  2086. }
  2087. function generateCard(tweet, tweetElement, user) {
  2088. if(!tweet.card) return;
  2089. if(tweet.card.name === 'promo_image_convo' || tweet.card.name === 'promo_video_convo') {
  2090. let vals = tweet.card.binding_values;
  2091. let a = document.createElement('a');
  2092. a.title = vals.thank_you_text.string_value;
  2093. if(tweet.card.name === 'promo_image_convo') {
  2094. a.href = vals.thank_you_url ? vals.thank_you_url.string_value : "#";
  2095. a.target = '_blank';
  2096. let img = document.createElement('img');
  2097. let imgValue = vals.promo_image;
  2098. if(!imgValue) {
  2099. imgValue = vals.cover_promo_image_original;
  2100. }
  2101. if(!imgValue) {
  2102. imgValue = vals.cover_promo_image_large;
  2103. }
  2104. if(!imgValue) {
  2105. return;
  2106. }
  2107. img.src = imgValue.image_value.url;
  2108. let [w, h] = sizeFunctions[1](imgValue.image_value.width, imgValue.image_value.height);
  2109. img.width = w;
  2110. img.height = h;
  2111. img.className = 'tweet-media-element';
  2112. a.append(img);
  2113. } else {
  2114. let overlay = document.createElement('div');
  2115. overlay.innerHTML = html`
  2116. <svg viewBox="0 0 24 24" class="tweet-media-video-overlay-play">
  2117. <g>
  2118. <path class="svg-play-path" d="M8 5v14l11-7z"></path>
  2119. <path d="M0 0h24v24H0z" fill="none"></path>
  2120. </g>
  2121. </svg>
  2122. `;
  2123. overlay.className = 'tweet-media-video-overlay';
  2124. overlay.addEventListener('click', async e => {
  2125. e.preventDefault();
  2126. e.stopImmediatePropagation();
  2127. try {
  2128. let res = await GM_fetch(vid.currentSrc); // weird problem with vids breaking cuz twitter sometimes doesnt send content-length
  2129. if(!res.headers.get('content-length')) await sleep(1000);
  2130. } catch(e) {
  2131. console.error(e);
  2132. }
  2133. vid.play();
  2134. vid.controls = true;
  2135. vid.classList.remove('tweet-media-element-censor');
  2136. overlay.style.display = 'none';
  2137. });
  2138. let vid = document.createElement('video');
  2139. let [w, h] = sizeFunctions[1](vals.player_image_original.image_value.width, vals.player_image_original.image_value.height);
  2140. vid.width = w;
  2141. vid.height = h;
  2142. vid.preload = 'none';
  2143. vid.poster = vals.player_image_large.image_value.url;
  2144. vid.className = 'tweet-media-element';
  2145. vid.addEventListener('click', async e => {
  2146. e.preventDefault();
  2147. e.stopImmediatePropagation();
  2148. });
  2149. GM_fetch(vals.player_stream_url.string_value).then(res => res.text()).then(blob => {
  2150. let xml = new DOMParser().parseFromString(blob, 'text/xml');
  2151. let MediaFile = xml.getElementsByTagName('MediaFile')[0];
  2152. vid.src = MediaFile.textContent.trim();
  2153. });
  2154. let tweetMedia = document.createElement('div');
  2155. tweetMedia.className = 'tweet-media';
  2156. tweetMedia.style.right = 'unset';
  2157. tweetMedia.append(overlay, vid);
  2158. a.append(tweetMedia);
  2159. }
  2160. let ctas = [];
  2161. if(vals.cta_one) {
  2162. ctas.push([vals.cta_one, vals.cta_one_tweet]);
  2163. }
  2164. if(vals.cta_two) {
  2165. ctas.push([vals.cta_two, vals.cta_two_tweet]);
  2166. }
  2167. if(vals.cta_three) {
  2168. ctas.push([vals.cta_three, vals.cta_three_tweet]);
  2169. }
  2170. if(vals.cta_four) {
  2171. ctas.push([vals.cta_four, vals.cta_four_tweet]);
  2172. }
  2173. } else if(tweet.card.name === "player") {
  2174. let iframe = document.createElement('iframe');
  2175. iframe.src = tweet.card.binding_values.player_url.string_value.replace("youtube.com", "youtube-nocookie.com").replace("autoplay=true", "autoplay=false").replace("autoplay=1", "autoplay=0");
  2176. iframe.classList.add('tweet-player');
  2177. let [w, h] = sizeFunctions[1](+tweet.card.binding_values.player_width.string_value, +tweet.card.binding_values.player_height.string_value);
  2178. iframe.width = w;
  2179. iframe.height = h;
  2180. iframe.loading = 'lazy';
  2181. iframe.allowFullscreen = true;
  2182. tweetElement.getElementsByClassName('tweet-card')[0].innerHTML = '';
  2183. tweetElement.getElementsByClassName('tweet-card')[0].append(iframe);
  2184. } else if(tweet.card.name === "unified_card") {
  2185. let uc = JSON.parse(tweet.card.binding_values.unified_card.string_value);
  2186. for(let cn of uc.components) {
  2187. let co = uc.component_objects[cn];
  2188. if(co.type === "media") {
  2189. let media = uc.media_entities[co.data.id];
  2190.  
  2191. if(media.type === "photo") {
  2192. let img = document.createElement('img');
  2193. img.className = 'tweet-media-element';
  2194. let [w, h] = sizeFunctions[1](media.original_info.width, media.original_info.height);
  2195. img.width = w;
  2196. img.height = h;
  2197. img.loading = 'lazy';
  2198. img.src = media.media_url_https;
  2199. img.addEventListener('click', () => {
  2200. new Viewer(img, {
  2201. transition: false,
  2202. zoomRatio: 0.3
  2203. });
  2204. });
  2205. tweetElement.getElementsByClassName('tweet-card')[0].append(img, document.createElement('br'));
  2206. } else if(media.type === "animated_gif" || media.type === "video") {
  2207. let video = document.createElement('video');
  2208. video.className = 'tweet-media-element tweet-media-element-one';
  2209. let [w, h] = sizeFunctions[1](media.original_info.width, media.original_info.height);
  2210. video.width = w;
  2211. video.height = h;
  2212. video.crossOrigin = 'anonymous';
  2213. video.loading = 'lazy';
  2214. video.controls = true;
  2215. if(!media.video_info) {
  2216. console.log(`bug found in ${tweet.id_str}, please report this message to https://github.com/dimdenGD/OldTwitter/issues`, tweet);
  2217. continue;
  2218. };
  2219. let variants = media.video_info.variants.sort((a, b) => {
  2220. if(!b.bitrate) return -1;
  2221. return b.bitrate-a.bitrate;
  2222. });
  2223. for(let v in variants) {
  2224. let source = document.createElement('source');
  2225. source.src = variants[v].url;
  2226. source.type = variants[v].content_type;
  2227. video.append(source);
  2228. }
  2229. tweetElement.getElementsByClassName('tweet-card')[0].append(video, document.createElement('br'));
  2230. }
  2231. } else if(co.type === "app_store_details") {
  2232. let app = uc.app_store_data[uc.destination_objects[co.data.destination].data.app_id][0];
  2233. let appElement = document.createElement('div');
  2234. appElement.classList.add('tweet-app-info');
  2235. appElement.innerHTML = html`
  2236. <h3>${escapeHTML(app.title.content)}</h3>
  2237. <span>${escapeHTML(app.category.content)}</span>
  2238. <br>
  2239. `;
  2240. tweetElement.getElementsByClassName('tweet-card')[0].append(appElement);
  2241. } else if(co.type === "button_group") {
  2242. let buttonGroup = document.createElement('div');
  2243. buttonGroup.classList.add('tweet-button-group');
  2244. for(let b of co.data.buttons) {
  2245. let app = uc.app_store_data[uc.destination_objects[b.destination].data.app_id][0];
  2246. let button = document.createElement('a');
  2247. button.href = `http://play.google.com/store/apps/details?id=${app.id}`;
  2248. button.target = '_blank';
  2249. button.className = `nice-button tweet-app-button tweet-app-button-${b.style}`
  2250. button.innerText = b.action[0].toUpperCase() + b.action.slice(1);
  2251. buttonGroup.append(button);
  2252. }
  2253. tweetElement.getElementsByClassName('tweet-card')[0].append(buttonGroup);
  2254. }
  2255. }
  2256. } else if(tweet.card.name === "summary" || tweet.card.name === "summary_large_image") {
  2257. let vals = tweet.card.binding_values;
  2258. let a = document.createElement('a');
  2259. let url = vals.card_url.string_value;
  2260. if(tweet.entities && tweet.entities.urls) {
  2261. let urlEntity = tweet.entities.urls.find(u => u.url === url);
  2262. if(urlEntity) {
  2263. url = urlEntity.expanded_url;
  2264. }
  2265. }
  2266. a.target = '_blank';
  2267. a.href = url;
  2268. a.className = 'tweet-card-link yeah-box';
  2269. a.innerHTML = html`
  2270. ${vals.thumbnail_image ? `<img src="${vals.thumbnail_image.image_value.url}" class="tweet-card-link-thumbnail">` : ''}
  2271. <div class="tweet-card-link-text">
  2272. ${vals.vanity_url ? `<span class="tweet-card-link-vanity">${escapeHTML(vals.vanity_url.string_value)}</span><br>` : ''}
  2273. ${vals.title ? `<h3 class="tweet-card-link-title">${escapeHTML(vals.title.string_value)}</h3>` : ''}
  2274. ${vals.description ? `<span class="tweet-card-link-description">${escapeHTML(vals.description.string_value)}</span>` : ''}
  2275. </div>
  2276. `;
  2277. tweetElement.getElementsByClassName('tweet-card')[0].append(a);
  2278. } else if(tweet.card.url.startsWith('card://')) {
  2279. generatePoll(tweet, tweetElement, user);
  2280. }
  2281. }
  2282. function createEmojiPicker(container, input, style = {}) {
  2283. let picker = new EmojiPicker();
  2284. for(let i in style) {
  2285. picker.style[i] = style[i];
  2286. }
  2287. picker.className = isDarkModeEnabled ? 'dark' : 'light';
  2288. picker.addEventListener('emoji-click', e => {
  2289. let pos = input.selectionStart;
  2290. let text = input.value;
  2291. input.value = text.slice(0, pos) + e.detail.unicode + text.slice(pos);
  2292. input.selectionStart = pos + e.detail.unicode.length;
  2293. });
  2294. container.append(picker);
  2295.  
  2296. let observer;
  2297.  
  2298. setTimeout(() => {
  2299. function oc (e) {
  2300. if (picker.contains(e.target)) return;
  2301. if(observer) {
  2302. observer.disconnect();
  2303. }
  2304. picker.remove();
  2305. document.removeEventListener('click', oc);
  2306. picker.database.close();
  2307. }
  2308. document.addEventListener('click', oc);
  2309. picker.shadowRoot.querySelector("input.search").focus();
  2310. }, 100);
  2311.  
  2312. return picker;
  2313. }
  2314. function isEmojiOnly(str) {
  2315. const stringToTest = str.replace(/ /g,'');
  2316. const emojiRegex = /^(?:(?:\p{RI}\p{RI}|\p{Emoji}(?:\p{Emoji_Modifier}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?(?:\u{200D}\p{Emoji}(?:\p{Emoji_Modifier}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?)*)|[\u{1f900}-\u{1f9ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}])+$/u;
  2317. return emojiRegex.test(stringToTest) && Number.isNaN(Number(stringToTest));
  2318. }
  2319.  
  2320. function renderMedia(t) {
  2321. let _html = '';
  2322. if(!t.extended_entities || !t.extended_entities.media) return '';
  2323.  
  2324. let cws = [];
  2325.  
  2326. for(let i = 0; i < t.extended_entities.media.length; i++) {
  2327. let m = t.extended_entities.media[i];
  2328. let toCensor = t.possibly_sensitive;
  2329. if(m.type === 'photo') {
  2330. let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height);
  2331. _html += html`
  2332. <img
  2333. ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text.replaceAll('"', "'"))}" title="${escapeHTML(m.ext_alt_text.replaceAll('"', "'"))}"` : ''}
  2334. crossorigin="anonymous"
  2335. width="${w}"
  2336. height="${h}"
  2337. loading="lazy"
  2338. src="${m.media_url_https + (false && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : window.navigator && navigator.connection && navigator.connection.type === 'cellular' ? '?name=small' : '')}"
  2339. class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
  2340. >`;
  2341. } else if(m.type === 'animated_gif') {
  2342. let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height);
  2343. let rid = m.id_str + m.media_key;
  2344. _html += html`
  2345. <video
  2346. ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''}
  2347. crossorigin="anonymous"
  2348. width="${w}"
  2349. height="${h}"
  2350. loop
  2351. disableRemotePlayback
  2352. autoplay
  2353. muted
  2354. class="tweet-media-element tweet-media-gif ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
  2355. >
  2356. ${m.video_info.variants.map(v => `<source src="${v.url}" type="${v.content_type}">`).join('\n')}
  2357. Unsupported video
  2358. </video>
  2359. `;
  2360. } else if(m.type === 'video') {
  2361. if(m.mediaStats && m.mediaStats.viewCount) {
  2362. m.ext = {
  2363. mediaStats: { r: { ok: { viewCount: m.mediaStats.viewCount } } }
  2364. }
  2365. }
  2366. let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height);
  2367. _html += html`
  2368. <video
  2369. ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''}
  2370. crossorigin="anonymous"
  2371. width="${w}"
  2372. height="${h}"
  2373. preload="none"
  2374. disableRemotePlayback
  2375. ${t.extended_entities.media.length > 1 ? 'controls' : ''}
  2376. poster="${m.media_url_https}"
  2377. class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
  2378. >
  2379. ${m.video_info.variants.map(v => `<source src="${v.url}" type="${v.content_type}">`).join('\n')}
  2380. Unsupported video
  2381. </video>
  2382. `;
  2383. }
  2384. if(i === 1 && t.extended_entities.media.length > 3) {
  2385. _html += '<br>';
  2386. }
  2387. }
  2388.  
  2389. if(cws.length > 0) {
  2390. cws = [...new Set(cws)];
  2391. cws = "Content warnings: " + cws.join(', ');
  2392. _html += html`<br><div class="tweet-media-cws">${cws}</div>`;
  2393. }
  2394. return _html;
  2395. }
  2396.  
  2397.  
  2398. function openInNewTab(href) {
  2399. Object.assign(document.createElement('a'), {
  2400. target: '_blank',
  2401. rel: 'noopener noreferrer',
  2402. href: href,
  2403. }).click();
  2404. }
  2405.  
  2406. function sleep(ms) {
  2407. return new Promise(resolve => setTimeout(resolve, ms));
  2408. }
  2409.  
  2410. async function appendTweet(t, timelineContainer, options = {}, user) {
  2411. if(typeof t !== 'object') {
  2412. console.error('Tweet is undefined', t, timelineContainer, options);
  2413. return;
  2414. }
  2415. if(typeof t.user !== 'object') {
  2416. console.error('Tweet user is undefined', t, timelineContainer, options);
  2417. return;
  2418. }
  2419. try {
  2420. // verification
  2421. if(t.user.ext_verified_type) {
  2422. t.user.verified_type = t.user.ext_verified_type;
  2423. t.user.verified = true;
  2424. }
  2425. if(t.user.ext && t.user.ext.isBlueVerified && t.user.ext.isBlueVerified.r && t.user.ext.isBlueVerified.r.ok) {
  2426. t.user.verified_type = "Blue";
  2427. t.user.verified = true;
  2428. }
  2429. if(t.user && t.user.ext && t.user.ext.verifiedType && t.user.ext.verifiedType.r && t.user.ext.verifiedType.r.ok) {
  2430. t.user.verified_type = t.user.ext.verifiedType.r.ok;
  2431. t.user.verified = true;
  2432. }
  2433. if(t.quoted_status && t.quoted_status.user.verified_type === "Blue") {
  2434. delete t.quoted_status.user.verified_type;
  2435. t.quoted_status.user.verified = false;
  2436. }
  2437.  
  2438. const tweet = document.createElement('div');
  2439. tweet.tweet = t;
  2440. t.element = tweet;
  2441. t.options = options;
  2442.  
  2443. if(!options.mainTweet) {
  2444. tweet.addEventListener('click', e => {
  2445. if(!e.target.closest(".tweet-button") && !e.target.closest(".tweet-body-text-span") && !e.target.closest(".tweet-edit-section") && !e.target.closest(".dropdown-menu") && !e.target.closest(".tweet-media-element") && !e.target.closest("a") && !e.target.closest("button")) {
  2446. let tweetData = t;
  2447. if(tweetData.retweeted_status) tweetData = tweetData.retweeted_status;
  2448. tweet.classList.add('tweet-preload');
  2449. let selection = window.getSelection();
  2450. if(selection.toString().length > 0 && selection.focusNode && selection.focusNode.closest(`div.tweet[data-tweet-id="${tweetData.id_str}"]`)) {
  2451. return;
  2452. }
  2453. let a = document.createElement('a');
  2454. a.href = `/${tweetData.user.screen_name}/status/${tweetData.id_str}`;
  2455. a.target = '_blank';
  2456. a.click();
  2457. }
  2458. });
  2459. }
  2460. tweet.addEventListener('mousedown', e => {
  2461. if(e.button === 1) {
  2462. // tweet-media-element is clickable, since it should open the tweet in a new tab.
  2463. if(!e.target.closest(".tweet-button") && !e.target.closest(".tweet-edit-section") && !e.target.closest(".dropdown-menu") && !e.target.closest("a") && !e.target.closest("button")) {
  2464. e.preventDefault();
  2465. openInNewTab(`/${t.user.screen_name}/status/${t.id_str}`);
  2466. }
  2467. }
  2468. });
  2469. tweet.tabIndex = -1;
  2470. tweet.className = `yeah-tweet ${options.mainTweet ? 'tweet-main' : location.pathname.includes('/status/') ? 'tweet-replying' : ''}`.trim();
  2471. tweet.dataset.tweetId = t.id_str;
  2472. tweet.dataset.userId = t.user.id_str;
  2473. try {
  2474. if(!activeTweet) {
  2475. tweet.classList.add('tweet-active');
  2476. activeTweet = tweet;
  2477. }
  2478. } catch(e) {};
  2479.  
  2480. if(t.nonReply) {
  2481. tweet.classList.add('tweet-non-reply');
  2482. }
  2483.  
  2484. if(t.threadContinuation) {
  2485. options.threadContinuation = true;
  2486. }
  2487. if(t.noTop) {
  2488. options.noTop = true;
  2489. }
  2490. if (options.threadContinuation) tweet.classList.add('tweet-self-thread-continuation');
  2491. if (options.selfThreadContinuation) tweet.classList.add('tweet-self-thread-continuation');
  2492.  
  2493. if (options.noTop) tweet.classList.add('tweet-no-top');
  2494. let full_text = t.full_text ? t.full_text : '';
  2495. let tweetLanguage = t.lang; // originally i used i18n api to detect languages simply because i didn't know of t.lang existence
  2496. if(!tweetLanguage) {
  2497. tweetLanguage = 'und';
  2498. }
  2499. if(tweetLanguage.includes('-')) {
  2500. let [lang, country] = tweetLanguage.split('-');
  2501. tweetLanguage = `${lang}_${country.toUpperCase()}`;
  2502. }
  2503. let videos = t.extended_entities && t.extended_entities.media && t.extended_entities.media.filter(m => m.type === 'video');
  2504. if(!videos || videos.length === 0) {
  2505. videos = undefined;
  2506. }
  2507. if(videos) {
  2508. for(let v of videos) {
  2509. if(!v.video_info) continue;
  2510. v.video_info.variants = v.video_info.variants.sort((a, b) => {
  2511. if(!b.bitrate) return -1;
  2512. return b.bitrate-a.bitrate;
  2513. });
  2514. }
  2515. }
  2516. if(full_text.includes("Learn more")) {
  2517. console.log(t);
  2518. }
  2519. if(t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) {
  2520. full_text = "";
  2521. }
  2522. if(!t.quoted_status) { //t.quoted_status is undefined if the user blocked the quoter (this also applies to deleted/private tweets too, but it just results in original behavior then)
  2523. try {
  2524. if(t.quoted_status_result && t.quoted_status_result.result.tweet) {
  2525. t.quoted_status = t.quoted_status_result.result.tweet.legacy;
  2526. t.quoted_status.user = t.quoted_status_result.result.tweet.core.user_results.result.legacy;
  2527. }/* else if(t.quoted_status_id_str) {
  2528. t.quoted_status = await API.tweet.getV2(t.quoted_status_id_str);
  2529. console.log(t.quoted_status);
  2530. }*/
  2531. } catch {
  2532. t.quoted_status = undefined;
  2533. }
  2534. }
  2535. let mentionedUserText = ``;
  2536. let quoteMentionedUserText = ``;
  2537. if(t.in_reply_to_screen_name && t.display_text_range) {
  2538. t.entities.user_mentions.forEach(user_mention => {
  2539. if(user_mention.indices[0] < t.display_text_range[0]){
  2540. mentionedUserText += `<a href="/${user_mention.screen_name}">@${user_mention.screen_name}</a> `
  2541. }
  2542. //else this is not reply but mention
  2543. });
  2544. }
  2545. if(t.quoted_status && t.quoted_status.in_reply_to_screen_name && t.display_text_range) {
  2546. t.quoted_status.entities.user_mentions.forEach(user_mention => {
  2547. if(user_mention.indices[0] < t.display_text_range[0]){
  2548. quoteMentionedUserText += `@${user_mention.screen_name} `
  2549. }
  2550. //else this is not reply but mention
  2551. });
  2552. }
  2553. // i fucking hate this thing
  2554. tweet.innerHTML = html`
  2555. <div class="tweet-top" hidden></div>
  2556. <a class="tweet-avatar-link" href="/${t.user.screen_name}">
  2557. <img
  2558. src="${`${t.user.profile_image_url_https}`.replace("_normal.", "_bigger.")}"
  2559. alt="${t.user.name}"
  2560. class="tweet-avatar"
  2561. width="48"
  2562. height="48"
  2563. >
  2564. </a>
  2565. <div class="tweet-header ${options.mainTweet ? 'tweet-header-main' : ''}">
  2566. <a class="tweet-header-info ${options.mainTweet ? 'tweet-header-info-main' : ''}" href="/${t.user.screen_name}">
  2567. <b
  2568. ${t.user.id_str === '1708130407663759360' ? 'title="Old Twitter Layout extension developer" ' : ''}
  2569. class="tweet-header-name ${options.mainTweet ? 'tweet-header-name-main' : ''} ${t.user.verified || t.user.verified_type ? 'user-verified' : t.user.id_str === '1708130407663759360' ? 'user-verified user-verified-dimden' : ''} ${t.user.protected ? 'user-protected' : ''} ${t.user.verified_type === 'Government' ? 'user-verified-gray' : t.user.verified_type === 'Business' ? 'user-verified-yellow' : t.user.verified_type === 'Blue' ? 'user-verified-blue' : ''}"
  2570. >${escapeHTML(t.user.name)}</b>
  2571. <span class="tweet-header-handle">@${t.user.screen_name}</span>
  2572. </a>
  2573. <a class="tweet-time" data-timestamp="${new Date(t.created_at).getTime()}" title="${new Date(t.created_at).toLocaleString()}" href="/${t.user.screen_name}/status/${t.id_str}">${timeElapsed(new Date(t.created_at).getTime())}</a>
  2574. </div>
  2575. <article class="tweet-body ${options.mainTweet ? 'tweet-body-main' : ''}">
  2576. ${mentionedUserText !== `` &&
  2577. !options.threadContinuation &&
  2578. !options.noTop &&
  2579. !location.pathname.includes('/status/') ? html`
  2580. <div class="tweet-reply-to"><span>${"Replying to $SCREEN_NAME$".replace('$SCREEN_NAME$', mentionedUserText.trim().replaceAll(`> <`, `>${", "}<`).replace(`>${", "}<`, `>${" and "}<`))}</span></div>
  2581. `: ''}
  2582. <div lang="${t.lang}" class="tweet-body-text tweet-body-text-long">
  2583. <span class="tweet-body-text-span">${full_text ? await renderTweetBodyHTML(t) : ''}</span>
  2584. </div>
  2585. ${t.extended_entities && t.extended_entities.media ? html`
  2586. <div class="tweet-media">
  2587. ${t.extended_entities.media.length === 1 && t.extended_entities.media[0].type === 'video' ? html`
  2588. <div class="tweet-media-video-overlay tweet-button">
  2589. <svg viewBox="0 0 24 24" class="tweet-media-video-overlay-play">
  2590. <g>
  2591. <path class="svg-play-path" d="M8 5v14l11-7z"></path>
  2592. <path d="M0 0h24v24H0z" fill="none"></path>
  2593. </g>
  2594. </svg>
  2595. </div>
  2596. ` : ''}
  2597. ${renderMedia(t)}
  2598. </div>
  2599. ${t.extended_entities && t.extended_entities.media && t.extended_entities.media.some(m => m.type === 'animated_gif') ? html`<div class="tweet-media-controls">GIF</div>` : ''}
  2600. <span class="tweet-media-data"></span>
  2601. ` : ``}
  2602. ${t.card ? `<div class="tweet-card"></div>` : ''}
  2603. ${t.quoted_status ? html`
  2604. <a class="tweet-body-quote" target="_blank" href="/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}">
  2605. <img src="${t.quoted_status.user.profile_image_url_https}" alt="${escapeHTML(t.quoted_status.user.name)}" class="tweet-avatar-quote" width="24" height="24">
  2606. <div class="tweet-header-quote">
  2607. <span class="tweet-header-info-quote">
  2608. <b class="tweet-header-name-quote ${t.quoted_status.user.verified ? 'user-verified' : t.quoted_status.user.id_str === '1708130407663759360' ? 'user-verified user-verified-dimden' : ''} ${t.quoted_status.user.protected ? 'user-protected' : ''} ${t.quoted_status.user.verified_type === 'Government' ? 'user-verified-gray' : t.quoted_status.user.verified_type === 'Business' ? 'user-verified-yellow' : t.quoted_status.user.verified_type === 'Blue' ? 'user-verified-blue' : ''}">${escapeHTML(t.quoted_status.user.name)}</b>
  2609. <span class="tweet-header-handle-quote">@${t.quoted_status.user.screen_name}</span>
  2610. </span>
  2611. </div>
  2612. <span class="tweet-time-quote" data-timestamp="${new Date(t.quoted_status.created_at).getTime()}" title="${new Date(t.quoted_status.created_at).toLocaleString()}">${timeElapsed(new Date(t.quoted_status.created_at).getTime())}</span>
  2613. ${quoteMentionedUserText !== `` ? html`
  2614. <span class="tweet-reply-to tweet-quote-reply-to">${"Replying to $SCREEN_NAME$".replace('$SCREEN_NAME$', quoteMentionedUserText.trim().replaceAll(` `,", ").replace(", "," and "))}</span>
  2615. ` : ''}
  2616. <span class="tweet-body-text tweet-body-text-quote tweet-body-text-long" style="color:var(--yeah-default-text-color)!important">${t.quoted_status.full_text ? await renderTweetBodyHTML(t, true) : ''}</span>
  2617. ${t.quoted_status.extended_entities && t.quoted_status.extended_entities.media ? html`
  2618. <div class="tweet-media-quote">
  2619. ${t.quoted_status.extended_entities.media.map(m => `<${m.type === 'photo' ? 'img' : 'video'} ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''} crossorigin="anonymous" width="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}" height="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}" loading="lazy" ${m.type === 'video' ? 'disableRemotePlayback controls' : ''} ${m.type === 'animated_gif' ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' : ''}${m.type === 'animated_gif' ? ' autoplay' : ''} src="${m.type === 'photo' ? m.media_url_https + (false && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : window.navigator && navigator.connection && navigator.connection.type === 'cellular' ? '?name=small' : '') : m.video_info.variants.find(v => v.content_type === 'video/mp4').url}" class="tweet-media-element tweet-media-element-quote ${m.type === 'animated_gif' ? 'tweet-media-element-quote-gif' : ''} ${mediaClasses[t.quoted_status.extended_entities.media.length]}">${m.type === 'photo' ? '' : '</video>'}`).join('\n')}
  2620. </div>
  2621. ` : ''}
  2622. </a>
  2623. ` : ``}
  2624. ${t.limited_actions === 'limit_trusted_friends_tweet' && (options.mainTweet || !location.pathname.includes('/status/')) ? html`
  2625. <div class="tweet-limited">
  2626. ${"This tweet is visible only to people who are in @$SCREEN_NAME$'s trusted friends circle."}
  2627. <a href="https://help.twitter.com/en/using-twitter/twitter-circle" target="_blank">${"Learn more."}</a>
  2628. </div>
  2629. `.replace('$SCREEN_NAME$', tweet.trusted_circle_owner ? tweet.trusted_circle_owner : tweetStorage[t.conversation_id_str] ? tweetStorage[t.conversation_id_str].user.screen_name : t.in_reply_to_screen_name ? t.in_reply_to_screen_name : t.user.screen_name) : ''}
  2630. ${t.tombstone ? `<div class="tweet-warning">${t.tombstone}</div>` : ''}
  2631. <a ${!options.mainTweet ? 'hidden' : ''} class="tweet-date" title="${new Date(t.created_at).toLocaleString()}" href="/${t.user.screen_name}/status/${t.id_str}"><br>${new Date(t.created_at).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }).toLowerCase()} - ${new Date(t.created_at).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })}・ ${t.source ? t.source.split('>')[1].split('<')[0] : 'Unknown'}</a>
  2632. <div class="tweet-interact">
  2633. <span class="tweet-button tweet-interact-reply" title="Replies" data-val="${t.reply_count}">${options.mainTweet ? '' : formatLargeNumber(t.reply_count).replace(/\s/g, ',')}</span>
  2634. <span title="Retweets" class="tweet-button tweet-interact-retweet${t.retweeted ? ' tweet-interact-retweeted' : ''}${(t.user.protected || t.limited_actions === 'limit_trusted_friends_tweet') && t.user.id_str !== user.id_str ? ' tweet-interact-retweet-disabled' : ''}" data-val="${t.retweet_count}">${options.mainTweet ? '' : formatLargeNumber(t.retweet_count).replace(/\s/g, ',')}</span>
  2635. <span title="Likes" class="tweet-button tweet-yeah-interact-favorite ${t.favorited ? 'tweet-yeah-interact-favorited' : ''}" data-val="${t.favorite_count}">${options.mainTweet ? '' : formatLargeNumber(t.favorite_count).replace(/\s/g, ',')}</span>
  2636. ${t.ext && t.ext.views && t.ext.views.r && t.ext.views.r.ok && t.ext.views.r.ok.count ? html`<span title="${"Views"}" class="tweet-interact-views tweet-button" data-val="${t.ext.views.r.ok.count}">${formatLargeNumber(t.ext.views.r.ok.count).replace(/\s/g, ',')}</span>` : ''}
  2637. </div>
  2638. </article>
  2639. `;
  2640. // gifs
  2641. let gifs = Array.from(tweet.querySelectorAll('.tweet-media-gif, .tweet-media-element-quote-gif'));
  2642. if(gifs.length) {
  2643. gifs.forEach(gif => {
  2644. gif.addEventListener('click', () => {
  2645. if(gif.paused) gif.play();
  2646. else gif.pause();
  2647. });
  2648. });
  2649. }
  2650. // video
  2651. let vidOverlay = tweet.getElementsByClassName('tweet-media-video-overlay')[0];
  2652. if(vidOverlay) {
  2653. vidOverlay.addEventListener('click', async () => {
  2654. let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0];
  2655. try {
  2656. let res = await GM_fetch(vid.currentSrc); // weird problem with vids breaking cuz twitter sometimes doesnt send content-length
  2657. if(!res.headers.get('content-length')) await sleep(1000);
  2658. } catch(e) {
  2659. console.error(e);
  2660. }
  2661. vid.play();
  2662. vid.controls = true;
  2663. vid.classList.remove('tweet-media-element-censor');
  2664. vidOverlay.style.display = 'none';
  2665. });
  2666. }
  2667. if(videos) {
  2668. let videoErrors = 0;
  2669. let vids = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO');
  2670. vids[0].addEventListener('error', () => {
  2671. if(videoErrors >= 3) return;
  2672. videoErrors++;
  2673. setTimeout(() => {
  2674. vids[0].load();
  2675. }, 25);
  2676. })
  2677. for(let vid of vids) {
  2678. vid.addEventListener('mousedown', e => {
  2679. if(e.button === 1) {
  2680. e.preventDefault();
  2681. window.open(vid.currentSrc, '_blank');
  2682. }
  2683. });
  2684. }
  2685. }
  2686.  
  2687. if(t.card) {
  2688. generateCard(t, tweet, user);
  2689. }
  2690. if (options.top) {
  2691. tweet.querySelector('.tweet-top').hidden = false;
  2692. const icon = document.createElement('span');
  2693. icon.innerText = options.top.icon;
  2694. icon.classList.add('tweet-top-icon');
  2695. icon.style.color = options.top.color;
  2696.  
  2697. const span = document.createElement("span");
  2698. span.classList.add("tweet-top-text");
  2699. span.innerHTML = options.top.text;
  2700. if(options.top.class) {
  2701. span.classList.add(options.top.class);
  2702. tweet.classList.add(`tweet-top-${options.top.class}`);
  2703. }
  2704. tweet.querySelector('.tweet-top').append(icon, span);
  2705. }
  2706.  
  2707. const tweetBodyQuote = tweet.getElementsByClassName('tweet-body-quote')[0];
  2708. const tweetMediaQuote = tweet.getElementsByClassName('tweet-media-quote')[0];
  2709. const tweetInteract = tweet.getElementsByClassName('tweet-interact')[0];
  2710. const tweetFooter = tweet.getElementsByClassName('tweet-footer')[0];
  2711.  
  2712. // community notes
  2713. if(t.birdwatch) {
  2714. if(t.birdwatch.subtitle) {
  2715. let div = document.createElement('div');
  2716. div.classList.add('tweet-birdwatch', 'box');
  2717. let text = Array.from(escapeHTML(t.birdwatch.subtitle.text));
  2718. for(let e = t.birdwatch.subtitle.entities.length - 1; e >= 0; e--) {
  2719. let entity = t.birdwatch.subtitle.entities[e];
  2720. if(!entity.ref) continue;
  2721. text = arrayInsert(text, entity.toIndex, '</a>');
  2722. text = arrayInsert(text, entity.fromIndex, `<a href="${entity.ref.url}" target="_blank">`);
  2723. }
  2724. text = text.join('');
  2725. div.innerHTML = html`
  2726. <div class="tweet-birdwatch-header">
  2727. <span class="tweet-birdwatch-title">${escapeHTML(t.birdwatch.title)}</span>
  2728. </div>
  2729. <div class="tweet-birdwatch-body">
  2730. <span class="tweet-birdwatch-subtitle">${text}</span>
  2731. </div>
  2732. `;
  2733. if(tweetFooter) tweetFooter.before(div);
  2734. else tweetInteract.before(div);
  2735. }
  2736. }
  2737.  
  2738. // Quote body
  2739. if(tweetMediaQuote) tweetMediaQuote.addEventListener('click', e => {
  2740. if(e && e.target && e.target.tagName === "VIDEO") {
  2741. e.preventDefault();
  2742. e.stopPropagation();
  2743. e.stopImmediatePropagation();
  2744. if(e.target.paused) {
  2745. e.target.play();
  2746. } else {
  2747. e.target.pause();
  2748. }
  2749. }
  2750. });
  2751. if(tweetBodyQuote) {
  2752. tweetBodyQuote.addEventListener('click', e => {
  2753. e.preventDefault();
  2754. let a = document.createElement('a');
  2755. a.href = `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`;
  2756. a.target = '_blank';
  2757. a.click();
  2758. });
  2759. }
  2760.  
  2761. // Media
  2762. if (t.extended_entities && t.extended_entities.media) {
  2763. const tweetMedia = tweet.getElementsByClassName('tweet-media')[0];
  2764. tweetMedia.addEventListener('click', e => {
  2765. if (e.target.className && e.target.className.includes('tweet-media-element-censor')) {
  2766. return e.target.classList.remove('tweet-media-element-censor');
  2767. }
  2768. if (e.target.tagName === 'IMG') {
  2769. if(!e.target.src.includes('?name=') && !e.target.src.endsWith(':orig') && !e.target.src.startsWith('data:')) {
  2770. e.target.src += '?name=orig';
  2771. } else if(e.target.src.includes('?name=small')) {
  2772. e.target.src = e.target.src.replace('?name=small', '?name=large');
  2773. }
  2774. new Viewer(tweetMedia, {
  2775. transition: false,
  2776. zoomRatio: 0.3
  2777. });
  2778. e.target.click();
  2779. }
  2780. });
  2781. }
  2782.  
  2783. if(options.noInsert) {
  2784. return tweet;
  2785. }
  2786.  
  2787. if(options.after) {
  2788. options.after.after(tweet);
  2789. } else if (options.before) {
  2790. options.before.before(tweet);
  2791. } else if (options.prepend) {
  2792. timelineContainer.prepend(tweet);
  2793. } else {
  2794. timelineContainer.append(tweet);
  2795. }
  2796. return tweet;
  2797. } catch(e) {
  2798. console.error(e);
  2799. if(Date.now() - lastTweetErrorDate > 1000) {
  2800. lastTweetErrorDate = Date.now();
  2801. createModal(/*html*/`
  2802. <div style="max-width:700px">
  2803. <span style="font-size:14px;color:var(--default-text-color)">
  2804. <h2 style="margin-top: 0">Something went wrong</h2>
  2805. Some tweets couldn't be loaded due to errors.<br>
  2806. ${"Please copy text below and send it to $AT1$issue tracker$AT2$ or $AT3$my email$AT2$. Thank you!".replace('$AT1$', "<a target='_blank' href='https://github.com/dimdenGD/YeahTwitter/issues'>").replace(/\$AT2\$/g, '</a>').replace("$AT3$", "<a target='_blank' href='mailto:admin@dimden.dev'>")}
  2807. </span>
  2808. <div class="box" style="font-family:monospace;line-break: anywhere;padding:5px;margin-top:5px;background:rgba(255, 0, 0, 0.1);color:#ff4545">
  2809. ${escapeHTML(e.stack ? e.stack : String(e))} at ${t.id_str} (YeahTwitter v${chrome.runtime.getManifest().version})
  2810. </div>
  2811. </div>
  2812. `);
  2813. }
  2814. return null;
  2815. }
  2816. }
  2817.  
  2818. // scripts/content.js
  2819. const API_URL = `https://yeah.dimden.dev/api`;
  2820.  
  2821. Promise.all([
  2822. GM_fetch('https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/styles/style.css').then(res => res.text()),
  2823. GM_fetch('https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/styles/tweet.css').then(res => res.text())
  2824. ]).then(styles => {
  2825. setTimeout(() => {
  2826. for(let css of styles) {
  2827. let style = document.createElement('style');
  2828. let head = document.head || document.getElementsByTagName('head')[0];
  2829. let isFirefox = navigator.userAgent.indexOf('Firefox') > -1;
  2830. if(isFirefox) css = css.replaceAll('chrome-extension://', 'moz-extension://');
  2831. style.innerHTML = css.replaceAll('__MSG_@@extension_id__', chrome.runtime.id);
  2832. head.appendChild(style);
  2833. }
  2834. }, 750);
  2835. });
  2836.  
  2837. setTimeout(async () => {
  2838. let yeahToken = await getYeahToken();
  2839. let ignorePopup = await new Promise(resolve => chrome.storage.local.get('ignorePopup', result => resolve(result.ignorePopup)));
  2840. let userId = await getUserId();
  2841. if(!yeahToken && ignorePopup && ignorePopup[userId]) {
  2842. return;
  2843. }
  2844. if(!yeahToken) {
  2845. let modalOpenTime = Date.now();
  2846. let modal = createModal(/*html*/`
  2847. <h2 style="margin-top:0">
  2848. <img src="${YEAH_images['yeah_on32.png']}" alt="Yeah!" style="width: 24px; height: 24px;margin-bottom: -4px;">
  2849. Welcome to Yeah! for Twitter extension!
  2850. </h2>
  2851. <p>This extension adds a <b>Yeah!</b> button to all tweets, which is essentially same thing as a Like but public to everyone. Everyone can see who Yeahed a tweet, and everyone can see all your Yeahs on your profile.</p>
  2852. <p>It doesn't send a spammy reply with an image, instead it saves your Yeahs into a shared database.</p>
  2853. <p>
  2854. In order to get started, you need to authenticate your Twitter account.
  2855. Click button below, and we'll automatically post a tweet on your behalf that will look like 'yeah-xxxxxxxx'.
  2856. Then our server will check for that tweet existence, confirm that it's you, and extension will automatically remove the tweet and save your token.
  2857. This tweet should be only up for about a second, so don't worry about posting nonsensical tweet.
  2858. </p>
  2859. <p>
  2860. <b>Important: your account must not be private so server can actually see the tweet. You'll need to make your account public for this auth, afterwards you can make it private again.</b>
  2861. </p>
  2862. <div class="error-message"></div>
  2863. <div>
  2864. <button class="auth-button nice-yeah-button">Authenticate</button>
  2865. </div>
  2866. <div style="margin-top: 10px">
  2867. <span class="subtle dontshow" role="button">Never show this popup for this account</span>
  2868. </div>
  2869. `, 'welcome-modal', () => {}, () => Date.now() - modalOpenTime > 1250);
  2870. let button = modal.querySelector('.auth-button');
  2871. button.addEventListener('click', async () => {
  2872. button.disabled = true;
  2873. button.textContent = 'Authenticating...';
  2874. let tweetId;
  2875. try {
  2876. // get tokens
  2877. let tokens = JSON.parse(await callYeahApi('/request_token'));
  2878. // create tweet
  2879. let tweet = await callTwitterApi('POST', '/graphql/oB-5XsHNAbjvARJEc8CZFw/CreateTweet', {}, {
  2880. "variables":{"tweet_text": `yeah-${tokens.public_token}`,"dark_request":false,"media":{"media_entities":[],"possibly_sensitive":false},"semantic_annotation_ids":[]},
  2881. "features":{"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"creator_subscriptions_quote_tweet_preview_enabled":false,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"articles_preview_enabled":true,"rweb_video_timestamps_enabled":true,"rweb_tipjar_consumption_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false},
  2882. "queryId":"oB-5XsHNAbjvARJEc8CZFw"
  2883. });
  2884. // parse tweet
  2885. let tweetResult = tweet.data.create_tweet.tweet_results.result;
  2886. let tweetData = tweetResult.legacy;
  2887. tweetData.user = tweetResult.core.user_results.result.legacy;
  2888. tweetData.user.id_str = tweetData.user_id_str;
  2889. tweetId = tweetData.id_str;
  2890. // send tweet
  2891. let res = await callYeahApi('/verify_token', {
  2892. tweet: tweetData,
  2893. public_token: tokens.public_token,
  2894. private_token: tokens.private_token
  2895. });
  2896. if(res === 'success') {
  2897. chrome.storage.local.get('yeahTokens', result => {
  2898. if(!result.yeahTokens) result.yeahTokens = {};
  2899. result.yeahTokens[userId] = tokens.private_token;
  2900. chrome.storage.local.set(result);
  2901. });
  2902. modal.removeModal();
  2903. modalOpenTime = Date.now();
  2904. let modal2 = createModal(/*html*/`
  2905. <h2 style="margin-top:0">
  2906. <img src="${YEAH_images['yeah_on32.png']}" alt="Yeah!" style="width: 24px; height: 24px;margin-bottom: -4px;">
  2907. Authentification successful!
  2908. </h2>
  2909. <p>You can now Yeah! on any tweet. Yeah!!!!!</p>
  2910. <div>
  2911. btw I (<a href="/dimden" target="_blank" style="text-decoration:none;color:#1d9bf0">@dimden</a>) make a lot of cool extensions for Twitter like this, maybe u wanna follow me?
  2912. </div>
  2913. <div style="margin-top: 10px;"><button class="follow-button nice-yeah-button">Yeah! (Follow)</button></div>
  2914. `, 'authentification-successful', () => {}, () => Date.now() - modalOpenTime > 1500);
  2915. let followButton = modal2.querySelector('.follow-button');
  2916. followButton.addEventListener('click', () => {
  2917. callTwitterApi('POST', '/1.1/friendships/create.json', {
  2918. "Content-Type": "application/x-www-form-urlencoded"
  2919. }, {
  2920. include_profile_interstitial_type: 1,
  2921. include_blocking: 1,
  2922. include_blocked_by: 1,
  2923. include_followed_by: 1,
  2924. include_want_retweets: 1,
  2925. include_mute_edge: 1,
  2926. include_can_dm: 1,
  2927. include_can_media_tag: 1,
  2928. include_ext_is_blue_verified: 1,
  2929. include_ext_verified_type: 1,
  2930. include_ext_profile_image_shape: 1,
  2931. skip_status: 1,
  2932. user_id: "1708130407663759360"
  2933. }).then(() => {
  2934. modal2.removeModal();
  2935. alert('Thank you! Happy Yeahing!');
  2936. }).catch(e => {
  2937. console.error(e);
  2938. location.href = '/dimden';
  2939. });
  2940. });
  2941. } else {
  2942. throw new Error(res);
  2943. }
  2944. } catch(e) {
  2945. console.error(e);
  2946. modal.querySelector('.error-message').innerHTML = `Failed to authenticate. Please try again later. (${e.message})`;
  2947. } finally {
  2948. button.disabled = false;
  2949. button.textContent = 'Authenticate';
  2950. if(tweetId) {
  2951. callTwitterApi('POST', `/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet`, {}, {
  2952. variables: {tweet_id: tweetId, dark_request: false},
  2953. queryId: "VaenaVgh5q5ih7kvyVjgtg"
  2954. });
  2955. }
  2956. }
  2957. });
  2958.  
  2959. let dontshow = modal.querySelector('.dontshow');
  2960. dontshow.addEventListener('click', () => {
  2961. chrome.storage.local.get('ignorePopup', result => {
  2962. if(!result.ignorePopup) result.ignorePopup = {};
  2963. result.ignorePopup[userId] = true;
  2964. chrome.storage.local.set(result);
  2965. modal.removeModal();
  2966. alert('Popup will not show again for this account. If you want to show it again, press on extension icon and press "Reset popup settings".');
  2967. });
  2968. });
  2969. };
  2970. }, 1000);
  2971.  
  2972. let fetchQueue = [];
  2973. function hookIntoTweets() {
  2974. let tweets = document.getElementsByTagName('article');
  2975.  
  2976. for (let i = 0; i < tweets.length; i++) {
  2977. let tweet = tweets[i];
  2978. if(tweet.dataset.yeahed) continue;
  2979. tweet.dataset.yeahed = true;
  2980.  
  2981. let linkToTweet = Array.from(tweet.querySelectorAll('a[role="link"]')).find(a => a.href.includes('/status/') && !a.href.includes('/photo') && !a.href.includes('/video'));
  2982. let oldTwitter = false;
  2983. if(!linkToTweet) {
  2984. let tweetDiv = tweet.closest('.tweet, .yeah-tweet');
  2985. if(tweetDiv) {
  2986. oldTwitter = true;
  2987. linkToTweet = tweetDiv.querySelector('.tweet-time');
  2988. } else {
  2989. continue;
  2990. }
  2991. };
  2992. let id = linkToTweet.href.match(/\/status\/(\d+)/)[1];
  2993. if(!id) continue;
  2994.  
  2995. fetchQueue.push(id);
  2996.  
  2997. let div = document.createElement('div');
  2998. let button = document.createElement('button');
  2999. button.dataset.count = tweetCache[id] ? tweetCache[id].count : 0;
  3000. button.addEventListener('click', async () => {
  3001. if(!await getYeahToken()) {
  3002. return alert('You need to authenticate first (refresh page for auth popup to appear)');
  3003. }
  3004. if(!button.classList.contains('yeahed')) {
  3005. callYeahApi('/yeah', {
  3006. post_id: id
  3007. });
  3008. button.querySelector('.yeah-image').src = YEAH_images['yeah_on32.png'];
  3009. let yeahCounter = button.querySelector('.yeah-counter');
  3010. let count = parseInt(button.dataset.count);
  3011. yeahCounter.innerText = formatLargeNumber(count + 1);
  3012. button.dataset.count = count + 1;
  3013. button.classList.add('yeahed');
  3014. if(tweetCache[id]) {
  3015. tweetCache[id].yeahed = true;
  3016. tweetCache[id].count++;
  3017. }
  3018. let likeButton = tweet.querySelector('button[data-testid="like"], .tweet-interact-favorite:not(.tweet-interact-favorited)');
  3019. if(likeButton) {
  3020. let settings = await getYeahSettings();
  3021. if(!settings.dontLike) likeButton.click();
  3022. }
  3023. } else {
  3024. callYeahApi('/unyeah', {
  3025. post_id: id
  3026. });
  3027. button.classList.remove('yeahed');
  3028. let yeahCounter = button.querySelector('.yeah-counter');
  3029. let count = parseInt(button.dataset.count);
  3030. yeahCounter.innerText = formatLargeNumber(count - 1);
  3031. button.dataset.count = count - 1;
  3032. if(count - 1 <= 0) yeahCounter.innerText = '';
  3033. if(tweetCache[id]) {
  3034. tweetCache[id].yeahed = false;
  3035. tweetCache[id].count--;
  3036. if(tweetCache[id].count < 0) tweetCache[id].count = 0;
  3037. }
  3038. let likeButton = tweet.querySelector('button[data-testid="unlike"], .tweet-interact-favorite.tweet-interact-favorited');
  3039. if(likeButton) {
  3040. let settings = await getYeahSettings();
  3041. if(!settings.dontLike) likeButton.click();
  3042. }
  3043. }
  3044. });
  3045. tweet.addEventListener("keydown", (e) => {
  3046. if(e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
  3047. if(e.key === "y") button.click();
  3048. });
  3049. button.addEventListener('mouseover', () => {
  3050. button.querySelector('.yeah-image').src = YEAH_images['yeah_on32.png'];
  3051. });
  3052. button.addEventListener('mouseout', () => {
  3053. if(!button.classList.contains('yeahed')) button.querySelector('.yeah-image').src = YEAH_images['yeah_off32.png'];
  3054. });
  3055. button.className = `yeah-button yeah-button-${id}`;
  3056. div.className = 'yeah-button-container';
  3057.  
  3058. let img = document.createElement('img');
  3059. img.src = tweetCache[id] && tweetCache[id].yeahed ? YEAH_images['yeah_on32.png'] : YEAH_images['yeah_off32.png'];
  3060. if(tweetCache[id] && tweetCache[id].yeahed) button.classList.add('yeahed');
  3061. img.className = 'yeah-image';
  3062. img.draggable = false;
  3063. button.appendChild(img);
  3064.  
  3065. let counter = document.createElement('span');
  3066. counter.className = 'yeah-counter';
  3067. counter.innerText = tweetCache[id] && typeof tweetCache[id].count === 'number' ? formatLargeNumber(tweetCache[id].count) : '';
  3068. if(oldTwitter) {
  3069. counter.classList.add('yeah-counter-oldtwitter');
  3070. }
  3071. button.appendChild(counter);
  3072. div.appendChild(button);
  3073.  
  3074. let group = tweet.querySelector('div[role="group"]');
  3075. if(group && group.children && group.children[3]) group.children[3].after(div);
  3076. else {
  3077. let interactButton = tweet.querySelector('.tweet-interact-favorite, .tweet-yeah-interact-favorite');
  3078. if(interactButton) {
  3079. div.classList.add('yeah-button-container-oldtwitter');
  3080. interactButton.after(div);
  3081. }
  3082. }
  3083. }
  3084. }
  3085.  
  3086. function updateButton(data) {
  3087. if(!data) return;
  3088. let buttons = Array.from(document.getElementsByClassName(`yeah-button-${data.post_id}`));
  3089. for(let button of buttons) {
  3090. if(data.yeahed) {
  3091. button.classList.add('yeahed');
  3092. button.querySelector('.yeah-image').src = YEAH_images['yeah_on32.png'];
  3093. } else {
  3094. button.classList.remove('yeahed');
  3095. button.querySelector('.yeah-image').src = YEAH_images['yeah_off32.png'];
  3096. }
  3097. button.dataset.count = data.count;
  3098.  
  3099. let counter = button.querySelector('.yeah-counter');
  3100. counter.innerText = data.count === 0 ? '' : formatLargeNumber(data.count);
  3101. }
  3102. }
  3103.  
  3104. let tweetCache = {};
  3105. setInterval(() => tweetCache = {}, 1000 * 60 * 5);
  3106. setInterval(async () => {
  3107. if(fetchQueue.length > 0 && await getYeahToken()) {
  3108. let first100 = fetchQueue.splice(0, 100);
  3109. let cachedData = first100.map(id => tweetCache[id]).filter(Boolean);
  3110. for(let cache of cachedData) {
  3111. updateButton(cache);
  3112. }
  3113. first100 = first100.filter((id) => !tweetCache[id]);
  3114. if(!first100.length) return;
  3115. for(let id of first100) {
  3116. tweetCache[id] = {
  3117. post_id: id,
  3118. yeahed: false,
  3119. count: 0
  3120. };
  3121. }
  3122. let data = JSON.parse(await callYeahApi('/get', {
  3123. post_ids: first100.join(',')
  3124. }));
  3125. for(let i in data) {
  3126. tweetCache[data[i].post_id] = data[i];
  3127. updateButton(data[i]);
  3128. }
  3129. }
  3130. }, 1500);
  3131.  
  3132. function hookIntoInteractions() {
  3133. let path = window.location.pathname;
  3134. let addedTab;
  3135. if(path.includes('/status/') && (path.endsWith('/quotes') || path.endsWith('/retweets') || path.endsWith('/likes'))) {
  3136. let tablist = document.querySelector('div[role="tablist"]');
  3137. if(!tablist) return;
  3138. if(tablist.dataset.yeahed) return;
  3139. tablist.dataset.yeahed = true;
  3140.  
  3141. let yeahTab = document.createElement('div');
  3142. yeahTab.className = 'yeah-tab';
  3143.  
  3144. let span = document.createElement('span');
  3145. span.innerText = 'Yeahs';
  3146. yeahTab.appendChild(span);
  3147. tablist.appendChild(yeahTab);
  3148.  
  3149. addedTab = yeahTab;
  3150. } else {
  3151. let tablist = document.querySelector('.tweet-footer-stats');
  3152. if(!tablist) return;
  3153. if(tablist.dataset.yeahed) return;
  3154. tablist.dataset.yeahed = true;
  3155.  
  3156. let yeahTab = document.createElement('a');
  3157. yeahTab.className = 'tweet-footer-stat';
  3158. yeahTab.style.cursor = 'pointer';
  3159.  
  3160. let span = document.createElement('span');
  3161. span.innerText = 'Yeahs';
  3162. span.className = 'tweet-footer-stat-text';
  3163.  
  3164. let b = document.createElement('b');
  3165. let id = location.pathname.match(/\/status\/(\d+)/)[1];
  3166. b.innerText = tweetCache[id] && typeof tweetCache[id].count === 'number' ? formatLargeNumber(tweetCache[id].count) : '?';
  3167. b.className = 'tweet-footer-stat-count';
  3168.  
  3169. yeahTab.appendChild(span);
  3170. yeahTab.appendChild(b);
  3171. tablist.appendChild(yeahTab);
  3172.  
  3173. addedTab = yeahTab;
  3174. }
  3175.  
  3176. if(addedTab) {
  3177. addedTab.addEventListener('click', async() => {
  3178. if(!await getYeahToken()) {
  3179. return alert('You need to authenticate first (refresh page for auth popup to appear)');
  3180. }
  3181. let modal = createModal(/*html*/`
  3182. <h3>Yeahs</h3>
  3183. <div class="list"></div>
  3184. <div class="loader" style="text-align:center">
  3185. <img src="${YEAH_images['loading.svg']}" width="64" height="64">
  3186. </div>
  3187. `, 'yeah-users');
  3188.  
  3189. let list = modal.querySelector('.list');
  3190.  
  3191. let data = JSON.parse(await callYeahApi('/get_users', {
  3192. post_id: path.match(/\/status\/(\d+)/)[1],
  3193. page: 1
  3194. }));
  3195.  
  3196. if(!data.length) {
  3197. modal.querySelector('.loader').hidden = true;
  3198. list.innerHTML = 'No Yeahs yet';
  3199. return;
  3200. }
  3201.  
  3202. let lookup = await API.user.lookup(data);
  3203.  
  3204. modal.querySelector('.loader').hidden = true;
  3205.  
  3206. let addedUsers = [];
  3207. for(let id of data) {
  3208. let user = lookup.find(user => user.id_str === id);
  3209. if(user) {
  3210. appendUser(user, list);
  3211. addedUsers.push(user.id_str);
  3212. }
  3213. }
  3214.  
  3215. let modalContent = modal.querySelector('.yeah-modal-content');
  3216. let over = false, loadingMore = false, page = 2;
  3217. modalContent.addEventListener('scroll', async () => {
  3218. if(over) return;
  3219. if(loadingMore) return;
  3220.  
  3221. let scrollPosition = modalContent.scrollTop + modalContent.offsetHeight;
  3222. if(scrollPosition >= modalContent.scrollHeight - 200) {
  3223. loadingMore = true;
  3224. modal.querySelector('.loader').hidden = false;
  3225. let data = JSON.parse(await callYeahApi('/get_users', {
  3226. post_id: path.match(/\/status\/(\d+)/)[1],
  3227. page: page++
  3228. }));
  3229. if(!data.length) {
  3230. over = true;
  3231. modal.querySelector('.loader').hidden = true;
  3232. return;
  3233. }
  3234. let lookup = await API.user.lookup(data);
  3235. for(let id of data) {
  3236. if(addedUsers.includes(id)) continue;
  3237.  
  3238. let user = lookup.find(user => user.id_str === id);
  3239. if(user) {
  3240. appendUser(user, list);
  3241. addedUsers.push(user.id_str);
  3242. }
  3243. }
  3244. loadingMore = false;
  3245. modal.querySelector('.loader').hidden = true;
  3246. }
  3247. });
  3248. });
  3249. }
  3250. }
  3251.  
  3252. function hookIntoProfile() {
  3253. if(['/notifications', '/explore', '/home', '/messages', '/compose'].includes(window.location.pathname)) return;
  3254. if(window.location.pathname.startsWith('/search')) return;
  3255. if(window.location.pathname.startsWith('/i/')) return;
  3256. if(window.location.pathname.startsWith('/explore/')) return;
  3257. if(window.location.pathname.startsWith('/notifications/')) return;
  3258. if(window.location.pathname.startsWith('/compose/')) return;
  3259. if(window.location.pathname.startsWith('/messages/')) return;
  3260. if(window.location.pathname.includes('/communities/')) return;
  3261. if(window.location.pathname.includes('/status/')) return;
  3262. if(window.location.pathname.includes('/settings/')) return;
  3263.  
  3264. let addedTab;
  3265. let profileStats = document.querySelector('#profile-stats');
  3266. if(!profileStats) {
  3267. let tablist = document.querySelector('div:not([data-testid="toolBar"]) > nav[role="navigation"][aria-live="polite"] div div[role="tablist"]');
  3268. if(!tablist) return;
  3269. if(tablist.dataset.yeahed) return;
  3270. tablist.dataset.yeahed = true;
  3271. let yeahTab = document.createElement('div');
  3272. yeahTab.className = 'yeah-tab';
  3273. let span = document.createElement('span');
  3274. span.innerText = 'Yeahs';
  3275.  
  3276. yeahTab.appendChild(span);
  3277. tablist.appendChild(yeahTab);
  3278. addedTab = yeahTab;
  3279. } else {
  3280. if(profileStats.dataset.yeahed) return;
  3281. profileStats.dataset.yeahed = true;
  3282.  
  3283. let yeahTab = document.createElement('a');
  3284. yeahTab.className = 'profile-stat';
  3285. yeahTab.style.cursor = 'pointer';
  3286.  
  3287. let span = document.createElement('span');
  3288. span.innerText = 'Yeahs';
  3289. span.className = 'profile-stat-text';
  3290.  
  3291. let span2 = document.createElement('span');
  3292. span2.className = 'profile-stat-value';
  3293. span2.innerText = '?';
  3294.  
  3295. setTimeout(() => {
  3296. let avatar = document.getElementById('profile-avatar');
  3297. if(!avatar || !avatar.dataset.user_id) return;
  3298. let id = avatar.dataset.user_id;
  3299. callYeahApi('/get_user_yeah_count', {
  3300. user_id: id
  3301. }).then(data => {
  3302. data = JSON.parse(data);
  3303. if(typeof data.count === 'number') span2.innerText = formatLargeNumber(data.count);
  3304. });
  3305. }, 2000);
  3306.  
  3307. yeahTab.appendChild(span);
  3308. yeahTab.appendChild(span2);
  3309.  
  3310. profileStats.appendChild(yeahTab);
  3311.  
  3312. addedTab = yeahTab;
  3313. }
  3314. if(addedTab) addedTab.addEventListener('click', async () => {
  3315. if(!await getYeahToken()) {
  3316. return alert('You need to authenticate first (refresh page for auth popup to appear)');
  3317. }
  3318. let username = window.location.pathname.split('/')[1];
  3319. let modal = createModal(/*html*/`
  3320. <h3>${username}'s Yeahs</h3>
  3321. <div class="list"></div>
  3322. <div class="loader" style="text-align:center">
  3323. <img src="${YEAH_images['loading.svg']}" width="64" height="64">
  3324. </div>
  3325. `, 'yeah-posts');
  3326.  
  3327. let list = modal.querySelector('.list');
  3328. let user = await API.user.get(username, false);
  3329.  
  3330. let data = JSON.parse(await callYeahApi('/get_yeahs', {
  3331. user_id: user.id_str,
  3332. page: 1
  3333. }));
  3334.  
  3335. if(!data.length) {
  3336. modal.querySelector('.loader').hidden = true;
  3337. list.innerHTML = 'No Yeahs yet';
  3338. return;
  3339. }
  3340.  
  3341. let tweets = await API.tweet.lookup(data);
  3342.  
  3343. if(!tweets.length) {
  3344. modal.querySelector('.loader').hidden = true;
  3345. list.innerHTML = 'No Yeahs yet';
  3346. return;
  3347. }
  3348.  
  3349. let addedPosts = [];
  3350. for(let id of data) {
  3351. let tweet = tweets.find(tweet => tweet.id_str === id);
  3352. if(tweet) {
  3353. await appendTweet(tweet, list, {}, user);
  3354. addedPosts.push(tweet.id_str);
  3355. }
  3356. }
  3357. modal.querySelector('.loader').hidden = true;
  3358.  
  3359. let modalContent = modal.querySelector('.yeah-modal-content');
  3360. let over = false, loadingMore = false, page = 2;
  3361. modalContent.addEventListener('scroll', async () => {
  3362. if(over) return;
  3363. if(loadingMore) return;
  3364.  
  3365. let scrollPosition = modalContent.scrollTop + modalContent.offsetHeight;
  3366. if(scrollPosition >= modalContent.scrollHeight - 200) {
  3367. loadingMore = true;
  3368. modal.querySelector('.loader').hidden = false;
  3369. let data = JSON.parse(await callYeahApi('/get_yeahs', {
  3370. user_id: user.id_str,
  3371. page: page++
  3372. }));
  3373. if(!data.length) {
  3374. over = true;
  3375. modal.querySelector('.loader').hidden = true;
  3376. return;
  3377. }
  3378. let tweets = await API.tweet.lookup(data);
  3379. for(let id of data) {
  3380. if(addedPosts.includes(id)) continue;
  3381. let tweet = tweets.find(tweet => tweet.id_str === id);
  3382. if(tweet) {
  3383. await appendTweet(tweet, list, {}, user);
  3384. addedPosts.push(tweet.id_str);
  3385. }
  3386. }
  3387. loadingMore = false;
  3388. modal.querySelector('.loader').hidden = true;
  3389. }
  3390. });
  3391. });
  3392. }
  3393.  
  3394. setInterval(hookIntoTweets, 250);
  3395. setInterval(hookIntoInteractions, 500);
  3396. setInterval(hookIntoProfile, 500);