Create GreaseMonkey4 compatible script. Add JSDoc strings to most functions.

The script also checks to see if you have the wrong version of bantflags for your userscript manager. Some general improvements to posting flags on 4chan.
dotnetflags
C-xC-c 4 years ago
parent d6b0a2af84
commit 421fcdca5c

@ -0,0 +1,22 @@
// ==UserScript==
// @name /bant/ Flags for GreaseMonkey 4
// @namespace BintFlegs_gm4
// @description More flags for r/banter
// @include http*://boards.4chan.org/bant/*
// @include http*://archive.nyafuu.org/bant/*
// @include http*://archived.moe/bant/*
// @include http*://thebarchive.com/bant/*
// @include http*://nineball.party/*
// @exclude http*://boards.4chan.org/bant/catalog
// @exclude http*://archive.nyafuu.org/bant/statistics/
// @exclude http*://archived.moe/bant/statistics/
// @exclude http*://thebarchive.com/bant/statistics/
// @version 1.3.0
// @grant GM.xmlHttpRequest
// @grant GM.getValue
// @grant GM.setValue
// @run-at document-idle
// @icon https://nineball.party/files/flags/actual_flags/0077.png
// @updateURL https://flags.plum.moe/bantflags.gm4.meta.js
// @downloadURL https://flags.plum.moe/bantflags.gm4.user.js
// ==/UserScript==

@ -0,0 +1,412 @@
// ==UserScript==
// @name /bant/ Flags for GreaseMonkey 4
// @namespace BintFlegs_gm4
// @description More flags for r/banter
// @include http*://boards.4chan.org/bant/*
// @include http*://archive.nyafuu.org/bant/*
// @include http*://archived.moe/bant/*
// @include http*://thebarchive.com/bant/*
// @include http*://nineball.party/*
// @exclude http*://boards.4chan.org/bant/catalog
// @exclude http*://archive.nyafuu.org/bant/statistics/
// @exclude http*://archived.moe/bant/statistics/
// @exclude http*://thebarchive.com/bant/statistics/
// @version 1.3.0
// @grant GM.xmlHttpRequest
// @grant GM.getValue
// @grant GM.setValue
// @run-at document-idle
// @icon https://nineball.party/files/flags/actual_flags/0077.png
// @updateURL https://flags.plum.moe/bantflags.gm4.meta.js
// @downloadURL https://flags.plum.moe/bantflags.gm4.user.js
// ==/UserScript==
// (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of /bant/ Flags.
// /bant/ Flags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/>
// Change this if you want verbose debuging information in the console.
const debugMode = true;
// For idiots using GM4. Does anyone else not support GM_ functions?
if (typeof GM === 'undefined') {
if (window.confirm('[BantFlags] This version of bantflags is specifically for GreaseMonkey 4.0 +. Press \'ok\' to download correct version.')) {
window.location.href = "https://flags.plum.moe/bantflags.user.js"; // Should I use thhis or window.location.replace?
}
return;
}
//
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT
//
const postRemoveCounter = 60;
const requestRetryInterval = 5000; // TODO: maybe a max retries counter as well?
const version = 2; // Breaking changes.
const back_end = 'https://flags.plum.moe/';
const api_flags = 'api/flags';
const flag_dir = 'flags/';
const api_get = 'api/get';
const api_post = 'api/post';
// If you increase this the server will ignore your post.
const max_flags = 30;
var regions = []; // The flags we have selected.
var postNrs = []; // all post numbers in the thread.
var board_id = ""; // The board we get flags for.
//
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT
//
// Test unqiue CSS paths to figure out what board software we're using.
const software = {
yotsuba: window.location.host === 'boards.4chan.org',
nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null,
foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null
};
// This is kinda necessary but I don't know how much. Can I just wrap checking regions on startup or is the whole script required? I won't be testing that for now.
(async () => {
/** Wrapper around Object.assign and document.createElement
* @param {string} element - The HTML Element to create.
* @param {Object} source - The properties to assign to the element.
* @returns {object} The HTML tag created*/
const createAndAssign = (element, source) => Object.assign(document.createElement(element), source);
/** Add a stylesheet to the head of the document.
* @param {string} css - The CSS rules for the stylesheet.
* @returns {object} The style element appended to the head */
const addGlobalStyle = css => document.head.appendChild(createAndAssign('style', {
type: 'text/css',
innerHTML: css
}));
/** Write extra information to the console if debugMode is set to true.
* @param {string} text - The text to write to the console. */
function debug(text) {
if (debugMode) {
console.log('[BantFlags] ' + text);
}
}
/** Wrapper around GM.xmlhttpRequest.
* @param {string} method - The HTTP method (GET, POST).
* @param {string} url - The URL of the request.
* @param {string} data - Data sent inn the form body.
* @param {Function} func - The function run when we recieve a response. Response data is sent directly to it. */
const MakeRequest = ((method, url, data, func) => {
GM.xmlHttpRequest({
method: method,
url: url,
data: data,
headers: { "Content-Type": 'application/x-www-form-urlencoded' },
onload: func
});
});
/** Try some MakeRequest again if it fails.
* @param {function} func - The function to retry.
* @param {XMLHttpRequest} resp - The XMLHttpResponse from the failed request.*/
function retry(func, resp) {
console.log('[BantFlags] Could not fetch flags, status: ' + resp.status);
console.log(resp.statusText);
setTimeout(func, requestRetryInterval);
}
// TODO: this shouldn't be a object.
var nsetup = { // not anymore a clone of the original setup
namespace: 'BintFlegs', // TODO: should be const.
flagsLoaded: false,
form: '<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. Saving happens automatically, you only need to refresh the pages that have an outdated flaglist on the page."><<</button><button id="flagLoad" type="button">Click to load flags.</button><select id="flagSelect"></select>',
fillHtml: function () { // TODO: this function should have a better name. Only called by nsetup.init, can be inlined?
MakeRequest(
"GET",
back_end + api_flags,
"version=" + encodeURIComponent(version),
function (resp) {
debug('Loading flags.');
if (resp.status !== 200) {
retry(nsetup.fillHtml, resp);
return;
}
let flagSelect = document.getElementById('flagSelect');
let flagLoad = document.getElementById('flagLoad');
let flags = resp.responseText.split('\n');
for (var i = 0; i < flags.length; i++) {
let flag = flags[i];
flagSelect.appendChild(createAndAssign('option', {
value: flag,
innerHTML: '<img src="' + back_end + flag_dir + flag + '.png" title="' + flag + '"> ' + flag
}));
}
flagLoad.style.display = 'none';
flagSelect.style.display = 'inline-block';
nsetup.flagsLoaded = true;
});
},
save: function () {
let storedFlags = [];
let selectedFlags = document.getElementsByClassName("bantflags_flag");
for (var i = 0; i < selectedFlags.length; i++) {
storedFlags[i] = selectedFlags[i].title;
}
GM.setValue(nsetup.namespace, storedFlags);
regions = GM.getValue(nsetup.namespace);
},
setFlag: function (flag) {
let UID = Math.random().toString(36).substring(7);
let flagName = flag ? flag : document.getElementById('flagSelect').value;
let flagContainer = document.getElementById('bantflags_container');
flagContainer.appendChild(createAndAssign('img', {
title: flagName,
src: back_end + flag_dir + flagName + '.png',
id: UID,
className: 'bantflags_flag'
}));
if (flagContainer.children.length >= max_flags) {
nsetup.toggleFlagButton('off');
}
document.getElementById(UID).addEventListener("click", (e) => {
flagContainer.removeChild(e.target);
nsetup.toggleFlagButton('on');
nsetup.save();
});
if (!flag) { // When we add a flag to our selection, save it for when we reload the page.
nsetup.save();
}
},
init: function () {
let flagsForm = createAndAssign('div', {
className: 'flagsForm',
innerHTML: nsetup.form
});
// Where do we append the flagsForm to?
if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); }
if (software.nodegucaDoushio) { document.querySelector('section').append(flagsForm); } // As posts are added the flagForm moves up the page. Could we append this after .section?
for (var i in regions) {
nsetup.setFlag(regions[i]);
}
document.getElementById('append_flag_button').addEventListener('click',
() => nsetup.flagsLoaded ? nsetup.setFlag() : alert('Load flags before adding them.'));
document.getElementById('flagLoad').addEventListener('click', nsetup.fillHtml, { once: true });
},
toggleFlagButton: state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false
};
/** Select all of the post numbers on the page.
* @param {string} selector - The CSS selector of the post numbers. */
function getPosts(selector) {
let posts = document.querySelectorAll(selector);
for (var i = 0; i < posts.length; i++) {
let postNumber = software.yotsuba
? posts[i].id.replace('pc', '') // Fuck you 4chan.
: posts[i].id;
postNrs.push(postNumber);
}
debug(postNrs);
}
/** Take the response from resolveRefFlags and append flags to their respective post numbers.
* @param {XMLHttpRequest} response - The response data from resolveRefFlags. */
function onFlagsLoad(response) {
debug('JSON: ' + response.responseText);
var jsonData = JSON.parse(response.responseText);
Object.keys(jsonData).forEach(function (post) {
// Get the post header with a CSS selector. Different for each board software.
var flagContainer;
if (software.nodegucaDoushio) { flagContainer = document.querySelector('[id="' + post + '"] header'); }
if (software.yotsuba) { flagContainer = document.querySelector('[id="pc' + post + '"] .postInfo .nameBlock'); }
if (software.foolfuuka) { flagContainer = document.querySelector('[id="' + post + '"] .post_data .post_type'); }
let flags = jsonData[post];
if (flags.length > 0) {
console.log('[BantFlags] Resolving flags for >>' + post);
for (var i = 0; i < flags.length; i++) {
let flag = flags[i];
let newFlag = createAndAssign('a', {
innerHTML: '<img src="' + back_end + flag_dir + flag + '.png" title="' + flag + '"> ',
className: 'bantFlag',
target: '_blank'
});
if (software.foolfuuka) {
newFlag.style = 'padding: 0px 0px 0px ' + (3 + 2 * (i > 0)) + 'px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;';
}
if (software.nodegucaDoushio) {
newFlag.title = flag;
}
flagContainer.append(newFlag);
console.log('\t -> ' + flag);
}
}
});
postNrs = [];
}
/** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */
function resolveRefFlags() {
debug('Board is: ' + board_id);
MakeRequest(
'POST',
back_end + api_get,
'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + encodeURIComponent(version),
function (resp) {
if (resp.status !== 200) {
retry(resolveRefFlags, resp);
return;
}
onFlagsLoad(resp);
}
);
}
// This one await is what stops regular bantflags from working with GM4 lole.
regions = await GM.getValue(nsetup.namespace);
if (!regions) { // No flags set.
regions = [];
window.confirm('[BantFlags]: No Flags detected.');
}
// TODO: look at this CSS and see what's important / where we can merge some of it.
addGlobalStyle('.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;} .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;}}');
// Flags need to be parsed differently and have different styles depending on board software.
if (software.yotsuba) {
debug('4chan');
board_id = 'bant';
getPosts('.postContainer');
addGlobalStyle('.bantFlag {padding: 0px 0px 0px 5px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;} .flag{top: 0px !important;left: -1px !important}');
}
if (software.nodegucaDoushio) {
debug('Nineball');
board_id = window.location.pathname.split('/')[1]; // 'nap' or 'srsbsn'
getPosts('section[id], article[id]');
addGlobalStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}');
}
if (software.foolfuuka) {
debug('FoolFuuka');
board_id = 'bant';
getPosts('article[id]');
addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}');
}
resolveRefFlags();
if (software.yotsuba) {
const GetEvDetail = e => e.detail || e.wrappedJSObject.detail;
const postFlags = post_nr => MakeRequest(
'POST',
back_end + api_post,
'post_nr=' + encodeURIComponent(post_nr) + '&board=' + encodeURIComponent(board_id) + '&regions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version),
func = resp => debug(resp.responseText));
// Send flags to the backend when we makle a post. Top is 4chanX, bottom is native extension.
document.addEventListener('QRPostSuccessful', e => postFlags(e.detail.postID), false);
document.addEventListener('4chanQRPostSuccess', e => postFlags(GetEvDetail(e).postId), false);
// I need to look at these.
document.addEventListener('ThreadUpdate', function (e) {
var evDetail = GetEvDetail(e);
var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail;
//ignore if 404 event
if (evDetail[404] === true) {
return;
}
evDetailClone.newPosts.forEach(function (post_board_nr) {
var post_nr = post_board_nr.split('.')[1];
postNrs.push(post_nr);
});
resolveRefFlags();
}, false);
document.addEventListener('4chanThreadUpdated', function (e) {
var evDetail = GetEvDetail(e);
let threadID = window.location.pathname.split('/')[3];
let postsContainer = Array.prototype.slice.call(document.getElementById('t' + threadID).childNodes);
let lastPosts = postsContainer.slice(Math.max(postsContainer.length - evDetail.count, 1)); //get the last n elements (where n is evDetail.count)
lastPosts.forEach(function (post_container) {
var post_nr = post_container.id.replace('pc', '');
postNrs.push(post_nr);
});
resolveRefFlags();
}, false);
nsetup.init();
}
if (software.nodegucaDoushio) {
nsetup.init();
// This is poking at the mutations made on the page to figure out what happened and thus what actions to take.
// There is full support for nodeguca but I don't have a Doushio board I feel comfortable spamming to ensure it works properly there. There is at least partial support.
new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.addedNodes.length > 0) { // A post was added.
var firstAddedNode = mutation.addedNodes[0].nodeName;
// Enter a thread or change boards. Checks for when a post is added while in the index.
if (mutation.target.nodeName === 'THREADS' && firstAddedNode !== 'HR' && firstAddedNode !== 'SECTION') {
board_id = window.location.pathname.split('/')[1];
setTimeout(getPosts('section[id], article[id]'), 2000);
resolveRefFlags();
nsetup.init();
}
// You post.
if (firstAddedNode === 'HEADER') {
let data = 'post_nr=' + encodeURIComponent(mutation.target.id) + '&board=' + encodeURIComponent(board_id) + '&regions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version);
MakeRequest(
'POST',
back_end + api_post,
data,
function () {
postNrs.push(mutation.target.id);
resolveRefFlags();
});
}
// Someone else posts. Checks to see if you're hovering over a post.
if (firstAddedNode === 'ARTICLE' && mutation.target.nodeName !== "BODY" && mutation.target.id !== 'hover_overlay') {
postNrs.push(mutation.addedNodes[0].id);
setTimeout(resolveRefFlags, 1500);
}
}
});
}).observe(document.body, { childList: true, subtree: true });
}
})();

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name BantFlags // @name /bant/ Flags
// @namespace BintFlegs // @namespace BintFlegs
// @description More flags for r/banter // @description More flags for r/banter
// @include http*://boards.4chan.org/bant/* // @include http*://boards.4chan.org/bant/*
@ -11,11 +11,11 @@
// @exclude http*://archive.nyafuu.org/bant/statistics/ // @exclude http*://archive.nyafuu.org/bant/statistics/
// @exclude http*://archived.moe/bant/statistics/ // @exclude http*://archived.moe/bant/statistics/
// @exclude http*://thebarchive.com/bant/statistics/ // @exclude http*://thebarchive.com/bant/statistics/
// @version 1.2.1 // @version 1.3.0
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @grant GM_getValue // @grant GM_getValue
// @grant GM_setValue // @grant GM_setValue
// @run-at document-end // @run-at document-idle
// @icon https://nineball.party/files/flags/actual_flags/0077.png // @icon https://nineball.party/files/flags/actual_flags/0077.png
// @updateURL https://flags.plum.moe/bantflags.meta.js // @updateURL https://flags.plum.moe/bantflags.meta.js
// @downloadURL https://flags.plum.moe/bantflags.user.js // @downloadURL https://flags.plum.moe/bantflags.user.js

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name BantFlags // @name /bant/ Flags
// @namespace BintFlegs // @namespace BintFlegs
// @description More flags for r/banter // @description More flags for r/banter
// @include http*://boards.4chan.org/bant/* // @include http*://boards.4chan.org/bant/*
@ -11,26 +11,32 @@
// @exclude http*://archive.nyafuu.org/bant/statistics/ // @exclude http*://archive.nyafuu.org/bant/statistics/
// @exclude http*://archived.moe/bant/statistics/ // @exclude http*://archived.moe/bant/statistics/
// @exclude http*://thebarchive.com/bant/statistics/ // @exclude http*://thebarchive.com/bant/statistics/
// @version 1.2.1 // @version 1.3.0
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @grant GM_getValue // @grant GM_getValue
// @grant GM_setValue // @grant GM_setValue
// @run-at document-end // @run-at document-idle
// @icon https://nineball.party/files/flags/actual_flags/0077.png // @icon https://nineball.party/files/flags/actual_flags/0077.png
// @updateURL https://flags.plum.moe/bantflags.meta.js // @updateURL https://flags.plum.moe/bantflags.meta.js
// @downloadURL https://flags.plum.moe/bantflags.user.js // @downloadURL https://flags.plum.moe/bantflags.user.js
// ==/UserScript== // ==/UserScript==
// (C) Copyright 2019 C-xC-c <boku@plum.moe> // (C) Copyright 2019 C-xC-c <boku@plum.moe>
// This file is part of BantFlags. // This file is part of /bant/ Flags.
// BantFlags is licensed under the GNU AGPL Version 3.0 or later. // /bant/ Flags is licensed under the GNU AGPL Version 3.0 or later.
// see the LICENSE file or <https://www.gnu.org/licenses/> // see the LICENSE file or <https://www.gnu.org/licenses/>
// This script specifically targets ECMAScript 2015 (const, let, arrow functions). Update your hecking browser.
// Change this if you want verbose debuging information in the console. // Change this if you want verbose debuging information in the console.
const debugMode = false; const debugMode = false;
// For idiots using GM4. Does anyone else not support GM_ functions?
if (typeof GM_setValue === 'undefined') {
if (window.confirm('[BantFlags] Couldn\' find required userscript functions. If you\'re using GreaseMonkey 4, press \'ok\' to be redirected to the correct version of bantflags.')) {
window.location.href = "https://flags.plum.moe/bantflags.gm4.user.js"; // Should I use thhis or window.location.replace?
}
return;
}
// //
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT // DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT
// //
@ -49,372 +55,353 @@ const max_flags = 30;
var regions = []; // The flags we have selected. var regions = []; // The flags we have selected.
var postNrs = []; // all post numbers in the thread. var postNrs = []; // all post numbers in the thread.
var board_id = ""; // The board we get flags for. var board_id = ""; // The board we get flags for.
//
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT
//
// Test unqiue CSS paths to figure out what board software we're using. // Test unqiue CSS paths to figure out what board software we're using.
const software = { const software = {
yotsuba: window.location.host === 'boards.4chan.org', yotsuba: window.location.host === 'boards.4chan.org',
nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null, nodegucaDoushio: document.querySelector('b[id="sync"], span[id="sync"]') !== null,
foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null foolfuuka: document.querySelector('div[id="main"] article header .post_data') !== null
}; };
// /** Wrapper around Object.assign and document.createElement
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT * @param {string} element - The HTML Element to create.
// * @param {object} source - The properties to assign to the element.
const sliceCall = x => Array.prototype.slice.call(x); * @returns {object} The HTML tag created */
const createAndAssign = (element, source) => Object.assign(document.createElement(element), source); const createAndAssign = (element, source) => Object.assign(document.createElement(element), source);
function addGlobalStyle(css) { /** Add a stylesheet to the head of the document.
let head = document.getElementsByTagName('head')[0]; * @param {string} css - The CSS rules for the stylesheet.
if (!head) { * @returns {object} The style element appended to the head */
console.error('[BantFlags] No head tag??'); const addGlobalStyle = css => document.head.appendChild(createAndAssign('style', {
return; type: 'text/css',
} innerHTML: css
}));
head.appendChild(createAndAssign('style', {
type: 'text/css',
innerHTML: css
}));
}
/** Write extra information to the console if debugMode is set to true.
* @param {string} text - The text to write to the console. */
function debug(text) { function debug(text) {
if (debugMode) { if (debugMode) {
console.log('[BantFlags] ' + text); console.log('[BantFlags] ' + text);
} }
}
/** Wrapper around GM_xmlhttpRequest
* @param {string} method - The HTTP method (GET, POST)
* @param {string} url - The URL of the request
* @param {string} data - Data sent inn the form body
* @param {Function} func - The function run after onload. Response data is sent directly to it. */
function MakeRequest(method, url, data, func) {
GM_xmlhttpRequest({
method: method,
url: url,
data: data,
headers: {
"Content-Type": 'application/x-www-form-urlencoded'
},
onload: func
});
} }
/** Wrapper around GM_xmlhttpRequest.
* @param {string} method - The HTTP method (GET, POST).
* @param {string} url - The URL of the request.
* @param {string} data - Data sent inn the form body.
* @param {Function} func - The function run when we recieve a response. Response data is sent directly to it. */
const MakeRequest = ((method, url, data, func) => {
GM_xmlhttpRequest({
method: method,
url: url,
data: data,
headers: { "Content-Type": 'application/x-www-form-urlencoded' },
onload: func
});
});
/** Try some MakeRequest again if it fails.
* @param {function} func - The function to retry.
* @param {XMLHttpRequest} resp - The XMLHttpResponse from the failed request.*/
function retry(func, resp) { function retry(func, resp) {
console.log('[BantFlags] Could not fetch flags, status: ' + resp.status); console.log('[BantFlags] Could not fetch flags, status: ' + resp.status);
console.log(resp.statusText); console.log(resp.statusText);
setTimeout(func, requestRetryInterval); setTimeout(func, requestRetryInterval);
} }
/** nSetup, preferences */
// TODO: this shouldn't be a object. // TODO: this shouldn't be a object.
var nsetup = { // not anymore a clone of the original setup var nsetup = { // not anymore a clone of the original setup
namespace: 'BintFlegs', // TODO: should be const. namespace: 'BintFlegs', // TODO: should be const.
flagsLoaded: false, flagsLoaded: false,
form: '<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. Saving happens automatically, you only need to refresh the pages that have an outdated flaglist on the page."><<</button><button id="flagLoad" type="button">Click to load flags.</button><select id="flagSelect"></select>', form: '<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. Saving happens automatically, you only need to refresh the pages that have an outdated flaglist on the page."><<</button><button id="flagLoad" type="button">Click to load flags.</button><select id="flagSelect"></select>',
fillHtml: function () { // TODO: this function should have a better name. Only called by nsetup.init, can be inlined? fillHtml: function () { // TODO: this function should have a better name. Only called by nsetup.init, can be inlined?
MakeRequest(
// resolve flags "GET",
MakeRequest( back_end + api_flags,
"GET", "version=" + encodeURIComponent(version),
back_end + api_flags, function (resp) {
"version=" + encodeURIComponent(version), debug('Loading flags.');
function (resp) { if (resp.status !== 200) {
debug('Loading flags'); retry(nsetup.fillHtml, resp);
if (resp.status !== 200) { return;
retry(nsetup.fillHtml, resp);
return;
}
let flagSelect = document.getElementById('flagSelect');
let flagLoad = document.getElementById('flagLoad');
let flagsSupported = resp.responseText.split('\n');
for (var i = 0; i < flagsSupported.length; i++) {
let flag = flagsSupported[i];
flagSelect.appendChild(createAndAssign('option', {
value: flag,
innerHTML: '<img src="' + back_end + flag_dir + flag + '.png" title="' + flag + '"> ' + flag
}));
}
flagLoad.style.display = 'none';
flagSelect.style.display = 'inline-block';
nsetup.flagsLoaded = true;
});
},
save: function (v) {
GM_setValue(nsetup.namespace, v);
regions = GM_getValue(nsetup.namespace);
},
setFlag: function (flag) {
let UID = Math.random().toString(36).substring(7);
let flagName = flag ? flag : document.getElementById('flagSelect').value;
let flagContainer = document.getElementById('bantflags_container');
flagContainer.appendChild(createAndAssign('img', {
title: flagName,
src: back_end + flag_dir + flagName + '.png',
id: UID,
className: 'bantflags_flag'
}));
if (flagContainer.children.length >= max_flags) {
nsetup.toggleFlagButton('off');
} }
document.getElementById(UID).addEventListener("click", function (e) { let flagSelect = document.getElementById('flagSelect');
flagContainer.removeChild(e.target); let flagLoad = document.getElementById('flagLoad');
nsetup.toggleFlagButton('on'); let flags = resp.responseText.split('\n');
nsetup.save(nsetup.parse());
});
if (!flag) { for (var i = 0; i < flags.length; i++) {
nsetup.save(nsetup.parse()); let flag = flags[i];
flagSelect.appendChild(createAndAssign('option', {
value: flag,
innerHTML: '<img src="' + back_end + flag_dir + flag + '.png" title="' + flag + '"> ' + flag
}));
} }
},
init: function () { flagLoad.style.display = 'none';
let flagsForm = createAndAssign('div', { flagSelect.style.display = 'inline-block';
className: 'flagsForm', nsetup.flagsLoaded = true;
innerHTML: nsetup.form });
}); },
save: function () {
let storedFlags = [];
let selectedFlags = document.getElementsByClassName("bantflags_flag");
for (var i = 0; i < selectedFlags.length; i++) {
storedFlags[i] = selectedFlags[i].title;
}
// Where do we append the flagsForm to? GM_setValue(nsetup.namespace, storedFlags);
if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); } regions = GM_getValue(nsetup.namespace); // TODO: this could be selectedFlags? GM_getValue is expensive.
if (software.nodegucaDoushio) { document.querySelector('section').append(flagsForm); } },
setFlag: function (flag) {
let UID = Math.random().toString(36).substring(7);
let flagName = flag ? flag : document.getElementById('flagSelect').value;
let flagContainer = document.getElementById('bantflags_container');
flagContainer.appendChild(createAndAssign('img', {
title: flagName,
src: back_end + flag_dir + flagName + '.png',
id: UID,
className: 'bantflags_flag'
}));
for (var i in regions) { if (flagContainer.children.length >= max_flags) {
nsetup.setFlag(regions[i]); nsetup.toggleFlagButton('off');
} }
document.getElementById('append_flag_button').addEventListener('click', document.getElementById(UID).addEventListener("click", (e) => {
() => nsetup.flagsLoaded ? nsetup.setFlag() : alert('Load flags before adding them.')); flagContainer.removeChild(e.target);
nsetup.toggleFlagButton('on');
nsetup.save();
});
document.getElementById('flagLoad').addEventListener('click', nsetup.fillHtml, { once: true }); if (!flag) { // When we add a flag to our selection, save it for when we reload the page.
}, nsetup.save();
parse: function () { }
let flagsArray = []; },
let flagElements = document.getElementsByClassName("bantflags_flag"); init: function () {
let flagsForm = createAndAssign('div', {
className: 'flagsForm',
innerHTML: nsetup.form
});
for (var i = 0; i < flagElements.length; i++) { // Where do we append the flagsForm to?
flagsArray[i] = flagElements[i].title; if (software.yotsuba) { document.getElementById('delform').appendChild(flagsForm); }
} if (software.nodegucaDoushio) { document.querySelector('section').append(flagsForm); } // As posts are added the flagForm moves up the page. Could we append this after .section?
return flagsArray; for (var i in regions) {
}, nsetup.setFlag(regions[i]);
toggleFlagButton: state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false }
};
/** Prompt to set region if regions is empty */ document.getElementById('append_flag_button').addEventListener('click',
regions = GM_getValue(nsetup.namespace); // TODO: move this to other init stuff () => nsetup.flagsLoaded ? nsetup.setFlag() : alert('Load flags before adding them.'));
if (!regions) {
regions = [];
setTimeout(function () {
window.confirm('Bant Flags: No Flags detected');
}, 2000);
}
function getPosts(selector) { document.getElementById('flagLoad').addEventListener('click', nsetup.fillHtml, { once: true });
let posts = document.querySelectorAll(selector); },
toggleFlagButton: state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false
};
for (var i = 0; i < posts.length; i++) { /** add all of thhe post numbers on the page to postNrs.
let postNumber = software.yotsuba * @param {string} selector - The CSS selector who's id is the post number. */
? posts[i].id.replace('pc', '') // Fuck you 4chan. function getPosts(selector) {
: posts[i].id; let posts = document.querySelectorAll(selector);
postNrs.push(postNumber);
} for (var i = 0; i < posts.length; i++) {
debug(postNrs); let postNumber = software.yotsuba
? posts[i].id.replace('pc', '') // Fuck you 4chan.
: posts[i].id;
postNrs.push(postNumber);
}
debug(postNrs);
} }
/** Take the response from resolveRefFlags and append flags to their respective post numbers.
* @param {XMLHttpRequest} response - The response data from resolveRefFlags. */
function onFlagsLoad(response) { function onFlagsLoad(response) {
let MakeFlag = (flag) => debug('JSON: ' + response.responseText);
createAndAssign('a', { var jsonData = JSON.parse(response.responseText);
innerHTML: '<img src="' + back_end + flag_dir + flag + '.png" title="' + flag + '"> ',
className: 'bantFlag',
target: '_blank'
});
debug('JSON: ' + response.responseText);
var jsonData = JSON.parse(response.responseText);
Object.keys(jsonData).forEach(function (post) { Object.keys(jsonData).forEach(function (post) {
// Here we get the header of the post using a different CSS selector depending on the board. // Get the post header with a CSS selector. Different for each board software.
var flagContainer; var flagContainer;
if (software.nodegucaDoushio) { flagContainer = document.querySelector('[id="' + post + '"] header'); } if (software.nodegucaDoushio) { flagContainer = document.querySelector('[id="' + post + '"] header'); }
if (software.yotsuba) { flagContainer = document.querySelector('[id="pc' + post + '"] .postInfo .nameBlock'); } if (software.yotsuba) { flagContainer = document.querySelector('[id="pc' + post + '"] .postInfo .nameBlock'); }
if (software.foolfuuka) { flagContainer = document.querySelector('[id="' + post + '"] .post_data .post_type'); } if (software.foolfuuka) { flagContainer = document.querySelector('[id="' + post + '"] .post_data .post_type'); }
let flags = jsonData[post]; let flags = jsonData[post];
if (flags.length > 0) { if (flags.length > 0) {
console.log('[BantFlags] Resolving flags for >>' + post); console.log('[BantFlags] Resolving flags for >>' + post);
for (var i = 0; i < flags.length; i++) { for (var i = 0; i < flags.length; i++) {
let flag = flags[i]; let flag = flags[i];
let newFlag = MakeFlag(flag); let newFlag = createAndAssign('a', {
innerHTML: '<img src="' + back_end + flag_dir + flag + '.png" title="' + flag + '"> ',
className: 'bantFlag',
target: '_blank'
});
if (software.foolfuuka) { if (software.foolfuuka) {
newFlag.style = 'padding: 0px 0px 0px ' + (3 + 2 * (i > 0)) + 'px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;'; newFlag.style = 'padding: 0px 0px 0px ' + (3 + 2 * (i > 0)) + 'px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;';
} }
if (software.nodegucaDoushio) { if (software.nodegucaDoushio) {
newFlag.title = flag; newFlag.title = flag;
} }
flagContainer.append(newFlag); flagContainer.append(newFlag);
console.log('\t -> ' + flag); console.log('\t -> ' + flag);
} }
} }
}); });
postNrs = []; postNrs = [];
} }
/** Gets flags from the database. */ /** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */
function resolveRefFlags() { function resolveRefFlags() {
debug('Board is: ' + board_id); debug('Board is: ' + board_id);
MakeRequest( MakeRequest(
'POST', 'POST',
back_end + api_get, back_end + api_get,
'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + encodeURIComponent(version), 'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + encodeURIComponent(version),
function (resp) { function (resp) {
if (resp.status !== 200) { if (resp.status !== 200) {
retry(resolveRefFlags, resp); retry(resolveRefFlags, resp);
return; return;
} }
onFlagsLoad(resp); onFlagsLoad(resp);
} }
); );
} }
addGlobalStyle('.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;}'); regions = GM_getValue(nsetup.namespace);
addGlobalStyle('.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;}}'); if (!regions) {
regions = [];
window.confirm('[BantFlags]: No Flags detected.');
}
// TODO: look at this CSS and see what's important / where we can merge some of it.
addGlobalStyle('.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;} .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;}}');
// Flags need to be parsed differently and have different styles depending on board software. // We get flags using different selectors, and we need to align them differently.
if (software.yotsuba) { if (software.yotsuba) {
debug('4chan'); debug('4chan');
board_id = 'bant'; board_id = 'bant';
getPosts('.postContainer'); getPosts('.postContainer');
addGlobalStyle('.bantFlag {padding: 0px 0px 0px 5px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;} .flag{top: 0px !important;left: -1px !important}'); addGlobalStyle('.bantFlag {padding: 0px 0px 0px 5px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;} .flag{top: 0px !important;left: -1px !important}');
} }
if (software.nodegucaDoushio) { if (software.nodegucaDoushio) {
debug('Nineball'); debug('Nineball');
board_id = window.location.pathname.split('/')[1]; // 'nap' or 'srsbsn' board_id = window.location.pathname.split('/')[1]; // 'nap' or 'srsbsn'
getPosts('section[id], article[id]'); getPosts('section[id], article[id]');
addGlobalStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}'); addGlobalStyle('.bantFlag {cursor: default} .bantFlag img {pointer-events: none;}');
} }
if (software.foolfuuka) { if (software.foolfuuka) {
debug('FoolFuuka'); debug('FoolFuuka');
board_id = 'bant'; board_id = 'bant';
getPosts('article[id]'); getPosts('article[id]');
addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}'); addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}');
} }
resolveRefFlags(); // Get flags from DB then add them to posts. resolveRefFlags();
// Posting new flags and getting flags as posts are added to the thread.
if (software.yotsuba) { if (software.yotsuba) {
let GetEvDetail = e => e.detail || e.wrappedJSObject.detail; const GetEvDetail = e => e.detail || e.wrappedJSObject.detail;
let method = 'POST', const postFlags = post_nr => MakeRequest(
url = back_end + api_post, 'POST',
func = function (resp) { back_end + api_post,
debug(resp.responseText); 'post_nr=' + encodeURIComponent(post_nr) + '&board=' + encodeURIComponent(board_id) + '&regions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version),
}; func = resp => debug(resp.responseText));
document.addEventListener('QRPostSuccessful', function (e) { // Send flags to the backend when we makle a post. Top is 4chanX, bottom is native extension.
var data = 'post_nr=' + encodeURIComponent(e.detail.postID) + '&board=' + encodeURIComponent(e.detail.boardID) + '&regions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version); document.addEventListener('QRPostSuccessful', e => postFlags(e.detail.postID), false);
MakeRequest(method, url, data, func); document.addEventListener('4chanQRPostSuccess', e => postFlags(GetEvDetail(e).postId), false);
}, false);
// I need to look at these.
/** send flag to system on 4chan inline post */ document.addEventListener('ThreadUpdate', function (e) {
document.addEventListener('4chanQRPostSuccess', function (e) { var evDetail = GetEvDetail(e);
var evDetail = GetEvDetail(e); var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail;
var data = 'post_nr=' + encodeURIComponent(evDetail.postId) + '&board=' + encodeURIComponent(board_id) + '&regions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version);
MakeRequest(method, url, data, func); //ignore if 404 event
}, false); if (evDetail[404] === true) {
return;
/** Listen to post updates from the thread updater for 4chan x v2 (loadletter) and v3 (ccd0 + ?) */ }
document.addEventListener('ThreadUpdate', function (e) {
var evDetail = GetEvDetail(e);
var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail;
//ignore if 404 event
if (evDetail[404] === true) {
return;
}
evDetailClone.newPosts.forEach(function (post_board_nr) { evDetailClone.newPosts.forEach(function (post_board_nr) {
var post_nr = post_board_nr.split('.')[1]; var post_nr = post_board_nr.split('.')[1];
postNrs.push(post_nr); postNrs.push(post_nr);
}); });
resolveRefFlags(); resolveRefFlags();
}, false); }, false);
/** Listen to post updates from the thread updater for inline extension */ document.addEventListener('4chanThreadUpdated', function (e) {
document.addEventListener('4chanThreadUpdated', function (e) { var evDetail = GetEvDetail(e);
var evDetail = GetEvDetail(e); let threadID = window.location.pathname.split('/')[3];
let threadID = window.location.pathname.split('/')[3]; let postsContainer = Array.prototype.slice.call(document.getElementById('t' + threadID).childNodes);
let postsContainer = sliceCall(document.getElementById('t' + threadID).childNodes); let lastPosts = postsContainer.slice(Math.max(postsContainer.length - evDetail.count, 1)); //get the last n elements (where n is evDetail.count)
let lastPosts = postsContainer.slice(Math.max(postsContainer.length - evDetail.count, 1)); //get the last n elements (where n is evDetail.count)
//add to temp posts and the DOM element to allPostsOnPage lastPosts.forEach(function (post_container) {
lastPosts.forEach(function (post_container) { var post_nr = post_container.id.replace('pc', '');
var post_nr = post_container.id.replace('pc', ''); postNrs.push(post_nr);
postNrs.push(post_nr); });
});
resolveRefFlags(); resolveRefFlags();
}, false); }, false);
nsetup.init(); nsetup.init();
} }
if (software.nodegucaDoushio) { if (software.nodegucaDoushio) {
nsetup.init(); nsetup.init();
// This is poking at the mutations made on the page to figure out what happened and thus what actions to take. // This is poking at the mutations made on the page to figure out what happened and thus what actions to take.
// There is full support for nodeguca but I don't have a Doushio board I feel comfortable spamming to ensure it works properly there. There is at least partial support. // There is full support for nodeguca but I don't have a Doushio board I feel comfortable spamming to ensure it works properly there. There is at least partial support.
new MutationObserver(function (mutations) { new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) { mutations.forEach(function (mutation) {
if (mutation.addedNodes.length > 0) { // A post was added. if (mutation.addedNodes.length > 0) { // A post was added.
var firstAddedNode = mutation.addedNodes[0].nodeName; var firstAddedNode = mutation.addedNodes[0].nodeName;
// Enter a thread or change boards. // Enter a thread or change boards. Checks for when a post is added while in the index.
if (mutation.target.nodeName === 'THREADS' && firstAddedNode !== 'HR' && firstAddedNode !== 'SECTION') { if (mutation.target.nodeName === 'THREADS' && firstAddedNode !== 'HR' && firstAddedNode !== 'SECTION') {
board_id = window.location.pathname.split('/')[1]; board_id = window.location.pathname.split('/')[1];
setTimeout(getPosts('section[id], article[id]'), 2000); setTimeout(getPosts('section[id], article[id]'), 2000);
resolveRefFlags(); resolveRefFlags();
nsetup.init(); nsetup.init();
} }
if (firstAddedNode === 'HEADER') { // You post. // You post.
let data = 'post_nr=' + encodeURIComponent(mutation.target.id) + '&board=' + encodeURIComponent(board_id) + '&regions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version); if (firstAddedNode === 'HEADER') {
MakeRequest( let data = 'post_nr=' + encodeURIComponent(mutation.target.id) + '&board=' + encodeURIComponent(board_id) + '&regions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version);
'POST', MakeRequest(
back_end + api_post, 'POST',
data, back_end + api_post,
function () { data,
postNrs.push(mutation.target.id); function () {
resolveRefFlags(); postNrs.push(mutation.target.id);
}); resolveRefFlags();
} });
}
// Someone else posts.
if (firstAddedNode === 'ARTICLE' && mutation.target.nodeName !== "BODY" && mutation.target.id !== 'hover_overlay') { // Someone else posts. Checks to see if you're hovering over a post.
postNrs.push(mutation.addedNodes[0].id); if (firstAddedNode === 'ARTICLE' && mutation.target.nodeName !== "BODY" && mutation.target.id !== 'hover_overlay') {
setTimeout(resolveRefFlags, 1500); postNrs.push(mutation.addedNodes[0].id);
} setTimeout(resolveRefFlags, 1500);
} }
}); }
}).observe(document.body, { childList: true, subtree: true }); });
}).observe(document.body, { childList: true, subtree: true });
} }
Loading…
Cancel
Save