You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bantflags/bantflags.user.js

417 lines
16 KiB

5 years ago
// ==UserScript==
// @name /bant/ Flags
5 years ago
// @namespace BintFlegs
// @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/*
5 years ago
// @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.4.0
5 years ago
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// @icon https://flags.plum.moe/flags/0077.png
5 years ago
// @updateURL https://flags.plum.moe/bantflags.meta.js
// @downloadURL https://flags.plum.moe/bantflags.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 = false;
5 years ago
// 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;
}
5 years ago
//
// DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE FLAG SELECT
5 years ago
//
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/';
5 years ago
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;
5 years ago
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
};
5 years ago
/** 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);
5 years ago
/** 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
}));
5 years ago
/** Write extra information to the console if debugMode is set to true.
* @param {string} text - The text to write to the console. */
5 years ago
function debug(text) {
if (debugMode) {
console.log('[BantFlags] ' + text);
}
5 years ago
}
/** Wrapper around GM_xmlhttpRequest.
* @param {string} method - The HTTP method (GET, POST).
* @param {string} url - The URL of the request.
* @param {string} data - text for 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.*/
5 years ago
function retry(func, resp) {
console.log('[BantFlags] Could not fetch flags, status: ' + resp.status);
console.log(resp.statusText);
setTimeout(func, requestRetryInterval);
5 years ago
}
// TODO: this shouldn't be a object.
5 years ago
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><div id="flagSelect" ><ul class="hide"></ul><input type="button" value="(You)" onclick=""></div>',
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;
}
5 years ago
let flagSelect = document.getElementById('flagSelect');
let flagList = flagSelect.querySelector('ul');
let flagInput = flagSelect.querySelector('input');
let flags = resp.responseText.split('\n');
5 years ago
for (var i = 0; i < flags.length; i++) {
let flag = flags[i];
flagList.appendChild(createAndAssign('li', {
innerHTML: '<img src="' + back_end + flag_dir + flag + '.png" title="' + flag + '"> <span>' + flag + '</span>'
}));
5 years ago
}
flagSelect.addEventListener('click', (e) => {
listItem = e.target.nodeName === 'LI' ? e.target : e.target.parentNode;
if (listItem.nodeName === 'LI') {
flagInput.value = listItem.querySelector('span').innerHTML;
}
flagList.classList.toggle('hide');
});
document.getElementById('flagLoad').style.display = 'none';
document.querySelector('.flagsForm').style.marginRight = "200px"; // Element has position: absolute and is ~200px long.
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;
}
5 years ago
GM_setValue(nsetup.namespace, storedFlags);
regions = GM_getValue(nsetup.namespace); // TODO: this could be selectedFlags? GM_getValue is expensive.
},
setFlag: function (flag) {
let UID = Math.random().toString(36).substring(7);
let flagName = flag ? flag : document.querySelector('#flagSelect input').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'
}));
5 years ago
if (flagContainer.children.length >= max_flags) {
nsetup.toggleFlagButton('off');
}
5 years ago
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
});
5 years ago
// 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?
5 years ago
for (var i in regions) {
nsetup.setFlag(regions[i]);
}
5 years ago
document.getElementById('append_flag_button').addEventListener('click',
() => nsetup.flagsLoaded ? nsetup.setFlag() : alert('Load flags before adding them.'));
5 years ago
document.getElementById('flagLoad').addEventListener('click', nsetup.fillHtml, { once: true });
},
toggleFlagButton: state => document.getElementById('append_flag_button').disabled = state === 'off' ? true : false
};
5 years ago
/** add all of thhe post numbers on the page to postNrs.
* @param {string} selector - The CSS selector who's id is the post number. */
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);
5 years ago
}
/** Take the response from resolveRefFlags and append flags to their respective post numbers.
* @param {XMLHttpRequest} response - The response data from resolveRefFlags. */
5 years ago
function onFlagsLoad(response) {
debug('JSON: ' + response.responseText);
var jsonData = JSON.parse(response.responseText);
5 years ago
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'); }
5 years ago
let flags = jsonData[post];
if (flags.length > 0) {
console.log('[BantFlags] Resolving flags for >>' + post);
5 years ago
for (var i = 0; i < flags.length; i++) {
let flag = flags[i];
5 years ago
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;
}
5 years ago
flagContainer.append(newFlag);
5 years ago
console.log('\t -> ' + flag);
}
}
});
5 years ago
postNrs = [];
5 years ago
}
/** Get flags from the database using values in postNrs and pass the response on to onFlagsLoad */
5 years ago
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);
}
);
5 years ago
}
// This should only happen before you set flags for the first time.
regions = GM_getValue(nsetup.namespace);
if (!regions) {
regions = [];
window.confirm('[BantFlags]: No Flags detected.');
}
// See Docs/styles.css
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;}} #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;}');
// We get flags using different selectors, and we need to align them differently.
if (software.yotsuba) {
debug('4chan');
board_id = 'bant';
getPosts('.postContainer');
5 years ago
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;}');
5 years ago
}
if (software.foolfuuka) {
debug('FoolFuuka');
board_id = 'bant';
getPosts('article[id]');
addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}');
}
5 years ago
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;
}
5 years ago
evDetailClone.newPosts.forEach(function (post_board_nr) {
var post_nr = post_board_nr.split('.')[1];
postNrs.push(post_nr);
});
5 years ago
resolveRefFlags();
}, false);
5 years ago
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)
5 years ago
lastPosts.forEach(function (post_container) {
var post_nr = post_container.id.replace('pc', '');
postNrs.push(post_nr);
});
5 years ago
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 });
5 years ago
}