diff --git a/BantFlags/Controllers/FlagsController.cs b/BantFlags/Controllers/FlagsController.cs index a64d162..00af538 100644 --- a/BantFlags/Controllers/FlagsController.cs +++ b/BantFlags/Controllers/FlagsController.cs @@ -35,12 +35,10 @@ namespace BantFlags.Controllers if (ver > 1) { // Improved data structuring, see Docs/GetPosts - return Json(await Database.GetPosts_V2(post_nrs)); - } - else - { - return Json(await Database.GetPosts_V1(post_nrs)); + return Json(await Database.GetPosts_V2(post_nrs, board)); } + + return Json(await Database.GetPosts_V1(post_nrs, board)); } /// diff --git a/BantFlags/Data/Database/DatabaseService.cs b/BantFlags/Data/Database/DatabaseService.cs index 31e65a2..be886a5 100644 --- a/BantFlags/Data/Database/DatabaseService.cs +++ b/BantFlags/Data/Database/DatabaseService.cs @@ -26,7 +26,7 @@ namespace BantFlags.Data.Database public async Task UpdateKnownFlags() { var flags = await GetFlags(); - flags.Remove("empty, or there were errors. Re-set your flags."); + flags.Remove("empty, or there were errors. Re-set your flags."); // So users can't select this. FlagList = string.Join("\n", flags); KnownFlags = flags.ToHashSet(); diff --git a/BantFlags/Data/Database/GetPosts.cs b/BantFlags/Data/Database/GetPosts.cs index c523b8b..5fdbc0d 100644 --- a/BantFlags/Data/Database/GetPosts.cs +++ b/BantFlags/Data/Database/GetPosts.cs @@ -8,29 +8,30 @@ namespace BantFlags.Data.Database public partial class DatabaseService { // Maybe this could be better but I don't know SQL lol - private readonly string GetPostsQuery = @"SELECT posts.post_nr, flags.flag FROM flags LEFT JOIN (postflags) ON (postflags.flag = flags.id) LEFT JOIN (posts) ON (postflags.post_nr = posts.id) WHERE FIND_IN_SET(posts.post_nr, (@posts))"; + private readonly string GetPostsQuery = @"SELECT posts.post_nr, flags.flag FROM flags LEFT JOIN (postflags) ON (postflags.flag = flags.id) LEFT JOIN (posts) ON (postflags.post_nr = posts.id) WHERE FIND_IN_SET(posts.post_nr, (@posts)) AND posts.board = @board"; /// /// Returns the post numbers and their flags from the post numbers in the input. /// - /// List of post numbers on the page. - public async Task>> GetPosts(string input) + /// List of post numbers on the page. + public async Task>> GetPosts(string post_nr, string board) { using var rentedConnection = await ConnectionPool.RentConnectionAsync(); DataTable table = await rentedConnection.Object.CreateQuery(GetPostsQuery) - .SetParam("@posts", input) + .SetParam("@posts", post_nr) + .SetParam("@board", board) .ExecuteTableAsync(); return table.AsEnumerable() .GroupBy(x => x.GetValue("post_nr")); } - public async Task>> GetPosts_V1(string input) + public async Task>> GetPosts_V1(string post_nr, string board) { List> posts = new List>(); - var x = await GetPosts(input); + var x = await GetPosts(post_nr, board); x.ForEach(x => posts.Add(new Dictionary { {"post_nr", x.Key.ToString() }, @@ -40,9 +41,9 @@ namespace BantFlags.Data.Database return posts; } - public async Task>> GetPosts_V2(string input) + public async Task>> GetPosts_V2(string post_nr, string board) { - var posts = await GetPosts(input); + var posts = await GetPosts(post_nr, board); return posts .ToDictionary( x => x.Key, diff --git a/BantFlags/Data/PostModel.cs b/BantFlags/Data/PostModel.cs index 6484347..45764ed 100644 --- a/BantFlags/Data/PostModel.cs +++ b/BantFlags/Data/PostModel.cs @@ -21,12 +21,13 @@ namespace BantFlags.Data public static Result Create(string post_nr, string board, string regions, string splitFlag, HashSet knownFlags) { - string[] empty = new string[] { "empty, or there were errors. Re-set your flags." }; + string[] empty = { "empty, or there were errors. Re-set your flags." }; + string[] boards = { "bant", "nap", "srsbsn" }; // TODO: Move this to appsettings and make a singleton for it. if (!int.TryParse(post_nr, out int postNumber)) return Result.Fail("Invalid post number."); - if (board != "bant") + if (!boards.Contains(board)) return Result.Fail("Invalid board parameter."); if (regions == null) diff --git a/BantFlags/Data/Staging.cs b/BantFlags/Data/Staging.cs index 4fae4cb..8ee8f69 100644 --- a/BantFlags/Data/Staging.cs +++ b/BantFlags/Data/Staging.cs @@ -46,6 +46,7 @@ namespace BantFlags.Data public Method FlagMethod { get; set; } + // This is bad but we need it so Flags can be generated by the input tag helper public Flag() { } @@ -78,7 +79,7 @@ namespace BantFlags.Data public static async Task> CreateFromFile(IFormFile upload, HashSet names) { - byte[] PNGHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + byte[] PNGHeader = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; if (upload.ContentType.ToLower() != "image/png") return Result.Fail("Image must be a png."); @@ -116,6 +117,11 @@ namespace BantFlags.Data return Result.Pass(new Flag(name, Method.Add)); } + /// + /// Filters file names created by users. + /// + /// The file name to validate. + /// The list of current file names. private static Result ValidateFileName(string name, HashSet names) { if (string.IsNullOrWhiteSpace(name)) diff --git a/BantFlags/Pages/Upload.cshtml.cs b/BantFlags/Pages/Upload.cshtml.cs index a806aff..b0ef0a5 100644 --- a/BantFlags/Pages/Upload.cshtml.cs +++ b/BantFlags/Pages/Upload.cshtml.cs @@ -165,10 +165,12 @@ namespace BantFlags case Method.Delete: await Database.DeleteFlagAsync(flag); + if (System.IO.File.Exists(WebRoot + "/flags/dead/" + flagname)) { - System.IO.File.Delete(WebRoot + "/flags/dead/" + flagname); + System.IO.File.Delete(WebRoot + "/flags/dead/" + flagname); // TODO: This is not the right way to handle it. } + Directory.Move(WebRoot + "/flags/" + flagname, WebRoot + "/flags/dead/" + flagname); break; diff --git a/bantflags.user.js b/bantflags.user.js index fe3f670..4f0dcbf 100644 --- a/bantflags.user.js +++ b/bantflags.user.js @@ -6,6 +6,7 @@ // @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/ @@ -29,10 +30,7 @@ const debugMode = false; // DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE CONFIGURATION BOXES // const postRemoveCounter = 60; -const requestRetryInterval = 5000; // TODO: maybe a max retries counter? -const regionDivider = "||"; //TODO: We can probably remove this and seperate by , -const is_archive = window.location.host !== "boards.4chan.org"; -const boardID = "bant"; //TODO: Hardcode /bant/ or accept other boards. +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'; @@ -45,14 +43,21 @@ 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. + +const site = { + fourchan: window.location.host === 'boards.4chan.org', + nineball: window.location.host === 'nineball.party' +}; + +// There are multiple foolfuuka archives we support; this has to be called after site is initialised as to not check each of them. +// I.E. we'd have to go window.location.host === 'archive.nyafuu.org' || window.location.host === 'archived.moe' +site.foolfuuka = !site.fourchan && !site.nineball; // // DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR FLAGS SHOULD BE CONFIGURED USING THE CONFIGURATION BOXES // - -const elementsInClass = x => document.getElementsByClassName(x); const sliceCall = x => Array.prototype.slice.call(x); -const firstChildInClass = (parent, className) => parent.getElementsByClassName(className)[0]; const createAndAssign = (element, source) => Object.assign(document.createElement(element), source); function addGlobalStyle(css) { @@ -70,7 +75,7 @@ function addGlobalStyle(css) { function debug(text) { if (debugMode) { - console.log("[BantFlags] " + text); + console.log('[BantFlags] ' + text); } } @@ -85,27 +90,25 @@ function MakeRequest(method, url, data, func) { url: url, data: data, headers: { - "Content-Type": "application/x-www-form-urlencoded" + "Content-Type": 'application/x-www-form-urlencoded' }, onload: func }); } 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); setTimeout(func, requestRetryInterval); } /** nSetup, preferences */ -// TODO: this shouldn't be a class. +// 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: "" + - "" + - "", + form: '', fillHtml: function () { // TODO: this function should have a better name. Only called by nsetup.init, can be inlined? // resolve flags @@ -120,7 +123,7 @@ var nsetup = { // not anymore a clone of the original setup return; } - let flagSelect = document.getElementById("flagSelect"); + let flagSelect = document.getElementById('flagSelect'); let flagLoad = document.getElementById('flagLoad'); let flagsSupported = resp.responseText.split('\n'); @@ -128,7 +131,7 @@ var nsetup = { // not anymore a clone of the original setup let flag = flagsSupported[i]; flagSelect.appendChild(createAndAssign('option', { value: flag, - innerHTML: "" + " " + flag + innerHTML: ' ' + flag })); } @@ -143,12 +146,12 @@ var nsetup = { // not anymore a clone of the original setup }, 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"); + 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", + src: back_end + flag_dir + flagName + '.png', id: UID, className: 'bantflags_flag' })); @@ -158,13 +161,11 @@ var nsetup = { // not anymore a clone of the original setup } document.getElementById(UID).addEventListener("click", function () { - console.log("removing flag"); let flagToRemove = document.getElementById(UID); flagToRemove.parentNode.removeChild(flagToRemove); nsetup.toggleFlagButton('on'); nsetup.save(nsetup.parse()); - console.log("flag removed"); }); if (!flag) { @@ -175,15 +176,22 @@ var nsetup = { // not anymore a clone of the original setup init: function () { // here we insert the form for placing flags. How? - let flagsForm = createAndAssign("div", { + let flagsForm = createAndAssign('div', { className: 'flagsForm', innerHTML: nsetup.form }); addGlobalStyle('.flagsForm{float: right; clear: right; margin: 20px 10px;} #flagSelect{display:none;}'); - 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;}}"); + 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;}}'); - firstChildInClass(document, 'bottomCtrl').parentNode.appendChild(flagsForm); + if (site.fourchan) { + document.getElementById('delform').appendChild(flagsForm); + } + + // nineball and we're not in the index + if (site.nineball && document.querySelector('threads .pagination') === null) { + document.querySelector('threads section').append(flagsForm); + } for (var i in regions) { nsetup.setFlag(regions[i]); @@ -196,7 +204,7 @@ var nsetup = { // not anymore a clone of the original setup }, parse: function () { let flagsArray = []; - let flagElements = elementsInClass("bantflags_flag"); + let flagElements = document.getElementsByClassName("bantflags_flag"); for (var i = 0; i < flagElements.length; i++) { flagsArray[i] = flagElements[i].title; @@ -212,68 +220,64 @@ regions = GM_getValue(nsetup.namespace); // TODO: move this to other init stuff if (!regions) { regions = []; setTimeout(function () { - window.confirm("Bant Flags: No Flags detected"); + window.confirm('Bant Flags: No Flags detected'); }, 2000); } /** parse the posts already on the page before thread updater kicks in */ function parse4chanPosts() { - let posts = sliceCall(elementsInClass('postContainer')); - + let posts = document.querySelectorAll('.postContainer'); for (var i = 0; i < posts.length; i++) { - let postNumber = posts[i].id.replace("pc", ""); + let postNumber = posts[i].id.replace('pc', ''); // Fuck you 4chan postNrs.push(postNumber); } debug(postNrs); } -function parseFoolFuukaPosts() { - let nums = x => x.filter(x => x.id !== '').map(x => x.id); - let getPostNumbers = x => nums(sliceCall(elementsInClass(x))); +function getposts(selector) { + let posts = document.querySelectorAll(selector); - postNrs = getPostNumbers('thread').concat(getPostNumbers('post')); + for (var i = 0; i < posts.length; i++) { + postNrs.push(posts[i].id); + } debug(postNrs); } function onFlagsLoad(response) { - - // because we only care about the end result, not how we got there. - // grandparent -> first parent -> first child. - let hopHTML = (post_nr, first, second) => - firstChildInClass(firstChildInClass(document.getElementById(post_nr), first), second); - let MakeFlag = (flag) => createAndAssign('a', { - innerHTML: "", - className: "bantFlag", - target: "_blank" + innerHTML: ' ', + className: 'bantFlag', + target: '_blank' }); - debug("JSON: " + response.responseText); + debug('JSON: ' + response.responseText); var jsonData = JSON.parse(response.responseText); Object.keys(jsonData).forEach(function (post) { - let flagContainer = is_archive - ? hopHTML(post, "post_data", "post_type") - : hopHTML("pc" + post, "postInfo", "nameBlock"); - let currentFlag = firstChildInClass(flagContainer, 'flag'); - let flags = jsonData[post]; + var flagContainer; + if (site.nineball) { flagContainer = document.querySelector('[id="' + post + '"] header'); } + if (site.fourchan) { flagContainer = document.querySelector('[id="pc' + post + '"] .postInfo .nameBlock'); } + if (site.foolfuuka) { flagContainer = document.querySelector('[id="' + post + '"] .post_data .post_type'); } - // If we have a bantflag and the original post has a flag - if (flags.length > 0 && currentFlag !== undefined) { - console.log("[BantFlags] Resolving flags for >>" + post); + 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 = MakeFlag(flag); - if (is_archive) { - newFlag.style = "padding: 0px 0px 0px " + (3 + 2 * (i > 0)) + "px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;"; + if (site.foolfuuka) { + newFlag.style = 'padding: 0px 0px 0px ' + (3 + 2 * (i > 0)) + 'px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;'; + } + if (site.nineball) { + newFlag.title = flag; } flagContainer.append(newFlag); - console.log("\t -> " + flag); + console.log('\t -> ' + flag); } } }); @@ -281,11 +285,13 @@ function onFlagsLoad(response) { postNrs = []; } +/** Gets flags from the database. */ function resolveRefFlags() { + debug('Board is: ' + board_id); MakeRequest( - "POST", + 'POST', back_end + api_get, - "post_nrs=" + encodeURIComponent(postNrs) + "&board=" + encodeURIComponent(boardID) + "&version=" + encodeURIComponent(version), + 'post_nrs=' + encodeURIComponent(postNrs) + '&board=' + encodeURIComponent(board_id) + '&version=' + encodeURIComponent(version), function (resp) { if (resp.status !== 200) { retry(resolveRefFlags, resp); @@ -297,26 +303,39 @@ function resolveRefFlags() { } // Flags need to be parsed and aligned differently between boards. -if (is_archive) { - debug("FoolFuuka."); - parseFoolFuukaPosts(); - - addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}'); -} -else { - debug("4chan."); +if (site.fourchan) { + debug('4chan'); + board_id = 'bant'; parse4chanPosts(); addGlobalStyle('.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;}"); + addGlobalStyle('.bantFlag {padding: 0px 0px 0px 5px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;}'); +} + +if (site.nineball) { + debug(regions); + 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;}'); } -resolveRefFlags(); // Get flags from db. +if (site.foolfuuka) { // Archive. + debug('FoolFuuka'); + board_id = 'bant'; + getposts('article[id]'); + + addGlobalStyle('.bantFlag{top: -2px !important;left: -1px !important}'); +} -if (!is_archive) { +resolveRefFlags(); // Get flags from DB. + +// Posting new flags and getting flags as posts are added to the thread. +if (site.fourchan) { let GetEvDetail = e => e.detail || e.wrappedJSObject.detail; - let method = "POST", + let method = 'POST', url = back_end + api_post, func = function (resp) { debug(resp.responseText); @@ -332,7 +351,7 @@ if (!is_archive) { //setTimeout to support greasemonkey 1.x setTimeout(function () { - var data = "post_nr=" + encodeURIComponent(e.detail.postID) + "&board=" + encodeURIComponent(e.detail.boardID) + "®ions=" + encodeURIComponent(regions) + "&version=" + encodeURIComponent(version); + var data = 'post_nr=' + encodeURIComponent(e.detail.postID) + '&board=' + encodeURIComponent(e.detail.boardID) + '®ions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version); MakeRequest(method, url, data, func); }, 0); }, false); @@ -343,7 +362,7 @@ if (!is_archive) { //setTimeout to support greasemonkey 1.x setTimeout(function () { - var data = "post_nr=" + encodeURIComponent(evDetail.postId) + "&board=" + encodeURIComponent(boardID) + "®ions=" + encodeURIComponent(regions) + "&version=" + encodeURIComponent(version); + var data = 'post_nr=' + encodeURIComponent(evDetail.postId) + '&board=' + encodeURIComponent(board_id) + '®ions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version); MakeRequest(method, url, data, func); }, 0); }, false); @@ -380,7 +399,7 @@ if (!is_archive) { //add to temp posts and the DOM element to allPostsOnPage 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); }); @@ -389,4 +408,27 @@ if (!is_archive) { }, false); /** setup init and start first calls */ nsetup.init(); +} + +if (site.nineball) { + nsetup.init(); + new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.addedNodes[0].nodeName == 'HEADER') { // When you make a post + let data = 'post_nr=' + encodeURIComponent(mutation.target.id) + '&board=' + encodeURIComponent(board_id) + '®ions=' + encodeURIComponent(regions) + '&version=' + encodeURIComponent(version); + MakeRequest( + 'POST', + back_end + api_post, + data, + function (resp) { + postNrs.push(mutation.target.id); + setTimeout(resolveRefFlags, 0); + }); + } + if (mutation.addedNodes[0].nodeName == 'ARTICLE') { // When someone else makes a post + postNrs.push(mutation.addedNodes[0].id); + setTimeout(resolveRefFlags, 1500); // Wait 1.5s so the database can process the post, since they appear instantly. + } + }); + }).observe(document.querySelector('threads'), { childList: true, subtree: true }); } \ No newline at end of file