Advertisement
Guest User

Untitled

a guest
Nov 16th, 2021
403
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.32 KB | None | 0 0
  1. // ==UserScript==
  2. // @name /bant/ Flags
  3. // @namespace BintFlegs
  4. // @description More flags for r/banter
  5. // @include http*://boards.4chan.org/bant/*
  6. // @include http*://archive.nyafuu.org/bant/*
  7. // @include http*://archived.moe/bant/*
  8. // @include http*://thebarchive.com/bant/*
  9. // @include http*://nineball.party/*
  10. // @exclude http*://boards.4chan.org/bant/catalog
  11. // @exclude http*://archive.nyafuu.org/bant/statistics/
  12. // @exclude http*://archived.moe/bant/statistics/
  13. // @exclude http*://thebarchive.com/bant/statistics/
  14. // @version 2.2.2
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM.setValue
  19. // @grant GM.getValue
  20. // @grant GM.xmlHttpRequest
  21. // @run-at document-idle
  22. // @icon https://flags.plum.moe/flags/0077.png
  23. // @updateURL https://flags.plum.moe/bantflags.meta.js
  24. // @downloadURL https://flags.plum.moe/bantflags.user.js
  25. // ==/UserScript==
  26.  
  27. // (C) Copyright 2019 C-xC-c <boku@plum.moe>
  28. // This file is part of /bant/ Flags.
  29. // /bant/ Flags is licensed under the GNU AGPL Version 3.0 or later.
  30. // see the LICENSE file or <https://www.gnu.org/licenses/>
  31.  
  32. // This will print a load of shit to the console
  33. const debugMode = false;
  34.  
  35. const isGM4 = typeof GM_setValue === 'undefined';
  36. const setValue = isGM4 ? GM.setValue : GM_setValue;
  37. const getValue = isGM4 ? GM.getValue : GM_getValue;
  38. const xmlHttpRequest = isGM4 ? GM.xmlHttpRequest : GM_xmlhttpRequest;
  39.  
  40. const version = encodeURIComponent(2); // Breaking changes.
  41. const back_end = 'https://flags.plum.moe/';
  42. const api_flags = back_end + 'api/flags';
  43. const flag_dir = back_end + 'flags/';
  44. const api_get = back_end + 'api/get';
  45. const api_post = back_end + 'api/post';
  46. const namespace = 'BintFlegs';
  47.  
  48. // If you increase this the server will ignore your post.
  49. const max_flags = 30;
  50.  
  51. let regions = []; // The flags we have selected.
  52. let postNrs = []; // all post numbers in the thread.
  53. let board_id = ""; // The board we get flags for.
  54. let flagsLoaded = false;
  55.  
  56. const debug = text => {
  57. if (debugMode)
  58. console.log('[BantFlags] ' + text);
  59. }
  60.  
  61. // Test unqiue CSS paths to figure out what board software we're using.
  62. const software = {
  63. yotsuba: window.location.host === 'boards.4chan.org',
  64. nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null,
  65. foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null
  66. };
  67.  
  68. const makeElement = (tag, options) => Object.assign(document.createElement(tag), options);
  69. const toggleFlagButton = state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false;
  70. const flagSource = flag => flag_dir + flag + '.png';
  71.  
  72. /** Add styles to the <head> */
  73. const addStyle = css => document.head.appendChild(makeElement('style', { innerHTML: css }));
  74.  
  75. const makeRequest = ((method, url, data, func) => {
  76. xmlHttpRequest({
  77. method: method,
  78. url: url,
  79. data: data,
  80. headers: { "Content-Type": 'application/x-www-form-urlencoded' },
  81. onload: func
  82. });
  83. });
  84.  
  85. /** Itterate over selected flags are store them across browser sessions.*/
  86. function saveFlags() {
  87. regions = [];
  88. const selectedFlags = document.querySelectorAll(".bantflags_flag");
  89.  
  90. for (let i = 0; i < selectedFlags.length; i++) {
  91. regions[i] = selectedFlags[i].title;
  92. }
  93.  
  94. setValue(namespace, regions);
  95. }
  96.  
  97. /** Add a flag to our selection. */
  98. function setFlag(flag, save) {
  99. const flagName = flag ? flag : document.querySelector('#flagSelect input').value;
  100. const flagContainer = document.getElementById('bantflags_container');
  101.  
  102. flagContainer.appendChild(makeElement('img', {
  103. title: flagName,
  104. src: flagSource(flagName),
  105. className: 'bantflags_flag',
  106. onclick: function() {
  107. flagContainer.removeChild(this);
  108. if (flagsLoaded)
  109. toggleFlagButton('on');
  110. saveFlags();
  111. }
  112. }));
  113.  
  114. if (flagContainer.children.length >= max_flags)
  115. toggleFlagButton('off');
  116.  
  117. if (!flag || save === true) // We've added a new flag to our selection
  118. saveFlags();
  119. }
  120.  
  121. function init() {
  122. const flagsForm = makeElement('div', {
  123. className: 'flagsForm',
  124. innerHTML: '<span id="bantflags_container"></span><button type="button" id="append_flag_button" title="Click to add selected flag to your flags. Click on flags to remove them." disabled="true"><<</button><button id="flagLoad" type="button">Click to load flags.</button><div id="flagSelect" ><ul class="hide"></ul><input type="button" value="(You)" onclick=""></div>'
  125. });
  126.  
  127. // Where do we append the flagsForm to?
  128. if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); }
  129. else if (software.nodegucaDoushio) { document.querySelector('section').insertAdjacentElement('afterEnd', flagsForm); }
  130.  
  131. for (let i = 0; i < regions.length; i++) {
  132. setFlag(regions[i]);
  133. }
  134.  
  135. document.getElementById('flagLoad').addEventListener('click', makeFlagSelect, { once: true });
  136. }
  137.  
  138. /** Get flag data from server and fill flags form. */
  139. function makeFlagSelect() {
  140. makeRequest(
  141. "GET",
  142. api_flags,
  143. "", // We can't send data, it's a GET request.
  144. function (resp) {
  145. debug('Loading flags.');
  146.  
  147. if (resp.status !== 200) {
  148. console.log('Couldn\'t get flag list from server')
  149. return;
  150. }
  151.  
  152. let flagSelect = document.getElementById('flagSelect');
  153. let flagInput = flagSelect.querySelector('input');
  154. let flagList = flagSelect.querySelector('ul');
  155.  
  156. let flags = resp.responseText.split('\n');
  157. for (let i = 0; i < flags.length; i++) {
  158. const flag = flags[i];
  159. flagList.appendChild(makeElement('li',{
  160. innerHTML: `<img src="${flagSource(flag)}" title="${flag}"><span>${flag}</span>`
  161. }));
  162. }
  163.  
  164. flagSelect.addEventListener('click', e => {
  165. // Maybe we clicked the flag image
  166. const node = e.target.nodeName === 'LI' ? e.target : e.target.parentNode;
  167. if (node.nodeName === 'LI')
  168. flagInput.value = node.querySelector('span').innerHTML;
  169.  
  170. flagList.classList.toggle('hide');
  171. });
  172.  
  173. const flagButton = document.getElementById('append_flag_button');
  174. flagButton.addEventListener('click', () => setFlag());
  175. flagButton.disabled = false;
  176.  
  177. document.getElementById('flagLoad').style.display = 'none';
  178. document.querySelector('.flagsForm').style.marginRight = "200px"; // flagsForm has position: absolute and is ~200px long.
  179. flagSelect.style.display = 'inline-block';
  180. flagsLoaded = true;
  181. });
  182. }
  183.  
  184. /** add all of the post numbers on the page to postNrs. */
  185. function getPosts(selector) {
  186. const posts = document.querySelectorAll(selector);
  187.  
  188. for (let i = 0; i < posts.length; i++) {
  189. const postNumber = software.yotsuba
  190. ? posts[i].id.substr(2) // you 4chan.
  191. : posts[i].id;
  192.  
  193. postNrs.push(postNumber);
  194. }
  195. debug(postNrs);
  196. }
  197.  
  198. /** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */
  199. function resolveFlags() {
  200. makeRequest(
  201. 'POST',
  202. api_get,
  203. 'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + version,
  204. function (resp) {
  205. if (resp.status !== 200) {
  206. console.log('[bantflags] Couldn\'t load flags. Refresh the page');
  207. debug(resp.responseText);
  208. return;
  209. }
  210.  
  211. const jsonData = JSON.parse(resp.responseText);
  212. debug(`JSON: ${resp.responseText}`);
  213.  
  214. Object.keys(jsonData).forEach(post => {
  215. const flags = jsonData[post];
  216.  
  217. if (flags.length <= 0) return;
  218.  
  219. debug(`Resolving flags for >>${post}`);
  220.  
  221. let flagContainer;
  222. if (software.yotsuba) { flagContainer = document.querySelector(`[id="pc${post}"] .postInfo .nameBlock`); }
  223. else if (software.foolfuuka) { flagContainer = document.querySelector(`[id="${post}"] .post_data .post_type`); }
  224. else if (software.nodegucaDoushio) { flagContainer = document.querySelector(`[id="${post}"] header`); }
  225.  
  226. for (let i = 0; i < flags.length; i++) {
  227. const flag = flags[i];
  228.  
  229. flagContainer.append(makeElement('a', {
  230. innerHTML: `<img src="${flagSource(flag)}" title="${flag}">`,
  231. className: 'bantFlag',
  232. target: '_blank',
  233. title: flag
  234. }));
  235.  
  236. debug(`\t -> ${flag}`);
  237. }
  238. });
  239.  
  240. postNrs = [];
  241. });
  242. }
  243.  
  244. function main() {
  245. if (!regions) {
  246. regions = [];
  247. }
  248.  
  249. // See Docs/styles.css
  250. addStyle('.bantFlag{padding: 0px 0px 0px 5px; display: inline-block; width: 16px; height: 11px; position: relative;} .bantflags_flag{padding: 1px;} [title^="Romania"]{ position: relative; animation: shakeAnim 0.1s linear infinite;} @keyframes shakeAnim{ 0% {left: 1px;} 25% {top: 2px;} 50% {left: 1px;} 75% {left: 0px;} 100% {left: 2px;}}.flagsForm{float: right; clear: right; margin: 0 20px 10px 0;} #flagSelect{display:none;} #flagSelect ul{list-style-type: none;padding: 0;margin-bottom: 0;cursor: pointer;bottom: 100%;height: 200px;overflow: auto;position: absolute;width:200px;background-color:#fff} #flagSelect ul li {display: block;} #flagSelect ul li:hover {background-color: #ddd;}#flagSelect {position: absolute;}#flagSelect input {width: 200px;} #flagSelect .hide {display: none;}#flagSelect img {margin-left: 2px;}')
  251.  
  252. if (software.yotsuba) {
  253. getPosts('.postContainer');
  254.  
  255. addStyle('.flag{top: 0px;left: -1px}');
  256. init();
  257. }
  258.  
  259. else if (software.nodegucaDoushio) {
  260. getPosts('section[id], article[id]');
  261.  
  262. addStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}');
  263. init();
  264. }
  265.  
  266. else if (software.foolfuuka) {
  267. getPosts('article[id]');
  268.  
  269. addStyle('.bantFlag{top: -2px !important;left: -1px !important}');
  270. }
  271.  
  272. board_id = window.location.pathname.split('/')[1];
  273. debug(`board: ${board_id}`);
  274.  
  275. try {
  276. resolveFlags();
  277. }
  278. catch (ywucky) {
  279. console.log(`Wah! Manx ed something up ;~;\nPoke him somewhere with this:\n${ywucky}`)
  280. }
  281. }
  282.  
  283. document.addEventListener('dblclick', e => {
  284. if (e.target.parentNode.classList.contains('bantFlag')) {
  285. setFlag(e.target.title, true);
  286. }
  287. });
  288.  
  289. if (isGM4) { // you GreaseMonkey
  290. (async () => {
  291. regions = await getValue(namespace);
  292. main();
  293. })();
  294. }
  295. else {
  296. regions = getValue(namespace);
  297. main();
  298. }
  299.  
  300. const postFlags = (post_nr, func = resp => debug(resp.responseText)) => makeRequest(
  301. 'POST',
  302. api_post,
  303. `post_nr=${encodeURIComponent(post_nr)}&board=${encodeURIComponent(board_id)}&regions=${encodeURIComponent(regions)}&version=${version}`,
  304. func);
  305.  
  306. if (software.yotsuba) {
  307. const e_detail = e => e.detail || e.wrappedJSObject.detail// what?
  308. document.addEventListener('QRPostSuccessful', e => postFlags(e_detail(e).postID));
  309. document.addEventListener('4chanQRPostSuccess', e => postFlags(e_detail(e).postId));
  310.  
  311. document.addEventListener('ThreadUpdate', e => {
  312. const d = e_detail(e)
  313. if (d[404]) return;
  314.  
  315. d.newPosts.forEach(post => postNrs.push(post.split('.')[1]));
  316.  
  317. resolveFlags();
  318. });
  319.  
  320. document.addEventListener('4chanThreadUpdated', e => {
  321. const d = e_detail(e);
  322. if (d.count <= 0) return;
  323.  
  324. // Get the added posts in reverse order, take post numbers from ID
  325. const posts = document.querySelectorAll('.postContainer');
  326. for (let i = 0; i < d.count; i++) {
  327. postNrs.push(posts[posts.length - 1 - i].id.substr(2));
  328. }
  329.  
  330. resolveFlags();
  331. });
  332. }
  333.  
  334. if (software.nodegucaDoushio) {
  335. const postFunc = () => {
  336. postNrs.push(mutation.target.id);
  337. resolveFlags();
  338. }
  339.  
  340. const badNodes = ['HR', 'SECTION'];
  341.  
  342. new MutationObserver(mutations => {
  343. mutations.forEach(mutation => {
  344. if (mutation.addedNodes.length <= 0)
  345. return; // We only care if something post was added
  346.  
  347. const firstAddedNode = mutation.addedNodes[0].nodeName;
  348.  
  349. // Enter a thread / change boards
  350. if (mutation.target.nodeName === 'THREADS') {
  351. if (badNodes.includes(firstAddedNode))
  352. return; // We are in the index and a post was added, handled properly further down
  353.  
  354. board_id = window.location.pathname.split('/')[1];
  355. setTimeout(getPosts('section[id], article[id]'), 2000);
  356. resolveFlags();
  357. init();
  358. }
  359.  
  360. // We post
  361. if (firstAddedNode === 'HEADER') {
  362. postFlags(mutation.target.id, postFunc)
  363. }
  364.  
  365. // Someone else posts
  366. if (firstAddedNode === 'ARTICLE') {
  367. if (mutation.target.nodeName === 'BODY' || mutation.target.id === 'hover_overlay')
  368. return; // User is hovering over a post
  369.  
  370. postNrs.push(mutation.addedNodes[0].id);
  371. setTimeout(resolveFlags, 1500);
  372. }
  373. });
  374. }).observe(document.body, { childList: true, subtree: true });
  375. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement