您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
make Linkomanija great again
// ==UserScript== // @name BetterLM // @namespace https://blm.hades.lt // @version 1.8.4 // @description make Linkomanija great again // @author Krupp // @match https://www.linkomanija.net/* // @grant none // ==/UserScript== //--- utils.js class Utils { static InsertAfter(element, newNode) { return element.parentElement.insertBefore(newNode, element.nextSibling); } static InsertBefore(element, newNode) { return element.parentElement.insertBefore(newNode, element.previousSibling); } static GetSelectedValues(element) { let res = []; if (element.options) { for (let i = 0; i < element.options.length; i++) { let opt = element.options[i]; if (opt.selected) res.push(opt.value); } } return res; } static async FetchJSON(url, method = 'GET', data = null) { let reqObj = {method}; if (data) { reqObj.body = JSON.stringify(data); reqObj.headers = {'content-type': 'application/json'}; } return await fetch(Settings.ApiUrl + url, reqObj).then(resp => resp.json()); } static PrintDate(dateStr) { if (!dateStr) return ''; // do not convert to UTC dateStr = dateStr.replace('T', ' '); let date = new Date(dateStr); return `${date.getFullYear()}-${Utils._WZero(date.getMonth() + 1)}-${Utils._WZero(date.getDate())} ${Utils._WZero(date.getHours())}:${Utils._WZero(date.getMinutes())}:${Utils._WZero(date.getSeconds())}`; } // with zero, so it outputs 09 minutes instead of 9 -.- static _WZero(inp) { return inp < 10 ? '0' + inp : '' + inp; // so that it always returns String, not Number sometimes } // Null or Undefined static NU(obj) { return obj === null || obj === undefined; } static GetPosts() { return document.querySelectorAll('div[id^="post_"]'); } static GetPostId(postEl) { if (!postEl) return null; let postIdAttr = postEl.getAttribute('id'); if (postIdAttr) { postIdAttr = postIdAttr.replace('post_', ''); let postId = Number(postIdAttr); if (Number.isNaN(postId)) return null; else return postId; } return null; } static get _postDateRegex() { return /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/; } static GetPostDate(postEl) { let infoTextEl = postEl.querySelector('span:first-child'); if (infoTextEl) { let regexResult = Utils._postDateRegex.exec(infoTextEl.textContent); if (regexResult) return new Date(regexResult[0]); } return null; } } //--- options.js class Options { constructor(obj = null) { if (obj) { this.notifyOnUserMention = obj.notifyOnUserMention; this.showLastPosts = obj.showLastPosts; this.ignoreHideTopics = obj.ignoreHideTopics; this.ignoreReplaceMessages = obj.ignoreReplaceMessages; this.ignoreReplaceString = obj.ignoreReplaceString; } } validate() { if (Utils.NU(this.notifyOnUserMention) || Utils.NU(this.showLastPosts) || Utils.NU(this.ignoreReplaceMessages) || Utils.NU(this.ignoreHideTopics)) throw "options aren't valid"; } setDefaults() { this.notifyOnUserMention = true; this.showLastPosts = false; this.ignoreHideTopics = false; this.ignoreReplaceMessages = true; this.ignoreReplaceString = 'Vartotojas ignoruotas.'; } } //--- settings.js class Settings { static get KeyOptions() { return 'blm_options'; } static get KeyIgnored() { // for backwards compatibility with 'LMRetard' script return 'retards'; } static get ApiUrl() { return 'https://blm.hades.lt'; } static Instance() { if (this._instance) return this._instance; this._instance = new Settings(); this._instance._load(); return this._instance; } save() { let optionsJson = JSON.stringify(this.options); localStorage.setItem(Settings.KeyOptions, optionsJson); let ignoredJson = JSON.stringify(this.ignored); localStorage.setItem(Settings.KeyIgnored, ignoredJson); } _load() { // load options try { let optionsJson = localStorage.getItem(Settings.KeyOptions); let options = JSON.parse(optionsJson); this.options = new Options(options); this.options.validate(); } catch (ex) { // oh well... this._resetOptions(); this.save(); } // load ignored users try { let ignoredJson = localStorage.getItem(Settings.KeyIgnored); let ignored = JSON.parse(ignoredJson); if (!Array.isArray(ignored)) throw 'do not swear'; ignored.forEach(x => { if (typeof x !== 'string') throw 'fuck, I told a swear word'; }); this.ignored = ignored; } catch (ex) { // well shitballs... this._resetIgnored(); this.save(); } // set own name let username = document.querySelector('#username'); if (username) this.ownName = username.innerText; // set own id let userHref = username.querySelector('a'); if (userHref) this.ownUserId = Number(userHref.href.split('?id=')[1]); // 99% of the time never throws } isIgnored(name) { if (name === this.ownName) return false; return this.ignored.indexOf(name) !== -1; } addIgnored(name) { if (name === this.ownName) return; if (!this.isIgnored(name)) { this.ignored.push(name); this.save(); } else { // todo: something better than alert would be nice alert(`${name} is already ignored, refresh the page.`); } } getIgnored() { return this.ignored; } removeIgnored(name, save = true) { let idx = this.ignored.indexOf(name); if (idx !== -1) { this.ignored.splice(idx, 1); if (save) this.save(); } } _resetOptions() { this.options = new Options(); this.options.setDefaults(); } _resetIgnored() { this.ignored = []; } } //--- base.js class Base { constructor() { this.settings = Settings.Instance(); } } //--- templates/templateEngine.js // "Holy Mashed Potatoes, Batman!" -Robin // update: fuck this shit, shoulda used handlebars class TemplateEngine { static get _ForeachEnd() { return '@{/foreach}'; } static get _ForeachStart() { return /@{foreach x in (.*)}/; } static get _IfStart() { return /@{if\((.*)\)}/; } static get _IfEnd() { return '@{/if}'; } static get _ExecStart() { return '@{exec}'; } static get _ExecEnd() { return '@{/exec}'; } static get _ForStart() { return /@{for\((.*)\)}/; } static get _ForEnd() { return '@{/for}'; } static get Template() { throw 'must override Template'; } static Render(model, html = this.Template, x = null) { let exprArr; while ((exprArr = /@{.*?}/.exec(html)) !== null) { let exprRaw = exprArr[0]; let expr = exprRaw.replace(/(^@{)|(}$)/g, ''); // code is repeating itself a lot QQ // todo: refactor (or fucking use handlebars for reals) // handle FOREACHs if (TemplateEngine._ForeachStart.test(exprRaw)) { let foreachExpArr = TemplateEngine._ForeachStart.exec(exprRaw); let endIdx = html.indexOf(TemplateEngine._ForeachEnd, foreachExpArr.index); let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ForeachEnd.length); let template = templateRaw.substr(foreachExpArr[0].length, templateRaw.length - TemplateEngine._ForeachEnd.length - foreachExpArr[0].length); let expressedTemplate = TemplateEngine._ApplyForeach(template, model, foreachExpArr[1]); html = html.replace(templateRaw, expressedTemplate); } // handle IFs else if (TemplateEngine._IfStart.test(exprRaw)) { let ifExprArr = TemplateEngine._IfStart.exec(exprRaw); let endIdx = html.indexOf(TemplateEngine._IfEnd, ifExprArr.index); let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._IfEnd.length); let template = templateRaw.substr(ifExprArr[0].length, templateRaw.length - TemplateEngine._IfEnd.length - ifExprArr[0].length); let expressedTemplate = TemplateEngine._ApplyIf(template, model, ifExprArr[1]); html = html.replace(templateRaw, expressedTemplate); } // handle exec else if (exprRaw === TemplateEngine._ExecStart) { let endIdx = html.indexOf(TemplateEngine._ExecEnd, exprArr.index); let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ExecEnd.length); let template = templateRaw.substr(TemplateEngine._ExecStart.length, templateRaw.length - TemplateEngine._ExecEnd.length - TemplateEngine._ExecStart.length); let expressedTemplate = TemplateEngine._ApplyExec(template, model); html = html.replace(templateRaw, expressedTemplate); } // handle FORs else if (TemplateEngine._ForStart.test(exprRaw)) { let forExprArr = TemplateEngine._ForStart.exec(exprRaw); let endIdx = html.indexOf(TemplateEngine._ForEnd, forExprArr.index); let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ForEnd.length); let template = templateRaw.substr(forExprArr[0].length, templateRaw.length - TemplateEngine._ForEnd.length - forExprArr[0].length); let expressedTemplate = TemplateEngine._ApplyFor(template, model, x, forExprArr[1]); html = html.replace(templateRaw, expressedTemplate); } // handle the stuff else { let expressed; try { expressed = eval(expr); } catch (ex) { expressed = ex; } html = html.replace(exprRaw, expressed); } } return html; } // does not support attributes with spaces in them, huehuehue static RenderElement(model, html = this.Template) { html = TemplateEngine.Render(model, html); // it's a Kirby! let rootElTagRes = /<(.*)>/.exec(html); let split = rootElTagRes[1].split(' '); // strip out parent el tags, replace replace replace REPLACE html = html.replace(rootElTagRes[0], '').replace(rootElTagRes[0].replace('<', '</'), ''); let element = document.createElement(split[0]); // set attributes for (let i = 1; i < split.length; i++) { let attr = split[i].split('='); if (attr.length === 2) element.setAttribute(attr[0], attr[1].replace(/"/g, '')); } element.innerHTML = html; return element; } // does not handle foreach inside foreach, huehuehue // also foreach x syntax is set in stone, cannot override x, huehuehue static _ApplyForeach(template, model, forEachStr) { let html = ''; eval(forEachStr).forEach((x, i) => { if (typeof x === 'object') x._INDEX = i; html += TemplateEngine.Render(null, template, x); }); return html; } // does not handle if inside if, might work with foreach though? // else is too hard, huehuehuehue (and saturation) static _ApplyIf(template, model, conditionStr) { if (!eval(conditionStr)) return ''; return TemplateEngine.Render(model, template); } static _ApplyExec(execStr, model) { eval(execStr); return ''; } static _ApplyFor(template, model, x, expr) { let html = ''; // I'll admit - JavaScript is quite fun lawl expr = `for(${expr}) { html += TemplateEngine.Render(model, template, x); }`; eval(expr); return html; } } //--- templates/loaderTemplate.js class LoaderTemplate extends TemplateEngine { static _loadStyle() { if (LoaderTemplate._styleLoaded) return; LoaderTemplate._styleLoaded = true; document.head.innerHTML += `<style> .spinner { margin: 100px auto; width: 40px; height: 40px; position: relative; } .cube1, .cube2 { background-color: royalblue; width: 15px; height: 15px; position: absolute; top: 0; left: 0; -webkit-animation: sk-cubemove 1.8s infinite ease-in-out; animation: sk-cubemove 1.8s infinite ease-in-out; } .cube2 { -webkit-animation-delay: -0.9s; animation-delay: -0.9s; } @-webkit-keyframes sk-cubemove { 25% { -webkit-transform: translateX(42px) rotate(-90deg) scale(0.5) } 50% { -webkit-transform: translateX(42px) translateY(42px) rotate(-180deg) } 75% { -webkit-transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5) } 100% { -webkit-transform: rotate(-360deg) } } @keyframes sk-cubemove { 25% { transform: translateX(42px) rotate(-90deg) scale(0.5); -webkit-transform: translateX(42px) rotate(-90deg) scale(0.5); } 50% { transform: translateX(42px) translateY(42px) rotate(-179deg); -webkit-transform: translateX(42px) translateY(42px) rotate(-179deg); } 50.1% { transform: translateX(42px) translateY(42px) rotate(-180deg); -webkit-transform: translateX(42px) translateY(42px) rotate(-180deg); } 75% { transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5); -webkit-transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5); } 100% { transform: rotate(-360deg); -webkit-transform: rotate(-360deg); } } </style>`; } // from https://github.com/tobiasahlin/SpinKit static get Template() { LoaderTemplate._loadStyle(); return ` <div class="spinner"> <div class="cube1"></div> <div class="cube2"></div> </div> `; } } LoaderTemplate._styleLoaded = false; // wow javascript, no other way to use static fields, good job! //--- templates/lastPostsTemplate.js class LastPostsTemplate extends TemplateEngine { static get Template() { return ` <h1>Paskutiniai pranešimai</h1> <table border="1" cellspacing="0" cellpadding="5" id="last-posts"> <tbody> <tr class="colhead"> <td class="tcenter">Autorius</td> <td class="tcenter">Žinutė</td> <td class="tcenter">Laikas</td> </tr> @{foreach x in model} <tr> <td><a href="userdetails.php?id=@{x.UserId}">@{x.Username}</a></td> <td class="hover last-post-content" data-post-id="@{x.Id}" data-thread-id="@{x.ThreadId}"> @{x.Content} </td> <td class="tcenter" style="min-width: 70px;">@{Utils.PrintDate(x.Date)}</td> </tr> @{/foreach} </tbody> </table> <style> .last-post-content { cursor: pointer; } </style> `; } } //--- templates/topUserTableTemplate.js class TopUserTableTemplate extends TemplateEngine { static get Template() { return ` <table class="top-table"> <tbody> <tr class="colhead"> <td class="tcenter">#</td> <td class="tcenter">Vartotojas</td> <td class="tcenter">Žinutės</td> </tr> </tbody> @{foreach x in model} <tr> <td class="tright">@{x._INDEX + 1}</td> <td><a href="/userdetails.php?id=@{x.Id}">@{x.Username}</a></td> <td class="tright">@{x.PostCount}</td> </tr> @{/foreach} </table>`; } } //--- templates/topPostersTemplate.js class TopPostersTemplate extends TemplateEngine { static get Template() { return ` <div id="top-container"> <div class="top-item"> <h2>All time</h2> @{TopUserTableTemplate.Render(model.All)} </div> <div class="top-item"> <h2>Paskutiniai metai</h2> @{TopUserTableTemplate.Render(model.Year)} </div> <div class="top-item"> <h2>Paskutinis mėnesis</h2> @{TopUserTableTemplate.Render(model.Month)} </div> </div> <style> #top-container { display: flex; justify-content: center; } .top-item { width: auto; padding: 20px; } .top-table td { padding-top: 7px; padding-bottom: 7px; padding-left: 6px; padding-right: 6px; } </style> `; } } //--- templates/userInfoTemplate.js class UserInfoTemplate extends TemplateEngine { static get Template() { return ` <tr> <td class="rowhead">Žinutės:</td> <td align="left"> ~ <a href="/userhistory.php?action=viewposts&id=@{model.Id}">@{model.PostCount}</a> </td> </tr> <tr> <td class="rowhead">Paskutinė:</td> <td align="left">@{Utils.PrintDate(model.LastPostDate)}</td> </tr> `; } // override, returns array with two tr elements static RenderElement(model) { let trs = []; let splitTemplate = this.Template.split('\n\n'); trs.push(super.RenderElement(model, splitTemplate[0])); trs.push(super.RenderElement(model, splitTemplate[1])); return trs; } } //--- templates/userPostsTemplate.js class UserPostsTemplate extends TemplateEngine { static get Template() { return ` <h1> <a href="/userdetails.php?id=@{model.UserId}"><b>@{model.Username}</b></a> postų istorija </h1> @{UserPostsPaginationTemplate.Render(model)} <table class="main" border="0" cellspacing="0" cellpadding="0"> <tbody> <tr> <td class="embedded"> <table width="98%" border="1" cellspacing="0" cellpadding="10"> <tbody> <tr> <td> @{foreach x in model.Posts} <p class="sub"></p> <table border="0" cellspacing="0" cellpadding="0"> <tbody> <tr> <td class="embedded"> @{Utils.PrintDate(x.Date)} -- <b>Tema:</b> <a href="/forums.php?action=viewtopic&topicid=@{x.ThreadId}">@{x.ThreadTitle}</a> -- <b>Posto ID: </b>#<a href="/forums.php?action=viewtopic&topicid=@{x.ThreadId}&page=p@{x.Id}#@{x.Id}">@{x.Id}</a> </td> </tr> </tbody> </table> <p></p> <table class="main" width="97%" border="1" cellspacing="0" cellpadding="5"> <tbody> <tr valign="top"> <td class="comment"> @{x.Content} </td> </tr> </tbody> </table> @{/foreach} </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> @{UserPostsPaginationTemplate.Render(model)} `; } } //--- templates/userPostsPaginationTemplate.js class UserPostsPaginationTemplate extends TemplateEngine { // fuck me static get Template() { return ` @{exec} model.totalPages = Math.ceil(model.TotalPosts / 25); model.pagination = []; if (model.Page > model.totalPages) model.Page = model.totalPages; for (let i = 0; i < model.totalPages; i++) { let lowerBound = i * 25 + 1; var upperBound = Math.min(lowerBound + 24, model.TotalPosts); model.pagination[i] = [lowerBound, upperBound]; } model.j = 0; @{/exec} <p align="center"> <!-- prev and first sticky --> @{if(model.Page === 1)} <span class="pageinactive">« Ankstesnis</span> <span class="pageinactive">1 - @{model.pagination[0][1]}</span> @{/if} @{if(model.Page !== 1)} <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.Page - 1}">« Ankstesnis</a> <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=1">1 - @{model.pagination[0][1]}</a> @{/if} <!-- render dots if needed --> @{if(model.Page > 4)} ... @{/if} <!-- render 2 previous buttons --> @{for(model.j = Math.max(model.Page - 3, 1); model.j < model.Page - 1; model.j++)} <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.j + 1}">@{model.pagination[model.j][0]} - @{model.pagination[model.j][1]}</a> @{/for} <!-- render current button --> @{exec} if (model.Page === 1) model.j = 0; @{/exec} @{if(model.j !== 0 && model.j !== model.totalPages - 1)} <span class="pageinactive">@{model.pagination[model.j][0]} - @{model.pagination[model.j][1]}</span> @{/if} <!-- render 2 next buttons --> @{for(model.j = model.j + 1; model.j < Math.min(model.totalPages - 1, model.Page + 2); model.j++)} <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.j + 1}">@{model.pagination[model.j][0]} - @{model.pagination[model.j][1]}</a> @{/for} <!-- dooooots --> @{if(model.Page < model.totalPages - 3)} ... @{/if} <!-- next and last sticky --> @{if(model.Page === model.totalPages && model.totalPages !== 1)} <span class="pageinactive">@{model.pagination[model.totalPages-1][0]} - @{model.pagination[model.totalPages - 1][1]}</span> @{/if} @{if(model.Page === model.totalPages)} <span class="pageinactive">Kitas »</span> @{/if} @{if(model.Page !== model.totalPages && model.totalPages !== 1)} <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.totalPages}">@{model.pagination[model.totalPages - 1][0]} - @{model.pagination[model.totalPages - 1][1]}</a> @{/if} @{if(model.Page !== model.totalPages)} <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.Page + 1}">Kitas »</a> @{/if} </p> `; } } //--- templates/showOriginalPostTemplate.js class ShowOriginalPostTemplate extends TemplateEngine { static get Template() { return `<p class="small"> <span style="cursor: pointer;">Rodyti originalų pranešimą?</span> </p>`; } static get TemplateOriginalPost() { return ` <p class="sub"> Originalus postas: </p> <table class="main" border="1" cellspacing="0" cellpadding="10"> <tbody> <tr> <td style="border: 1px black dotted">@{model.Content}</td> </tr> </tbody> </table>`; } static RenderOriginalPost(model) { return this.Render(model, this.TemplateOriginalPost); } } //--- templates/userSettingsTemplate.js class UserSettingsTemplate extends TemplateEngine { static get HeaderTemplate() { return `<h1> BetterLM nustatymai </h1>`; } static RenderHeaderElement() { return this.RenderElement(null, this.HeaderTemplate); } static get Template() { return ` <table border="1" cellspacing="0" cellpadding="10" align="center" width="100%"> <!-- I don't even this html structure --> <tbody><tr><td colspan="7"><table border="1" cellspacing="0" cellpadding="5" width="100%"><tbody> <tr> <td class="rowhead" valign="top" align="right">Rodyti paskutinius pranešimus</td> <td valign="top" align="left"> <label><input type="checkbox" id="showLastPosts">Rodyti paskutinius pranešimus</label> </td> </tr> <tr> <td class="rowhead" valign="top" align="right">Ignoravimo nustatymai</td> <td valign="top" align="left"> <label><input type="checkbox" id="hideTopics">Slėpti sukurtas temas forume</label><br> <label><input type="checkbox" id="replaceMessages">Pakeisti žinutės tekstą vietoje pašalinimo:</label><br> <input type="text" id="replaceString"> </td> </tr> <tr> <td class="rowhead" valign="top" align="right">Ignoruotieji</td> <td valign="top" align="left"> @{if(model.ignored.length > 0)} <select size="@{Math.min(12, model.ignored.length)}" multiple id="ignoredList"> @{foreach x in model.ignored} <option value="@{x}">@{x}</option> @{/foreach} </select><br/> <button id="blmRemoveIgnoredBtn" type="button">Pašalinti</button> @{/if} @{if(model.ignored.length <= 0)} <div>Nieko neignoruoji, sugyveni draugiškai</div> @{/if} </td> </tr> <tr> <td colspan="2" class="center"> <input type="button" id="blmSaveBtn" value="Patvirtinti!!1" style="height: 25px"> </td> </tr> </tbody></table></td></tr></tbody> </table> `; } } //--- templates/userMentionTemplate.js //--- modules/userIgnore.js class UserIgnore extends Base { static get _QuoteRegex() { return /\[quote=\w*\]/g; } static get _AuthorRegex() { return /(?:\[quote=)(\w*)(?:\])/; } static get _EndQuoteRegex() { return /\[\/quote\]/g; } init() { } hideTopics() { if (!this.settings.options.ignoreHideTopics) return; let tables = document.querySelectorAll('table'); if (tables.length < 1) return; let rows = tables[0].querySelectorAll('tr:not(.colhead)'); [...rows].forEach(row => { let authorLink = row.querySelector('td:nth-child(4) > a'); if (authorLink) { let author = authorLink.textContent; if (author && this.settings.isIgnored(author)) row.remove(); } }); } clearTorrentDetails() { let comments = document.querySelectorAll('#comments > div.comment'); if (!comments) return; [...comments].forEach(comment => { let authorEl = comments.querySelector('div.comment-user > a'); if (!authorEl) return; let author = authorEl.textContent; if (!author || !this.settings.isIgnored(author)) return; if (this.settings.options.ignoreReplaceMessages) { let commentText = comment.querySelector('div.comment-text'); if (commentText) commentText.textContent = this.settings.options.ignoreReplaceString; } else { comment.remove(); } }); } clearReplyQuote() { this.clearReply(); let textArea = document.querySelector('textarea#body'); if (!textArea) return; let text = textArea.value; let quotes = text.match(UserIgnore._QuoteRegex); if (!quotes) return; for (let i = 0; i < quotes.length; i++) { let quote = quotes[i]; let authorMatch = quote.match(UserIgnore._AuthorRegex); if (!authorMatch || authorMatch.length !== 2) continue; let author = authorMatch[1]; if (this.settings.isIgnored(author)) { try { let idxStart = text.indexOf(quote) + quote.length; if (idxStart === (-1 + quote.length)) throw 'mangled markup'; let idxEnd; /* [quote] and [/quote] count should equal, otherwise formatting is mangled and there's fuck I can do and the fuck I care lel */ for (let j = 0; j < quotes.length; j++) { let match = UserIgnore._EndQuoteRegex.exec(text); // black box logic go figure if (quotes.length - 1 - j !== i) continue; idxEnd = match.index; break; } let toReplace = text.substring(idxStart, idxEnd); text = text.replace(toReplace, this.settings.options.ignoreReplaceString); textArea.value = text; return; } catch (ex) { /* noop */ } } } } clearReply() { let replies = document.querySelectorAll('p.sub'); for (let i = 0; i < replies.length; i++) { let reply = replies[i]; let authorParts = reply.textContent.split(' '); if (authorParts.length < 2) return; let author = authorParts[1]; let contentEl = reply.nextElementSibling; if (this.settings.isIgnored(author)) { if (this.settings.options.ignoreReplaceMessages) { let textEl = contentEl.querySelector('tbody > tr > td:last-child'); if (textEl) textEl.textContent = this.settings.options.ignoreReplaceString; } else { contentEl.remove(); reply.remove(); } } else { this.clearQuote(contentEl); } } } clearQuote(el) { try { let quoteHeaders = el.querySelectorAll('p.sub'); for (let j = 0; j < quoteHeaders.length; j++) { let quoteHeader = quoteHeaders[j]; let quoteAuthorArr = quoteHeader.textContent.split(' '); if (!quoteAuthorArr || quoteAuthorArr.length !== 2) continue; let quoteAuthor = quoteAuthorArr[0]; if (this.settings.isIgnored(quoteAuthor)) { let quoteContent = quoteHeader.nextElementSibling.querySelector('td'); quoteContent.innerHTML = this.settings.options.ignoreReplaceString; } } } catch (ex) { /* do not expect a comment in every empty catch block */ } } clearTopic() { let posts = Utils.GetPosts(); this._clearTopic(); // todo: move to separate function, come on... for (let i = 0; i < posts.length; i++) { let post = posts[i]; let authorLink = post.querySelector('p > span > a'); if (!authorLink || !authorLink.href) continue; let author = authorLink.textContent; if (author === this.settings.ownName || !author) continue; let ignored = this.settings.isIgnored(author); // render ignore/unignore button let link = document.createElement('a'); link.textContent = ignored ? 'Nebeignoruoti' : 'Ignoruoti'; link.dataset.author = author; link.dataset.ignored = ignored; link.href = '#'; link.onclick = evt => { let link = evt.target; let author = link.dataset.author; let ignored = link.dataset.ignored; if (ignored === 'false') { if (confirm(`Ar tikrai norite ignoruoti ${author}?`)) { this.settings.addIgnored(author); location.reload(); // too much to change } } else { if (confirm(`Ar tikrai nebenorite ignoruoti ${author}?`)) { this.settings.removeIgnored(author, true); location.reload(); } } evt.preventDefault(); }; let span = post.querySelector('p > span'); let el = Utils.InsertAfter(span, document.createTextNode('[')); el = Utils.InsertAfter(el, link); Utils.InsertAfter(el, document.createTextNode('] ')); } } _clearTopic() { let posts = Utils.GetPosts(); for (let i = 0; i < posts.length; i++) { let post = posts[i]; let authorLink = post.querySelector('p > span > a'); if (authorLink) { let author = authorLink.textContent; let content = post.querySelector('td.forumpost[id^="post_"]'); // re-retardify posts if (author && this.settings.isIgnored(author)) { if (this.settings.options.ignoreReplaceMessages) { content.innerHTML = this.settings.options.ignoreReplaceString; let signatureEl = post.querySelector('p.sig'); if (signatureEl) signatureEl.remove(); } else post.remove(); } } } } renderUserDetails() { let authorEl = document.querySelector('td.embedded > h1'); if (!authorEl) return; let author = authorEl.innerText; let ignored = this.settings.isIgnored(author); let blockEl = document.querySelector('a[href^="friends.php?action=add&type=block&"]'); let insertEl = document.createElement('a'); insertEl.innerText = ignored ? 'pašalinti iš ignoravimo' : 'ignoruoti'; insertEl.href = '#'; let inserted = Utils.InsertAfter(blockEl.nextSibling, document.createTextNode(' - (')); Utils.InsertAfter(inserted, insertEl); Utils.InsertAfter(insertEl, document.createTextNode(')')); insertEl.onclick = () => { if (ignored) this.settings.removeIgnored(author, true); else this.settings.addIgnored(author); location.reload(); }; } } //--- modules/lastPosts.js class LastPosts extends Base { async init() { if (!this.settings.options.showLastPosts) return Promise.resolve(); let bottomEl = document.querySelector('p.tcenter:last-child'); let lastPostsEl = document.createElement('p'); lastPostsEl.innerHTML = LoaderTemplate.Render(); Utils.InsertAfter(bottomEl, lastPostsEl); let posts = await Utils.FetchJSON('/forums/lastposts', 'POST', this.settings.getIgnored()); lastPostsEl.innerHTML = LastPostsTemplate.Render(posts); // set width (fuck css, this hack is awesome) let forumTable = document.querySelector('#forum'); let lastPostTable = document.querySelector('#last-posts'); lastPostTable.width = forumTable.offsetWidth; let contentLinks = lastPostTable.querySelectorAll('td[data-post-id]'); for (let i = 0; i < contentLinks.length; i++) { let postId = contentLinks[i].getAttribute('data-post-id'); let threadId = contentLinks[i].getAttribute('data-thread-id'); contentLinks[i].addEventListener('click', () => { location.href = `/forums.php?action=viewtopic&topicid=${threadId}&page=p${postId}#${postId}`; }); } } } //--- modules/topPosters.js class TopPosters { async renderTable() { document.querySelector('#content > table.main').remove(); let contentEl = document.querySelector('#content'); contentEl.innerHTML = LoaderTemplate.Render(); let top = await Utils.FetchJSON('/forums/top'); contentEl.innerHTML = TopPostersTemplate.Render(top); } renderTopLinks() { let searchAnchors = document.querySelectorAll('a[href="?action=search"]'); [...searchAnchors].forEach(searchAnchor => { let anchor = document.createElement('a'); anchor.href = '/forums.php?action=top'; anchor.innerText = 'Top'; Utils.InsertBefore(searchAnchor, anchor); Utils.InsertAfter(anchor, document.createTextNode(' | ')); }); } } //--- modules/userInfo.js class UserInfo { constructor(userId) { this._userId = userId; } async renderPostCount() { let userInfo = await Utils.FetchJSON(`/users/info/${this._userId}`); if (!userInfo) return; let rowToAppendAfter = document.querySelector('table.main tr:nth-child(4)'); let rows = UserInfoTemplate.RenderElement(userInfo); rowToAppendAfter = Utils.InsertAfter(rowToAppendAfter, rows[0]); Utils.InsertAfter(rowToAppendAfter, rows[1]); } } //--- modules/userPosts.js class UserPosts { constructor(userId, page) { this._userId = userId; this._page = page; } async init() { document.querySelector('#content > table.main').remove(); let contentEl = document.querySelector('#content'); contentEl.innerHTML = LoaderTemplate.Render(); let userPosts = await Utils.FetchJSON(`/users/${this._userId}/posts/${this._page > 1 ? this._page : ''}`); contentEl.innerHTML = UserPostsTemplate.Render(userPosts); } } //--- modules/originalPost.js class OriginalPost { constructor() { this.dateSince = new Date('2017-05-21'); } init() { Utils.GetPosts().forEach(post => { if (Utils.GetPostDate(post) < this.dateSince) return; let postEditedEl = post.querySelector('.forumpost p.small'); if (postEditedEl && postEditedEl.textContent.startsWith('Paskutinį kartą redagavo:')) { // render 'show original button' let postId = Utils.GetPostId(post); let showOriginalEl = ShowOriginalPostTemplate.RenderElement(postId); Utils.InsertAfter(postEditedEl, showOriginalEl); showOriginalEl.onclick = async () => { showOriginalEl.onclick = null; // too fast to show loader // showOriginalEl.innerHTML = LoaderTemplate.Render(); let origPost = await Utils.FetchJSON(`/forums/post/${postId}`); showOriginalEl.innerHTML = ShowOriginalPostTemplate.RenderOriginalPost(origPost); }; } }); } } //--- modules/userSettings.js class UserSettings extends Base { init() { let self = this; let contentEl = document.querySelector('#content'); let headerEl = UserSettingsTemplate.RenderHeaderElement(); let settingsEl = UserSettingsTemplate.RenderElement(this.settings); Utils.InsertAfter(contentEl.querySelector('table'), headerEl); Utils.InsertAfter(headerEl, settingsEl); // button remove ignored let btnRemoveIgnored = settingsEl.querySelector('#blmRemoveIgnoredBtn'); if (btnRemoveIgnored) btnRemoveIgnored.onclick = () => { let healedPlebs = Utils.GetSelectedValues(settingsEl.querySelector('#ignoredList')); healedPlebs.forEach(pleb => this.settings.removeIgnored(pleb, false)); let removeOptions = settingsEl.querySelectorAll('#ignoredList > option:checked'); removeOptions.forEach(opt => opt.remove()); }; // button save settings let btnSave = settingsEl.querySelector('#blmSaveBtn'); btnSave.onclick = () => { this.settings.save(); location.reload(); }; // checkbox last posts let lastPostsEl = settingsEl.querySelector('#showLastPosts'); lastPostsEl.checked = self.settings.options.showLastPosts; lastPostsEl.onchange = () => self.settings.options.showLastPosts = lastPostsEl.checked; // checkbox hide topics let hideTopicEl = settingsEl.querySelector('#hideTopics'); hideTopicEl.checked = self.settings.options.ignoreHideTopics; hideTopicEl.onchange = () => self.settings.options.ignoreHideTopics = hideTopicEl.checked; // checkbox replace messages let replaceMessagesEl = settingsEl.querySelector('#replaceMessages'); replaceMessagesEl.checked = self.settings.options.ignoreReplaceMessages; replaceMessagesEl.onchange = () => self.settings.options.ignoreReplaceMessages = replaceMessagesEl.checked; // input replace string let replaceStringEl = settingsEl.querySelector('#replaceString'); replaceStringEl.value = self.settings.options.ignoreReplaceString; replaceStringEl.onchange = () => self.settings.options.ignoreReplaceString = replaceStringEl.value; } } //--- modules/userMention.js // class UserMention { // constructor(textArea) { // this._textArea = textArea; // textArea.oninput = evt => { // let entered = textArea.value[textArea.selectionStart - 1]; // if (entered === '@') { // let prevSymbol = textArea.value[textArea.selectionStart - 2]; // // check for conditions to display autocomplete // if (prevSymbol === undefined || prevSymbol === ' ' || prevSymbol === '\n' // || prevSymbol === ']' || prevSymbol === ':' || prevSymbol === '>') { // } // } // }; // } // } //--- modules/deletedUsernames.js class DeletedUsernames { async init() { let anchorIdMap = new Map(); let posts = Utils.GetPosts(); [...posts].forEach(posts => { let anchor = posts.querySelector('p > span > a'); if (anchor && anchor.text === '') { let id = Number(anchor.href.split('?id=')[1]); if (anchorIdMap.has(id)) anchorIdMap.get(id).push(anchor); else anchorIdMap.set(id, [anchor]); } }); if (anchorIdMap.size === 0) return; let usernames = await Utils.FetchJSON('/users/usernames', 'POST', [...anchorIdMap.keys()]); if (!usernames) return; // yeah time to start defensive programming in case service is unreachable? usernames.forEach(u => { let anchors = anchorIdMap.get(u.Id); anchors.forEach(a => { a.text = `~ ${u.Username}`; a.removeAttribute('href'); }); }); } } //--- modules/twitchEmotes.js class TwitchEmotes extends Base { init() { // intercept editMessageSubmit function let origSubmitFn = window.editMessageSubmit; if (origSubmitFn) { window.editMessageSubmit = (form, id) => { this.handleSubmit(form); origSubmitFn.apply(this, [form, id]); }; } document.addEventListener('submit', evt => this.handleSubmit(evt.target)); } handleSubmit(form) { let textArea = form.querySelector('textarea'); textArea.value = textArea.value.replace(TwitchEmotes._EmotesRegex, (match, s1, s2, s3) => `${s1}${this.formImgEl(s2)}${s3}`); } formEmoteUrl(emote) { return `${Settings.ApiUrl}/assets/images/twitch/${emote}.png`; } formImgEl(emote) { return `[img]${this.formEmoteUrl(emote)}[/img]`; } // generated by tools/RipTwitchEmotes static get _EmotesRegex() { return new RegExp(`(\\.|\\n|^|,| |$)(4Head|AMPTropPunch|ANELE|ArgieB8|ArigatoNas|ArsonNoSexy|AsianGlow|BabyRage|BatChest|BCWarrior|BegWan|BibleThump|BigBrother|BigPhish|BJBlazkowicz|BlargNaut|bleedPurple|BlessRNG|BloodTrail|BrainSlug|BrokeBack|BuddhaBar|BudStar|CarlSmile|ChefFrank|cmonBruh|CoolCat|CoolStoryBob|copyThis|CorgiDerp|CrreamAwk|CurseLit|DAESuppy|DansGame|DatSheffy|DBstyle|DendiFace|DogFace|DoritosChip|duDudu|DxCat|EagleEye|EleGiggle|FailFish|FrankerZ|FreakinStinkin|FUNgineer|FunRun|FutureMan|GingerPower|GivePLZ|GOWSkull|GrammarKing|HassaanChop|HassanChop|HeyGuys|HotPokket|HumbleLife|imGlitch|InuyoFace|ItsBoshyTime|Jebaited|JKanStyle|JonCarnage|KAPOW|Kappa|KappaClaus|KappaPride|KappaRoss|KappaWealth|Kappu|Keepo|KevinTurtle|Kippa|KonCha|Kreygasm|Mau5|mcaT|MikeHogu|MingLee|MorphinTime|MrDestructoid|MVGame|NinjaGrumpy|NomNom|NotATK|NotLikeThis|OhMyDog|OneHand|OpieOP|OptimizePrime|OSblob|OSfrog|OSkomodo|OSsloth|panicBasket|PanicVis|PartyTime|pastaThat|PeoplesChamp|PermaSmug|PicoMause|PipeHype|PJSalt|PJSugar|PMSTwin|PogChamp|Poooound|PraiseIt|PRChase|PrimeMe|PunchTrees|PunOko|QuadDamage|RaccAttack|RalpherZ|RedCoat|ResidentSleeper|riPepperonis|RitzMitz|RlyTho|RuleFive|SabaPing|SeemsGood|ShadyLulu|ShazBotstix|SmoocherZ|SMOrc|SoBayed|SoonerLater|SPKFace|SPKWave|Squid1|Squid2|Squid3|Squid4|SSSsss|StinkyCheese|StoneLightning|StrawBeary|SuperVinlin|SwiftRage|TakeNRG|TBAngel|TBCrunchy|TBTacoBag|TBTacoProps|TearGlove|TehePelo|TF2John|ThankEgg|TheIlluminati|TheRinger|TheTarFu|TheThing|ThunBeast|TinyFace|TooSpicy|TriHard|TTours|TwitchLit|twitchRaid|TwitchRPG|TwitchUnity|UncleNox|UnSane|UWot|VaultBoy|VoHiYo|VoteNay|VoteYea|WholeWheat|WTRuck|WutFace|YouDontSay|YouWHY)(\\b)`, 'g'); } } //--- pages/forumPage.js class ForumPage { init() { let lastPosts = new LastPosts(); lastPosts.init(); let topPosters = new TopPosters(); topPosters.renderTopLinks(); } } //--- pages/forumViewPage.js class ForumViewPage { init() { // let forumId = Number(location.href.match(/forumid=(\d+)/)[1]); // let userIgnore = new UserIgnore(); userIgnore.hideTopics(); } } //--- pages/profilePage.js class ProfilePage { init() { let userSettings = new UserSettings(); userSettings.init(); } } //--- pages/replyPage.js class ReplyPage { init() { // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement; // let userMention = new UserMention(replyTextArea); // let userIgnore = new UserIgnore(); userIgnore.clearReply(); let twitchEmotes = new TwitchEmotes(); twitchEmotes.init(); } } //--- pages/replyQuotePage.js class ReplyQuotePage { init() { // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement; // let userMention = new UserMention(replyTextArea); // let userIgnore = new UserIgnore(); userIgnore.clearReplyQuote(); let twitchEmotes = new TwitchEmotes(); twitchEmotes.init(); } } //--- pages/topPage.js class TopPage { init() { let topPosters = new TopPosters(); topPosters.renderTable(); } } //--- pages/torrentDetailPage.js class TorrentDetailPage { init() { let userIgnore = new UserIgnore(); userIgnore.clearTorrentDetails(); } } //--- pages/userDetailsPage.js class UserDetailsPage extends Base { init() { let userDetailsId = Number(location.href.match(new RegExp(/\.php\?id=(\d+)/))[1]); if (userDetailsId !== this.settings.ownUserId) { let userInfo = new UserInfo(userDetailsId); userInfo.renderPostCount(); let userIgnore = new UserIgnore(); userIgnore.renderUserDetails(); } } } //--- pages/userHistoryPage.js class UserHistoryPage extends Base { init() { let userId = Number(location.href.match(/\/userhistory\.php\?action=viewposts&id=(\d+)/)[1]); if (this.settings.ownUserId === userId) return; let pageNumber; // and right about here I went 'fuck it' try { pageNumber = Number(location.href.match(/page=(\d+)/)[1]); } catch (ex) { pageNumber = 1; } let userPosts = new UserPosts(userId, pageNumber); userPosts.init(); } } //--- pages/viewTopicPage.js class ViewTopicPage { init() { // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement; // let userMention = new UserMention(replyTextArea); // let userIgnore = new UserIgnore(); userIgnore.clearTopic(); let originalPost = new OriginalPost(); originalPost.init(); let deletedUsernames = new DeletedUsernames(); deletedUsernames.init(); // yep ignore async we don't care, the train is rolling let twitchEmotes = new TwitchEmotes(); twitchEmotes.init(); } } //--- pages/editPage.js class EditPage { init () { // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement; // let userMention = new UserMention(replyTextArea); // let userIgnore = new UserIgnore(); userIgnore.clearReply(); let twitchEmotes = new TwitchEmotes(); twitchEmotes.init(); } } //--- main.js class BetterLM { static Init() { if (BetterLM.IsReply) new ReplyPage().init(); else if (BetterLM.IsReplyQuote) new ReplyQuotePage().init(); else if (BetterLM.IsViewTopic) new ViewTopicPage().init(); else if (BetterLM.IsForum) new ForumPage().init(); else if (BetterLM.IsForumView) new ForumViewPage().init(); else if (BetterLM.IsUserDetails) new UserDetailsPage().init(); else if (BetterLM.IsUserHistory) new UserHistoryPage().init(); else if (BetterLM.IsTorrentDetail) new TorrentDetailPage().init(); else if (BetterLM.IsProfile) new ProfilePage().init(); else if (BetterLM.IsTop) new TopPage().init(); else if (BetterLM.IsEdit) new EditPage().init(); } static get IsReply() { return BetterLM._test(/\/forums\.php\?action=reply&topicid=/); } static get IsViewTopic() { return BetterLM._test(/\/forums\.php\?action=viewtopic/); } static get IsReplyQuote() { return BetterLM._test(/\/forums\.php\?action=quotepost&topicid=/); } static get IsForum() { return BetterLM._test(/\/forums\.php$/); } static get IsForumView() { return BetterLM._test(/\/forums\.php\?action=viewforum/); } static get IsUserDetails() { return BetterLM._test(/\/userdetails\.php\?id=/); } static get IsUserHistory() { return BetterLM._test(/\/userhistory\.php\?action=viewposts&id=/); } static get IsTorrentDetail() { return BetterLM._test(/\/details?/) || BetterLM._test(/\/torrent?/); } static get IsProfile() { return BetterLM._test(/\/my.php(?:(\?edited=1)?)$/); } static get IsTop() { return BetterLM._test(/\/forums\.php\?action=top$/); } static get IsEdit() { return BetterLM._test(/\/forums\.php\?action=editpost/); } static _test(expr) { return expr.test(location.href); } } // here we go for the brighter tomorrow BetterLM.Init();