BetterLM

make Linkomanija great again

  1. // ==UserScript==
  2. // @name BetterLM
  3. // @namespace https://blm.hades.lt
  4. // @version 1.8.4
  5. // @description make Linkomanija great again
  6. // @author Krupp
  7. // @match https://www.linkomanija.net/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. //--- utils.js
  12. class Utils {
  13. static InsertAfter(element, newNode) {
  14. return element.parentElement.insertBefore(newNode, element.nextSibling);
  15. }
  16.  
  17. static InsertBefore(element, newNode) {
  18. return element.parentElement.insertBefore(newNode, element.previousSibling);
  19. }
  20.  
  21. static GetSelectedValues(element) {
  22. let res = [];
  23.  
  24. if (element.options) {
  25. for (let i = 0; i < element.options.length; i++) {
  26. let opt = element.options[i];
  27. if (opt.selected)
  28. res.push(opt.value);
  29. }
  30. }
  31.  
  32. return res;
  33. }
  34.  
  35. static async FetchJSON(url, method = 'GET', data = null) {
  36. let reqObj = {method};
  37. if (data) {
  38. reqObj.body = JSON.stringify(data);
  39. reqObj.headers = {'content-type': 'application/json'};
  40. }
  41.  
  42. return await fetch(Settings.ApiUrl + url, reqObj).then(resp => resp.json());
  43. }
  44.  
  45. static PrintDate(dateStr) {
  46. if (!dateStr) return '';
  47.  
  48. // do not convert to UTC
  49. dateStr = dateStr.replace('T', ' ');
  50. let date = new Date(dateStr);
  51.  
  52. return `${date.getFullYear()}-${Utils._WZero(date.getMonth() + 1)}-${Utils._WZero(date.getDate())}
  53. ${Utils._WZero(date.getHours())}:${Utils._WZero(date.getMinutes())}:${Utils._WZero(date.getSeconds())}`;
  54. }
  55.  
  56. // with zero, so it outputs 09 minutes instead of 9 -.-
  57. static _WZero(inp) {
  58. return inp < 10 ? '0' + inp : '' + inp; // so that it always returns String, not Number sometimes
  59. }
  60.  
  61. // Null or Undefined
  62. static NU(obj) {
  63. return obj === null || obj === undefined;
  64. }
  65.  
  66. static GetPosts() {
  67. return document.querySelectorAll('div[id^="post_"]');
  68. }
  69.  
  70. static GetPostId(postEl) {
  71. if (!postEl) return null;
  72.  
  73. let postIdAttr = postEl.getAttribute('id');
  74.  
  75. if (postIdAttr) {
  76. postIdAttr = postIdAttr.replace('post_', '');
  77. let postId = Number(postIdAttr);
  78.  
  79. if (Number.isNaN(postId)) return null;
  80. else return postId;
  81. }
  82.  
  83. return null;
  84. }
  85.  
  86. static get _postDateRegex() {
  87. return /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/;
  88. }
  89.  
  90. static GetPostDate(postEl) {
  91. let infoTextEl = postEl.querySelector('span:first-child');
  92. if (infoTextEl) {
  93. let regexResult = Utils._postDateRegex.exec(infoTextEl.textContent);
  94. if (regexResult) return new Date(regexResult[0]);
  95. }
  96.  
  97. return null;
  98. }
  99. }
  100.  
  101. //--- options.js
  102. class Options {
  103. constructor(obj = null) {
  104. if (obj) {
  105. this.notifyOnUserMention = obj.notifyOnUserMention;
  106. this.showLastPosts = obj.showLastPosts;
  107. this.ignoreHideTopics = obj.ignoreHideTopics;
  108. this.ignoreReplaceMessages = obj.ignoreReplaceMessages;
  109. this.ignoreReplaceString = obj.ignoreReplaceString;
  110. }
  111. }
  112.  
  113. validate() {
  114. if (Utils.NU(this.notifyOnUserMention) || Utils.NU(this.showLastPosts)
  115. || Utils.NU(this.ignoreReplaceMessages) || Utils.NU(this.ignoreHideTopics))
  116. throw "options aren't valid";
  117. }
  118.  
  119. setDefaults() {
  120. this.notifyOnUserMention = true;
  121. this.showLastPosts = false;
  122. this.ignoreHideTopics = false;
  123. this.ignoreReplaceMessages = true;
  124. this.ignoreReplaceString = 'Vartotojas ignoruotas.';
  125. }
  126. }
  127.  
  128. //--- settings.js
  129. class Settings {
  130. static get KeyOptions() {
  131. return 'blm_options';
  132. }
  133.  
  134. static get KeyIgnored() {
  135. // for backwards compatibility with 'LMRetard' script
  136. return 'retards';
  137. }
  138.  
  139. static get ApiUrl() {
  140. return 'https://blm.hades.lt';
  141. }
  142.  
  143. static Instance() {
  144. if (this._instance)
  145. return this._instance;
  146.  
  147. this._instance = new Settings();
  148. this._instance._load();
  149. return this._instance;
  150. }
  151.  
  152. save() {
  153. let optionsJson = JSON.stringify(this.options);
  154. localStorage.setItem(Settings.KeyOptions, optionsJson);
  155.  
  156. let ignoredJson = JSON.stringify(this.ignored);
  157. localStorage.setItem(Settings.KeyIgnored, ignoredJson);
  158. }
  159.  
  160. _load() {
  161. // load options
  162. try {
  163. let optionsJson = localStorage.getItem(Settings.KeyOptions);
  164. let options = JSON.parse(optionsJson);
  165.  
  166. this.options = new Options(options);
  167. this.options.validate();
  168. }
  169. catch (ex) {
  170. // oh well...
  171. this._resetOptions();
  172. this.save();
  173. }
  174.  
  175. // load ignored users
  176. try {
  177. let ignoredJson = localStorage.getItem(Settings.KeyIgnored);
  178. let ignored = JSON.parse(ignoredJson);
  179.  
  180. if (!Array.isArray(ignored))
  181. throw 'do not swear';
  182.  
  183. ignored.forEach(x => {
  184. if (typeof x !== 'string')
  185. throw 'fuck, I told a swear word';
  186. });
  187.  
  188. this.ignored = ignored;
  189. }
  190. catch (ex) {
  191. // well shitballs...
  192. this._resetIgnored();
  193. this.save();
  194. }
  195.  
  196. // set own name
  197. let username = document.querySelector('#username');
  198. if (username)
  199. this.ownName = username.innerText;
  200.  
  201. // set own id
  202. let userHref = username.querySelector('a');
  203. if (userHref)
  204. this.ownUserId = Number(userHref.href.split('?id=')[1]); // 99% of the time never throws
  205. }
  206.  
  207. isIgnored(name) {
  208. if (name === this.ownName)
  209. return false;
  210.  
  211. return this.ignored.indexOf(name) !== -1;
  212. }
  213.  
  214. addIgnored(name) {
  215. if (name === this.ownName)
  216. return;
  217.  
  218. if (!this.isIgnored(name)) {
  219. this.ignored.push(name);
  220. this.save();
  221. } else {
  222. // todo: something better than alert would be nice
  223. alert(`${name} is already ignored, refresh the page.`);
  224. }
  225. }
  226.  
  227. getIgnored() {
  228. return this.ignored;
  229. }
  230.  
  231. removeIgnored(name, save = true) {
  232. let idx = this.ignored.indexOf(name);
  233.  
  234. if (idx !== -1) {
  235. this.ignored.splice(idx, 1);
  236. if (save)
  237. this.save();
  238. }
  239. }
  240.  
  241. _resetOptions() {
  242. this.options = new Options();
  243. this.options.setDefaults();
  244. }
  245.  
  246. _resetIgnored() {
  247. this.ignored = [];
  248. }
  249. }
  250.  
  251. //--- base.js
  252. class Base {
  253. constructor() {
  254. this.settings = Settings.Instance();
  255. }
  256. }
  257.  
  258. //--- templates/templateEngine.js
  259. // "Holy Mashed Potatoes, Batman!" -Robin
  260. // update: fuck this shit, shoulda used handlebars
  261. class TemplateEngine {
  262. static get _ForeachEnd() {
  263. return '@{/foreach}';
  264. }
  265.  
  266. static get _ForeachStart() {
  267. return /@{foreach x in (.*)}/;
  268. }
  269.  
  270. static get _IfStart() {
  271. return /@{if\((.*)\)}/;
  272. }
  273.  
  274. static get _IfEnd() {
  275. return '@{/if}';
  276. }
  277.  
  278. static get _ExecStart() {
  279. return '@{exec}';
  280. }
  281.  
  282. static get _ExecEnd() {
  283. return '@{/exec}';
  284. }
  285.  
  286. static get _ForStart() {
  287. return /@{for\((.*)\)}/;
  288. }
  289.  
  290. static get _ForEnd() {
  291. return '@{/for}';
  292. }
  293.  
  294. static get Template() {
  295. throw 'must override Template';
  296. }
  297.  
  298. static Render(model, html = this.Template, x = null) {
  299. let exprArr;
  300.  
  301. while ((exprArr = /@{.*?}/.exec(html)) !== null) {
  302. let exprRaw = exprArr[0];
  303. let expr = exprRaw.replace(/(^@{)|(}$)/g, '');
  304.  
  305. // code is repeating itself a lot QQ
  306. // todo: refactor (or fucking use handlebars for reals)
  307. // handle FOREACHs
  308. if (TemplateEngine._ForeachStart.test(exprRaw)) {
  309. let foreachExpArr = TemplateEngine._ForeachStart.exec(exprRaw);
  310.  
  311. let endIdx = html.indexOf(TemplateEngine._ForeachEnd, foreachExpArr.index);
  312.  
  313. let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ForeachEnd.length);
  314. let template = templateRaw.substr(foreachExpArr[0].length,
  315. templateRaw.length - TemplateEngine._ForeachEnd.length - foreachExpArr[0].length);
  316.  
  317. let expressedTemplate = TemplateEngine._ApplyForeach(template, model, foreachExpArr[1]);
  318.  
  319. html = html.replace(templateRaw, expressedTemplate);
  320. }
  321. // handle IFs
  322. else if (TemplateEngine._IfStart.test(exprRaw)) {
  323. let ifExprArr = TemplateEngine._IfStart.exec(exprRaw);
  324.  
  325. let endIdx = html.indexOf(TemplateEngine._IfEnd, ifExprArr.index);
  326.  
  327. let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._IfEnd.length);
  328. let template = templateRaw.substr(ifExprArr[0].length,
  329. templateRaw.length - TemplateEngine._IfEnd.length - ifExprArr[0].length);
  330.  
  331. let expressedTemplate = TemplateEngine._ApplyIf(template, model, ifExprArr[1]);
  332.  
  333. html = html.replace(templateRaw, expressedTemplate);
  334. }
  335. // handle exec
  336. else if (exprRaw === TemplateEngine._ExecStart) {
  337. let endIdx = html.indexOf(TemplateEngine._ExecEnd, exprArr.index);
  338.  
  339. let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ExecEnd.length);
  340. let template = templateRaw.substr(TemplateEngine._ExecStart.length,
  341. templateRaw.length - TemplateEngine._ExecEnd.length - TemplateEngine._ExecStart.length);
  342.  
  343. let expressedTemplate = TemplateEngine._ApplyExec(template, model);
  344.  
  345. html = html.replace(templateRaw, expressedTemplate);
  346. }
  347. // handle FORs
  348. else if (TemplateEngine._ForStart.test(exprRaw)) {
  349. let forExprArr = TemplateEngine._ForStart.exec(exprRaw);
  350.  
  351. let endIdx = html.indexOf(TemplateEngine._ForEnd, forExprArr.index);
  352.  
  353. let templateRaw = html.substring(exprArr.index, endIdx + TemplateEngine._ForEnd.length);
  354. let template = templateRaw.substr(forExprArr[0].length,
  355. templateRaw.length - TemplateEngine._ForEnd.length - forExprArr[0].length);
  356.  
  357. let expressedTemplate = TemplateEngine._ApplyFor(template, model, x, forExprArr[1]);
  358.  
  359. html = html.replace(templateRaw, expressedTemplate);
  360. }
  361. // handle the stuff
  362. else {
  363. let expressed;
  364. try {
  365. expressed = eval(expr);
  366. }
  367. catch (ex) {
  368. expressed = ex;
  369. }
  370. html = html.replace(exprRaw, expressed);
  371. }
  372. }
  373.  
  374. return html;
  375. }
  376.  
  377. // does not support attributes with spaces in them, huehuehue
  378. static RenderElement(model, html = this.Template) {
  379. html = TemplateEngine.Render(model, html);
  380.  
  381. // it's a Kirby!
  382. let rootElTagRes = /<(.*)>/.exec(html);
  383.  
  384. let split = rootElTagRes[1].split(' ');
  385.  
  386. // strip out parent el tags, replace replace replace REPLACE
  387. html = html.replace(rootElTagRes[0], '').replace(rootElTagRes[0].replace('<', '</'), '');
  388.  
  389. let element = document.createElement(split[0]);
  390.  
  391. // set attributes
  392. for (let i = 1; i < split.length; i++) {
  393. let attr = split[i].split('=');
  394. if (attr.length === 2)
  395. element.setAttribute(attr[0], attr[1].replace(/"/g, ''));
  396. }
  397.  
  398. element.innerHTML = html;
  399.  
  400. return element;
  401. }
  402.  
  403. // does not handle foreach inside foreach, huehuehue
  404. // also foreach x syntax is set in stone, cannot override x, huehuehue
  405. static _ApplyForeach(template, model, forEachStr) {
  406. let html = '';
  407.  
  408. eval(forEachStr).forEach((x, i) => {
  409. if (typeof x === 'object') x._INDEX = i;
  410. html += TemplateEngine.Render(null, template, x);
  411. });
  412.  
  413. return html;
  414. }
  415.  
  416. // does not handle if inside if, might work with foreach though?
  417. // else is too hard, huehuehuehue (and saturation)
  418. static _ApplyIf(template, model, conditionStr) {
  419. if (!eval(conditionStr)) return '';
  420.  
  421. return TemplateEngine.Render(model, template);
  422. }
  423.  
  424. static _ApplyExec(execStr, model) {
  425. eval(execStr);
  426.  
  427. return '';
  428. }
  429.  
  430. static _ApplyFor(template, model, x, expr) {
  431. let html = '';
  432.  
  433. // I'll admit - JavaScript is quite fun lawl
  434. expr = `for(${expr}) { html += TemplateEngine.Render(model, template, x); }`;
  435. eval(expr);
  436.  
  437. return html;
  438. }
  439. }
  440.  
  441. //--- templates/loaderTemplate.js
  442. class LoaderTemplate extends TemplateEngine {
  443.  
  444. static _loadStyle() {
  445. if (LoaderTemplate._styleLoaded) return;
  446.  
  447. LoaderTemplate._styleLoaded = true;
  448. document.head.innerHTML += `<style>
  449. .spinner {
  450. margin: 100px auto;
  451. width: 40px;
  452. height: 40px;
  453. position: relative;
  454. }
  455.  
  456. .cube1, .cube2 {
  457. background-color: royalblue;
  458. width: 15px;
  459. height: 15px;
  460. position: absolute;
  461. top: 0;
  462. left: 0;
  463.  
  464. -webkit-animation: sk-cubemove 1.8s infinite ease-in-out;
  465. animation: sk-cubemove 1.8s infinite ease-in-out;
  466. }
  467.  
  468. .cube2 {
  469. -webkit-animation-delay: -0.9s;
  470. animation-delay: -0.9s;
  471. }
  472.  
  473. @-webkit-keyframes sk-cubemove {
  474. 25% { -webkit-transform: translateX(42px) rotate(-90deg) scale(0.5) }
  475. 50% { -webkit-transform: translateX(42px) translateY(42px) rotate(-180deg) }
  476. 75% { -webkit-transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5) }
  477. 100% { -webkit-transform: rotate(-360deg) }
  478. }
  479.  
  480. @keyframes sk-cubemove {
  481. 25% {
  482. transform: translateX(42px) rotate(-90deg) scale(0.5);
  483. -webkit-transform: translateX(42px) rotate(-90deg) scale(0.5);
  484. } 50% {
  485. transform: translateX(42px) translateY(42px) rotate(-179deg);
  486. -webkit-transform: translateX(42px) translateY(42px) rotate(-179deg);
  487. } 50.1% {
  488. transform: translateX(42px) translateY(42px) rotate(-180deg);
  489. -webkit-transform: translateX(42px) translateY(42px) rotate(-180deg);
  490. } 75% {
  491. transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5);
  492. -webkit-transform: translateX(0px) translateY(42px) rotate(-270deg) scale(0.5);
  493. } 100% {
  494. transform: rotate(-360deg);
  495. -webkit-transform: rotate(-360deg);
  496. }
  497. }
  498. </style>`;
  499. }
  500.  
  501. // from https://github.com/tobiasahlin/SpinKit
  502. static get Template() {
  503. LoaderTemplate._loadStyle();
  504. return `
  505. <div class="spinner">
  506. <div class="cube1"></div>
  507. <div class="cube2"></div>
  508. </div>
  509. `;
  510. }
  511. }
  512. LoaderTemplate._styleLoaded = false; // wow javascript, no other way to use static fields, good job!
  513.  
  514. //--- templates/lastPostsTemplate.js
  515. class LastPostsTemplate extends TemplateEngine {
  516. static get Template() {
  517. return `
  518. <h1>Paskutiniai pranešimai</h1>
  519. <table border="1" cellspacing="0" cellpadding="5" id="last-posts">
  520. <tbody>
  521. <tr class="colhead">
  522. <td class="tcenter">Autorius</td>
  523. <td class="tcenter"inutė</td>
  524. <td class="tcenter">Laikas</td>
  525. </tr>
  526. @{foreach x in model}
  527. <tr>
  528. <td><a href="userdetails.php?id=@{x.UserId}">@{x.Username}</a></td>
  529. <td class="hover last-post-content" data-post-id="@{x.Id}" data-thread-id="@{x.ThreadId}">
  530. @{x.Content}
  531. </td>
  532. <td class="tcenter" style="min-width: 70px;">@{Utils.PrintDate(x.Date)}</td>
  533. </tr>
  534. @{/foreach}
  535. </tbody>
  536. </table>
  537.  
  538. <style>
  539. .last-post-content {
  540. cursor: pointer;
  541. }
  542. </style>
  543. `;
  544. }
  545. }
  546.  
  547. //--- templates/topUserTableTemplate.js
  548. class TopUserTableTemplate extends TemplateEngine {
  549. static get Template() {
  550. return `
  551. <table class="top-table">
  552. <tbody>
  553. <tr class="colhead">
  554. <td class="tcenter">#</td>
  555. <td class="tcenter">Vartotojas</td>
  556. <td class="tcenter"inutės</td>
  557. </tr>
  558. </tbody>
  559. @{foreach x in model}
  560. <tr>
  561. <td class="tright">@{x._INDEX + 1}</td>
  562. <td><a href="/userdetails.php?id=@{x.Id}">@{x.Username}</a></td>
  563. <td class="tright">@{x.PostCount}</td>
  564. </tr>
  565. @{/foreach}
  566. </table>`;
  567. }
  568. }
  569.  
  570. //--- templates/topPostersTemplate.js
  571. class TopPostersTemplate extends TemplateEngine {
  572. static get Template() {
  573. return `
  574. <div id="top-container">
  575. <div class="top-item">
  576. <h2>All time</h2>
  577. @{TopUserTableTemplate.Render(model.All)}
  578. </div>
  579.  
  580. <div class="top-item">
  581. <h2>Paskutiniai metai</h2>
  582. @{TopUserTableTemplate.Render(model.Year)}
  583. </div>
  584.  
  585. <div class="top-item">
  586. <h2>Paskutinis mėnesis</h2>
  587. @{TopUserTableTemplate.Render(model.Month)}
  588. </div>
  589. </div>
  590.  
  591. <style>
  592. #top-container {
  593. display: flex;
  594. justify-content: center;
  595. }
  596.  
  597. .top-item {
  598. width: auto;
  599. padding: 20px;
  600. }
  601.  
  602. .top-table td {
  603. padding-top: 7px;
  604. padding-bottom: 7px;
  605. padding-left: 6px;
  606. padding-right: 6px;
  607. }
  608. </style>
  609. `;
  610. }
  611. }
  612.  
  613. //--- templates/userInfoTemplate.js
  614. class UserInfoTemplate extends TemplateEngine {
  615. static get Template() {
  616. return `
  617. <tr>
  618. <td class="rowhead"inutės:</td>
  619. <td align="left">
  620. ~ <a href="/userhistory.php?action=viewposts&id=@{model.Id}">@{model.PostCount}</a>
  621. </td>
  622. </tr>
  623.  
  624. <tr>
  625. <td class="rowhead">Paskutinė:</td>
  626. <td align="left">@{Utils.PrintDate(model.LastPostDate)}</td>
  627. </tr>
  628. `;
  629. }
  630.  
  631. // override, returns array with two tr elements
  632. static RenderElement(model) {
  633. let trs = [];
  634.  
  635. let splitTemplate = this.Template.split('\n\n');
  636.  
  637. trs.push(super.RenderElement(model, splitTemplate[0]));
  638. trs.push(super.RenderElement(model, splitTemplate[1]));
  639.  
  640. return trs;
  641. }
  642. }
  643.  
  644. //--- templates/userPostsTemplate.js
  645. class UserPostsTemplate extends TemplateEngine {
  646. static get Template() {
  647. return `
  648. <h1>
  649. <a href="/userdetails.php?id=@{model.UserId}"><b>@{model.Username}</b></a> postų istorija
  650. </h1>
  651. @{UserPostsPaginationTemplate.Render(model)}
  652.  
  653. <table class="main" border="0" cellspacing="0" cellpadding="0">
  654. <tbody>
  655. <tr>
  656. <td class="embedded">
  657. <table width="98%" border="1" cellspacing="0" cellpadding="10">
  658. <tbody>
  659. <tr>
  660. <td>
  661. @{foreach x in model.Posts}
  662. <p class="sub"></p>
  663. <table border="0" cellspacing="0" cellpadding="0">
  664. <tbody>
  665. <tr>
  666. <td class="embedded">
  667. @{Utils.PrintDate(x.Date)} -- <b>Tema:</b> <a href="/forums.php?action=viewtopic&topicid=@{x.ThreadId}">@{x.ThreadTitle}</a>
  668. -- <b>Posto ID: </b>#<a href="/forums.php?action=viewtopic&topicid=@{x.ThreadId}&page=p@{x.Id}#@{x.Id}">@{x.Id}</a>
  669. </td>
  670. </tr>
  671. </tbody>
  672. </table>
  673. <p></p>
  674. <table class="main" width="97%" border="1" cellspacing="0" cellpadding="5">
  675. <tbody>
  676. <tr valign="top">
  677. <td class="comment">
  678. @{x.Content}
  679. </td>
  680. </tr>
  681. </tbody>
  682. </table>
  683. @{/foreach}
  684. </td>
  685. </tr>
  686. </tbody>
  687. </table>
  688. </td>
  689. </tr>
  690. </tbody>
  691. </table>
  692.  
  693. @{UserPostsPaginationTemplate.Render(model)}
  694. `;
  695. }
  696. }
  697.  
  698. //--- templates/userPostsPaginationTemplate.js
  699. class UserPostsPaginationTemplate extends TemplateEngine {
  700. // fuck me
  701. static get Template() {
  702. return `
  703. @{exec}
  704. model.totalPages = Math.ceil(model.TotalPosts / 25);
  705. model.pagination = [];
  706. if (model.Page > model.totalPages) model.Page = model.totalPages;
  707.  
  708. for (let i = 0; i < model.totalPages; i++) {
  709. let lowerBound = i * 25 + 1;
  710. var upperBound = Math.min(lowerBound + 24, model.TotalPosts);
  711. model.pagination[i] = [lowerBound, upperBound];
  712. }
  713.  
  714. model.j = 0;
  715. @{/exec}
  716. <p align="center">
  717. <!-- prev and first sticky -->
  718. @{if(model.Page === 1)}
  719. <span class="pageinactive">«&nbsp;Ankstesnis</span>
  720. <span class="pageinactive">1&nbsp;-&nbsp;@{model.pagination[0][1]}</span>
  721. @{/if}
  722. @{if(model.Page !== 1)}
  723. <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.Page - 1}">«&nbsp;Ankstesnis</a>
  724. <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=1">1&nbsp;-&nbsp;@{model.pagination[0][1]}</a>
  725. @{/if}
  726. <!-- render dots if needed -->
  727. @{if(model.Page > 4)}
  728. ...
  729. @{/if}
  730. <!-- render 2 previous buttons -->
  731. @{for(model.j = Math.max(model.Page - 3, 1); model.j < model.Page - 1; model.j++)}
  732. <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.j + 1}">@{model.pagination[model.j][0]}&nbsp;-&nbsp;@{model.pagination[model.j][1]}</a>
  733. @{/for}
  734. <!-- render current button -->
  735. @{exec}
  736. if (model.Page === 1) model.j = 0;
  737. @{/exec}
  738. @{if(model.j !== 0 && model.j !== model.totalPages - 1)}
  739. <span class="pageinactive">@{model.pagination[model.j][0]}&nbsp;-&nbsp;@{model.pagination[model.j][1]}</span>
  740. @{/if}
  741. <!-- render 2 next buttons -->
  742. @{for(model.j = model.j + 1; model.j < Math.min(model.totalPages - 1, model.Page + 2); model.j++)}
  743. <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.j + 1}">@{model.pagination[model.j][0]}&nbsp;-&nbsp;@{model.pagination[model.j][1]}</a>
  744. @{/for}
  745. <!-- dooooots -->
  746. @{if(model.Page < model.totalPages - 3)}
  747. ...
  748. @{/if}
  749. <!-- next and last sticky -->
  750. @{if(model.Page === model.totalPages && model.totalPages !== 1)}
  751. <span class="pageinactive">@{model.pagination[model.totalPages-1][0]}&nbsp;-&nbsp;@{model.pagination[model.totalPages - 1][1]}</span>
  752. @{/if}
  753. @{if(model.Page === model.totalPages)}
  754. <span class="pageinactive">Kitas&nbsp;»</span>
  755. @{/if}
  756. @{if(model.Page !== model.totalPages && model.totalPages !== 1)}
  757. <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.totalPages}">@{model.pagination[model.totalPages - 1][0]}&nbsp;-&nbsp;@{model.pagination[model.totalPages - 1][1]}</a>
  758. @{/if}
  759. @{if(model.Page !== model.totalPages)}
  760. <a class="pagelink" href="/userhistory.php?action=viewposts&id=@{model.UserId}&page=@{model.Page + 1}">Kitas&nbsp;»</a>
  761. @{/if}
  762. </p>
  763. `;
  764. }
  765. }
  766.  
  767. //--- templates/showOriginalPostTemplate.js
  768. class ShowOriginalPostTemplate extends TemplateEngine {
  769. static get Template() {
  770. return `<p class="small">
  771. <span style="cursor: pointer;">Rodyti originalų pranešimą?</span>
  772. </p>`;
  773. }
  774.  
  775. static get TemplateOriginalPost() {
  776. return `
  777. <p class="sub">
  778. Originalus postas:
  779. </p>
  780. <table class="main" border="1" cellspacing="0" cellpadding="10">
  781. <tbody>
  782. <tr>
  783. <td style="border: 1px black dotted">@{model.Content}</td>
  784. </tr>
  785. </tbody>
  786. </table>`;
  787. }
  788.  
  789. static RenderOriginalPost(model) {
  790. return this.Render(model, this.TemplateOriginalPost);
  791. }
  792. }
  793.  
  794. //--- templates/userSettingsTemplate.js
  795. class UserSettingsTemplate extends TemplateEngine {
  796. static get HeaderTemplate() {
  797. return `<h1>
  798. BetterLM nustatymai
  799. </h1>`;
  800. }
  801.  
  802. static RenderHeaderElement() {
  803. return this.RenderElement(null, this.HeaderTemplate);
  804. }
  805.  
  806. static get Template() {
  807. return `
  808. <table border="1" cellspacing="0" cellpadding="10" align="center" width="100%">
  809. <!-- I don't even this html structure -->
  810. <tbody><tr><td colspan="7"><table border="1" cellspacing="0" cellpadding="5" width="100%"><tbody>
  811.  
  812. <tr>
  813. <td class="rowhead" valign="top" align="right">Rodyti paskutinius pranešimus</td>
  814. <td valign="top" align="left">
  815. <label><input type="checkbox" id="showLastPosts">Rodyti paskutinius pranešimus</label>
  816. </td>
  817. </tr>
  818.  
  819. <tr>
  820. <td class="rowhead" valign="top" align="right">Ignoravimo nustatymai</td>
  821. <td valign="top" align="left">
  822. <label><input type="checkbox" id="hideTopics">Slėpti sukurtas temas forume</label><br>
  823. <label><input type="checkbox" id="replaceMessages">Pakeisti žinutės tekstą vietoje pašalinimo:</label><br>
  824. <input type="text" id="replaceString">
  825. </td>
  826. </tr>
  827.  
  828. <tr>
  829. <td class="rowhead" valign="top" align="right">Ignoruotieji</td>
  830. <td valign="top" align="left">
  831. @{if(model.ignored.length > 0)}
  832. <select size="@{Math.min(12, model.ignored.length)}" multiple id="ignoredList">
  833. @{foreach x in model.ignored}
  834. <option value="@{x}">@{x}</option>
  835. @{/foreach}
  836. </select><br/>
  837. <button id="blmRemoveIgnoredBtn" type="button">Pašalinti</button>
  838. @{/if}
  839. @{if(model.ignored.length <= 0)}
  840. <div>Nieko neignoruoji, sugyveni draugiškai</div>
  841. @{/if}
  842. </td>
  843. </tr>
  844.  
  845. <tr>
  846. <td colspan="2" class="center">
  847. <input type="button" id="blmSaveBtn" value="Patvirtinti!!1" style="height: 25px">
  848. </td>
  849. </tr>
  850.  
  851. </tbody></table></td></tr></tbody>
  852. </table>
  853. `;
  854. }
  855. }
  856.  
  857. //--- templates/userMentionTemplate.js
  858.  
  859. //--- modules/userIgnore.js
  860. class UserIgnore extends Base {
  861. static get _QuoteRegex() {
  862. return /\[quote=\w*\]/g;
  863. }
  864.  
  865. static get _AuthorRegex() {
  866. return /(?:\[quote=)(\w*)(?:\])/;
  867. }
  868.  
  869. static get _EndQuoteRegex() {
  870. return /\[\/quote\]/g;
  871. }
  872.  
  873. init() {
  874. }
  875.  
  876. hideTopics() {
  877. if (!this.settings.options.ignoreHideTopics) return;
  878.  
  879. let tables = document.querySelectorAll('table');
  880. if (tables.length < 1) return;
  881.  
  882. let rows = tables[0].querySelectorAll('tr:not(.colhead)');
  883. [...rows].forEach(row => {
  884. let authorLink = row.querySelector('td:nth-child(4) > a');
  885. if (authorLink) {
  886. let author = authorLink.textContent;
  887. if (author && this.settings.isIgnored(author))
  888. row.remove();
  889. }
  890. });
  891. }
  892.  
  893. clearTorrentDetails() {
  894. let comments = document.querySelectorAll('#comments > div.comment');
  895. if (!comments) return;
  896.  
  897. [...comments].forEach(comment => {
  898. let authorEl = comments.querySelector('div.comment-user > a');
  899. if (!authorEl) return;
  900.  
  901. let author = authorEl.textContent;
  902. if (!author || !this.settings.isIgnored(author)) return;
  903.  
  904. if (this.settings.options.ignoreReplaceMessages) {
  905. let commentText = comment.querySelector('div.comment-text');
  906. if (commentText)
  907. commentText.textContent = this.settings.options.ignoreReplaceString;
  908. } else {
  909. comment.remove();
  910. }
  911. });
  912. }
  913.  
  914. clearReplyQuote() {
  915. this.clearReply();
  916.  
  917. let textArea = document.querySelector('textarea#body');
  918. if (!textArea) return;
  919.  
  920. let text = textArea.value;
  921. let quotes = text.match(UserIgnore._QuoteRegex);
  922. if (!quotes) return;
  923.  
  924. for (let i = 0; i < quotes.length; i++) {
  925. let quote = quotes[i];
  926. let authorMatch = quote.match(UserIgnore._AuthorRegex);
  927. if (!authorMatch || authorMatch.length !== 2) continue;
  928.  
  929. let author = authorMatch[1];
  930. if (this.settings.isIgnored(author)) {
  931. try {
  932. let idxStart = text.indexOf(quote) + quote.length;
  933. if (idxStart === (-1 + quote.length)) throw 'mangled markup';
  934.  
  935. let idxEnd;
  936.  
  937. /* [quote] and [/quote] count should equal, otherwise formatting
  938. is mangled and there's fuck I can do and the fuck I care lel */
  939. for (let j = 0; j < quotes.length; j++) {
  940. let match = UserIgnore._EndQuoteRegex.exec(text);
  941. // black box logic go figure
  942. if (quotes.length - 1 - j !== i) continue;
  943. idxEnd = match.index;
  944. break;
  945. }
  946.  
  947. let toReplace = text.substring(idxStart, idxEnd);
  948. text = text.replace(toReplace, this.settings.options.ignoreReplaceString);
  949. textArea.value = text;
  950. return;
  951. }
  952. catch (ex) { /* noop */
  953. }
  954. }
  955. }
  956. }
  957.  
  958. clearReply() {
  959. let replies = document.querySelectorAll('p.sub');
  960.  
  961. for (let i = 0; i < replies.length; i++) {
  962. let reply = replies[i];
  963.  
  964. let authorParts = reply.textContent.split(' ');
  965. if (authorParts.length < 2) return;
  966.  
  967. let author = authorParts[1];
  968. let contentEl = reply.nextElementSibling;
  969.  
  970. if (this.settings.isIgnored(author)) {
  971. if (this.settings.options.ignoreReplaceMessages) {
  972. let textEl = contentEl.querySelector('tbody > tr > td:last-child');
  973. if (textEl)
  974. textEl.textContent = this.settings.options.ignoreReplaceString;
  975. } else {
  976. contentEl.remove();
  977. reply.remove();
  978. }
  979. } else {
  980. this.clearQuote(contentEl);
  981. }
  982. }
  983. }
  984.  
  985. clearQuote(el) {
  986. try {
  987. let quoteHeaders = el.querySelectorAll('p.sub');
  988.  
  989. for (let j = 0; j < quoteHeaders.length; j++) {
  990. let quoteHeader = quoteHeaders[j];
  991. let quoteAuthorArr = quoteHeader.textContent.split(' ');
  992. if (!quoteAuthorArr || quoteAuthorArr.length !== 2) continue;
  993.  
  994. let quoteAuthor = quoteAuthorArr[0];
  995. if (this.settings.isIgnored(quoteAuthor)) {
  996. let quoteContent = quoteHeader.nextElementSibling.querySelector('td');
  997. quoteContent.innerHTML = this.settings.options.ignoreReplaceString;
  998. }
  999. }
  1000. }
  1001. catch (ex) { /* do not expect a comment in every empty catch block */
  1002. }
  1003. }
  1004.  
  1005. clearTopic() {
  1006. let posts = Utils.GetPosts();
  1007. this._clearTopic();
  1008.  
  1009. // todo: move to separate function, come on...
  1010. for (let i = 0; i < posts.length; i++) {
  1011. let post = posts[i];
  1012. let authorLink = post.querySelector('p > span > a');
  1013. if (!authorLink || !authorLink.href) continue;
  1014.  
  1015. let author = authorLink.textContent;
  1016. if (author === this.settings.ownName || !author) continue;
  1017.  
  1018. let ignored = this.settings.isIgnored(author);
  1019.  
  1020. // render ignore/unignore button
  1021. let link = document.createElement('a');
  1022. link.textContent = ignored ? 'Nebeignoruoti' : 'Ignoruoti';
  1023. link.dataset.author = author;
  1024. link.dataset.ignored = ignored;
  1025. link.href = '#';
  1026. link.onclick = evt => {
  1027. let link = evt.target;
  1028. let author = link.dataset.author;
  1029. let ignored = link.dataset.ignored;
  1030.  
  1031. if (ignored === 'false') {
  1032. if (confirm(`Ar tikrai norite ignoruoti ${author}?`)) {
  1033. this.settings.addIgnored(author);
  1034. location.reload(); // too much to change
  1035. }
  1036. } else {
  1037. if (confirm(`Ar tikrai nebenorite ignoruoti ${author}?`)) {
  1038. this.settings.removeIgnored(author, true);
  1039. location.reload();
  1040. }
  1041. }
  1042.  
  1043. evt.preventDefault();
  1044. };
  1045.  
  1046. let span = post.querySelector('p > span');
  1047.  
  1048. let el = Utils.InsertAfter(span, document.createTextNode('['));
  1049. el = Utils.InsertAfter(el, link);
  1050. Utils.InsertAfter(el, document.createTextNode('] '));
  1051. }
  1052. }
  1053.  
  1054. _clearTopic() {
  1055. let posts = Utils.GetPosts();
  1056.  
  1057. for (let i = 0; i < posts.length; i++) {
  1058. let post = posts[i];
  1059. let authorLink = post.querySelector('p > span > a');
  1060.  
  1061. if (authorLink) {
  1062. let author = authorLink.textContent;
  1063. let content = post.querySelector('td.forumpost[id^="post_"]');
  1064.  
  1065. // re-retardify posts
  1066. if (author && this.settings.isIgnored(author)) {
  1067. if (this.settings.options.ignoreReplaceMessages) {
  1068. content.innerHTML = this.settings.options.ignoreReplaceString;
  1069. let signatureEl = post.querySelector('p.sig');
  1070. if (signatureEl)
  1071. signatureEl.remove();
  1072. } else
  1073. post.remove();
  1074. }
  1075. }
  1076. }
  1077. }
  1078.  
  1079. renderUserDetails() {
  1080. let authorEl = document.querySelector('td.embedded > h1');
  1081. if (!authorEl) return;
  1082.  
  1083. let author = authorEl.innerText;
  1084. let ignored = this.settings.isIgnored(author);
  1085. let blockEl = document.querySelector('a[href^="friends.php?action=add&type=block&"]');
  1086. let insertEl = document.createElement('a');
  1087.  
  1088. insertEl.innerText = ignored ? 'pašalinti iš ignoravimo' : 'ignoruoti';
  1089. insertEl.href = '#';
  1090.  
  1091. let inserted = Utils.InsertAfter(blockEl.nextSibling, document.createTextNode(' - ('));
  1092. Utils.InsertAfter(inserted, insertEl);
  1093. Utils.InsertAfter(insertEl, document.createTextNode(')'));
  1094.  
  1095. insertEl.onclick = () => {
  1096. if (ignored)
  1097. this.settings.removeIgnored(author, true);
  1098. else
  1099. this.settings.addIgnored(author);
  1100. location.reload();
  1101. };
  1102. }
  1103. }
  1104.  
  1105. //--- modules/lastPosts.js
  1106. class LastPosts extends Base {
  1107. async init() {
  1108. if (!this.settings.options.showLastPosts) return Promise.resolve();
  1109.  
  1110. let bottomEl = document.querySelector('p.tcenter:last-child');
  1111. let lastPostsEl = document.createElement('p');
  1112. lastPostsEl.innerHTML = LoaderTemplate.Render();
  1113.  
  1114. Utils.InsertAfter(bottomEl, lastPostsEl);
  1115.  
  1116. let posts = await Utils.FetchJSON('/forums/lastposts', 'POST', this.settings.getIgnored());
  1117. lastPostsEl.innerHTML = LastPostsTemplate.Render(posts);
  1118.  
  1119. // set width (fuck css, this hack is awesome)
  1120. let forumTable = document.querySelector('#forum');
  1121. let lastPostTable = document.querySelector('#last-posts');
  1122. lastPostTable.width = forumTable.offsetWidth;
  1123.  
  1124. let contentLinks = lastPostTable.querySelectorAll('td[data-post-id]');
  1125.  
  1126. for (let i = 0; i < contentLinks.length; i++) {
  1127. let postId = contentLinks[i].getAttribute('data-post-id');
  1128. let threadId = contentLinks[i].getAttribute('data-thread-id');
  1129.  
  1130. contentLinks[i].addEventListener('click', () => {
  1131. location.href = `/forums.php?action=viewtopic&topicid=${threadId}&page=p${postId}#${postId}`;
  1132. });
  1133. }
  1134. }
  1135. }
  1136.  
  1137. //--- modules/topPosters.js
  1138. class TopPosters {
  1139. async renderTable() {
  1140. document.querySelector('#content > table.main').remove();
  1141. let contentEl = document.querySelector('#content');
  1142. contentEl.innerHTML = LoaderTemplate.Render();
  1143.  
  1144. let top = await Utils.FetchJSON('/forums/top');
  1145.  
  1146. contentEl.innerHTML = TopPostersTemplate.Render(top);
  1147. }
  1148.  
  1149. renderTopLinks() {
  1150. let searchAnchors = document.querySelectorAll('a[href="?action=search"]');
  1151.  
  1152. [...searchAnchors].forEach(searchAnchor => {
  1153. let anchor = document.createElement('a');
  1154. anchor.href = '/forums.php?action=top';
  1155. anchor.innerText = 'Top';
  1156.  
  1157. Utils.InsertBefore(searchAnchor, anchor);
  1158. Utils.InsertAfter(anchor, document.createTextNode(' | '));
  1159. });
  1160. }
  1161. }
  1162.  
  1163. //--- modules/userInfo.js
  1164. class UserInfo {
  1165. constructor(userId) {
  1166. this._userId = userId;
  1167. }
  1168.  
  1169. async renderPostCount() {
  1170. let userInfo = await Utils.FetchJSON(`/users/info/${this._userId}`);
  1171. if (!userInfo) return;
  1172.  
  1173. let rowToAppendAfter = document.querySelector('table.main tr:nth-child(4)');
  1174.  
  1175. let rows = UserInfoTemplate.RenderElement(userInfo);
  1176.  
  1177. rowToAppendAfter = Utils.InsertAfter(rowToAppendAfter, rows[0]);
  1178. Utils.InsertAfter(rowToAppendAfter, rows[1]);
  1179. }
  1180. }
  1181.  
  1182. //--- modules/userPosts.js
  1183. class UserPosts {
  1184. constructor(userId, page) {
  1185. this._userId = userId;
  1186. this._page = page;
  1187. }
  1188.  
  1189. async init() {
  1190. document.querySelector('#content > table.main').remove();
  1191. let contentEl = document.querySelector('#content');
  1192. contentEl.innerHTML = LoaderTemplate.Render();
  1193.  
  1194. let userPosts = await Utils.FetchJSON(`/users/${this._userId}/posts/${this._page > 1 ? this._page : ''}`);
  1195.  
  1196. contentEl.innerHTML = UserPostsTemplate.Render(userPosts);
  1197. }
  1198. }
  1199.  
  1200. //--- modules/originalPost.js
  1201. class OriginalPost {
  1202. constructor() {
  1203. this.dateSince = new Date('2017-05-21');
  1204. }
  1205.  
  1206. init() {
  1207. Utils.GetPosts().forEach(post => {
  1208. if (Utils.GetPostDate(post) < this.dateSince) return;
  1209.  
  1210. let postEditedEl = post.querySelector('.forumpost p.small');
  1211.  
  1212. if (postEditedEl && postEditedEl.textContent.startsWith('Paskutinį kartą redagavo:')) {
  1213. // render 'show original button'
  1214. let postId = Utils.GetPostId(post);
  1215.  
  1216. let showOriginalEl = ShowOriginalPostTemplate.RenderElement(postId);
  1217. Utils.InsertAfter(postEditedEl, showOriginalEl);
  1218.  
  1219. showOriginalEl.onclick = async () => {
  1220. showOriginalEl.onclick = null;
  1221.  
  1222. // too fast to show loader
  1223. // showOriginalEl.innerHTML = LoaderTemplate.Render();
  1224.  
  1225. let origPost = await Utils.FetchJSON(`/forums/post/${postId}`);
  1226. showOriginalEl.innerHTML = ShowOriginalPostTemplate.RenderOriginalPost(origPost);
  1227. };
  1228. }
  1229. });
  1230. }
  1231. }
  1232.  
  1233. //--- modules/userSettings.js
  1234. class UserSettings extends Base {
  1235. init() {
  1236. let self = this;
  1237. let contentEl = document.querySelector('#content');
  1238.  
  1239. let headerEl = UserSettingsTemplate.RenderHeaderElement();
  1240. let settingsEl = UserSettingsTemplate.RenderElement(this.settings);
  1241.  
  1242. Utils.InsertAfter(contentEl.querySelector('table'), headerEl);
  1243. Utils.InsertAfter(headerEl, settingsEl);
  1244.  
  1245. // button remove ignored
  1246. let btnRemoveIgnored = settingsEl.querySelector('#blmRemoveIgnoredBtn');
  1247. if (btnRemoveIgnored)
  1248. btnRemoveIgnored.onclick = () => {
  1249. let healedPlebs = Utils.GetSelectedValues(settingsEl.querySelector('#ignoredList'));
  1250.  
  1251. healedPlebs.forEach(pleb => this.settings.removeIgnored(pleb, false));
  1252.  
  1253. let removeOptions = settingsEl.querySelectorAll('#ignoredList > option:checked');
  1254. removeOptions.forEach(opt => opt.remove());
  1255. };
  1256.  
  1257. // button save settings
  1258. let btnSave = settingsEl.querySelector('#blmSaveBtn');
  1259. btnSave.onclick = () => {
  1260. this.settings.save();
  1261. location.reload();
  1262. };
  1263.  
  1264. // checkbox last posts
  1265. let lastPostsEl = settingsEl.querySelector('#showLastPosts');
  1266. lastPostsEl.checked = self.settings.options.showLastPosts;
  1267. lastPostsEl.onchange = () => self.settings.options.showLastPosts = lastPostsEl.checked;
  1268.  
  1269. // checkbox hide topics
  1270. let hideTopicEl = settingsEl.querySelector('#hideTopics');
  1271. hideTopicEl.checked = self.settings.options.ignoreHideTopics;
  1272. hideTopicEl.onchange = () => self.settings.options.ignoreHideTopics = hideTopicEl.checked;
  1273.  
  1274. // checkbox replace messages
  1275. let replaceMessagesEl = settingsEl.querySelector('#replaceMessages');
  1276. replaceMessagesEl.checked = self.settings.options.ignoreReplaceMessages;
  1277. replaceMessagesEl.onchange = () => self.settings.options.ignoreReplaceMessages = replaceMessagesEl.checked;
  1278.  
  1279. // input replace string
  1280. let replaceStringEl = settingsEl.querySelector('#replaceString');
  1281. replaceStringEl.value = self.settings.options.ignoreReplaceString;
  1282. replaceStringEl.onchange = () => self.settings.options.ignoreReplaceString = replaceStringEl.value;
  1283. }
  1284. }
  1285.  
  1286. //--- modules/userMention.js
  1287. // class UserMention {
  1288. // constructor(textArea) {
  1289. // this._textArea = textArea;
  1290. // textArea.oninput = evt => {
  1291. // let entered = textArea.value[textArea.selectionStart - 1];
  1292. // if (entered === '@') {
  1293. // let prevSymbol = textArea.value[textArea.selectionStart - 2];
  1294. // // check for conditions to display autocomplete
  1295. // if (prevSymbol === undefined || prevSymbol === ' ' || prevSymbol === '\n'
  1296. // || prevSymbol === ']' || prevSymbol === ':' || prevSymbol === '>') {
  1297. // }
  1298. // }
  1299. // };
  1300. // }
  1301. // }
  1302.  
  1303. //--- modules/deletedUsernames.js
  1304. class DeletedUsernames {
  1305. async init() {
  1306. let anchorIdMap = new Map();
  1307.  
  1308. let posts = Utils.GetPosts();
  1309.  
  1310. [...posts].forEach(posts => {
  1311. let anchor = posts.querySelector('p > span > a');
  1312. if (anchor && anchor.text === '') {
  1313. let id = Number(anchor.href.split('?id=')[1]);
  1314.  
  1315. if (anchorIdMap.has(id))
  1316. anchorIdMap.get(id).push(anchor);
  1317. else
  1318. anchorIdMap.set(id, [anchor]);
  1319. }
  1320. });
  1321.  
  1322. if (anchorIdMap.size === 0) return;
  1323.  
  1324. let usernames = await Utils.FetchJSON('/users/usernames', 'POST', [...anchorIdMap.keys()]);
  1325.  
  1326. if (!usernames) return; // yeah time to start defensive programming in case service is unreachable?
  1327.  
  1328. usernames.forEach(u => {
  1329. let anchors = anchorIdMap.get(u.Id);
  1330.  
  1331. anchors.forEach(a => {
  1332. a.text = `~ ${u.Username}`;
  1333. a.removeAttribute('href');
  1334. });
  1335. });
  1336. }
  1337. }
  1338.  
  1339. //--- modules/twitchEmotes.js
  1340. class TwitchEmotes extends Base {
  1341. init() {
  1342. // intercept editMessageSubmit function
  1343. let origSubmitFn = window.editMessageSubmit;
  1344. if (origSubmitFn) {
  1345. window.editMessageSubmit = (form, id) => {
  1346. this.handleSubmit(form);
  1347. origSubmitFn.apply(this, [form, id]);
  1348. };
  1349. }
  1350.  
  1351. document.addEventListener('submit', evt => this.handleSubmit(evt.target));
  1352. }
  1353.  
  1354. handleSubmit(form) {
  1355. let textArea = form.querySelector('textarea');
  1356.  
  1357. textArea.value = textArea.value.replace(TwitchEmotes._EmotesRegex, (match, s1, s2, s3) => `${s1}${this.formImgEl(s2)}${s3}`);
  1358. }
  1359.  
  1360. formEmoteUrl(emote) {
  1361. return `${Settings.ApiUrl}/assets/images/twitch/${emote}.png`;
  1362. }
  1363.  
  1364. formImgEl(emote) {
  1365. return `[img]${this.formEmoteUrl(emote)}[/img]`;
  1366. }
  1367.  
  1368. // generated by tools/RipTwitchEmotes
  1369. static get _EmotesRegex() {
  1370. 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');
  1371. }
  1372. }
  1373.  
  1374. //--- pages/forumPage.js
  1375. class ForumPage {
  1376. init() {
  1377. let lastPosts = new LastPosts();
  1378. lastPosts.init();
  1379.  
  1380. let topPosters = new TopPosters();
  1381. topPosters.renderTopLinks();
  1382. }
  1383. }
  1384.  
  1385. //--- pages/forumViewPage.js
  1386. class ForumViewPage {
  1387. init() {
  1388. // let forumId = Number(location.href.match(/forumid=(\d+)/)[1]);
  1389. //
  1390. let userIgnore = new UserIgnore();
  1391. userIgnore.hideTopics();
  1392. }
  1393. }
  1394.  
  1395. //--- pages/profilePage.js
  1396. class ProfilePage {
  1397. init() {
  1398. let userSettings = new UserSettings();
  1399. userSettings.init();
  1400. }
  1401. }
  1402.  
  1403. //--- pages/replyPage.js
  1404. class ReplyPage {
  1405. init() {
  1406. // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
  1407. // let userMention = new UserMention(replyTextArea);
  1408. //
  1409. let userIgnore = new UserIgnore();
  1410. userIgnore.clearReply();
  1411.  
  1412. let twitchEmotes = new TwitchEmotes();
  1413. twitchEmotes.init();
  1414. }
  1415. }
  1416.  
  1417. //--- pages/replyQuotePage.js
  1418. class ReplyQuotePage {
  1419. init() {
  1420. // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
  1421. // let userMention = new UserMention(replyTextArea);
  1422. //
  1423. let userIgnore = new UserIgnore();
  1424. userIgnore.clearReplyQuote();
  1425.  
  1426. let twitchEmotes = new TwitchEmotes();
  1427. twitchEmotes.init();
  1428. }
  1429. }
  1430.  
  1431. //--- pages/topPage.js
  1432. class TopPage {
  1433. init() {
  1434. let topPosters = new TopPosters();
  1435. topPosters.renderTable();
  1436. }
  1437. }
  1438.  
  1439. //--- pages/torrentDetailPage.js
  1440. class TorrentDetailPage {
  1441. init() {
  1442. let userIgnore = new UserIgnore();
  1443. userIgnore.clearTorrentDetails();
  1444. }
  1445. }
  1446.  
  1447. //--- pages/userDetailsPage.js
  1448. class UserDetailsPage extends Base {
  1449. init() {
  1450. let userDetailsId = Number(location.href.match(new RegExp(/\.php\?id=(\d+)/))[1]);
  1451.  
  1452. if (userDetailsId !== this.settings.ownUserId) {
  1453. let userInfo = new UserInfo(userDetailsId);
  1454. userInfo.renderPostCount();
  1455.  
  1456. let userIgnore = new UserIgnore();
  1457. userIgnore.renderUserDetails();
  1458. }
  1459. }
  1460. }
  1461.  
  1462. //--- pages/userHistoryPage.js
  1463. class UserHistoryPage extends Base {
  1464. init() {
  1465. let userId = Number(location.href.match(/\/userhistory\.php\?action=viewposts&id=(\d+)/)[1]);
  1466.  
  1467. if (this.settings.ownUserId === userId) return;
  1468.  
  1469. let pageNumber;
  1470. // and right about here I went 'fuck it'
  1471. try {
  1472. pageNumber = Number(location.href.match(/page=(\d+)/)[1]);
  1473. }
  1474. catch (ex) {
  1475. pageNumber = 1;
  1476. }
  1477.  
  1478. let userPosts = new UserPosts(userId, pageNumber);
  1479. userPosts.init();
  1480. }
  1481. }
  1482.  
  1483. //--- pages/viewTopicPage.js
  1484. class ViewTopicPage {
  1485. init() {
  1486. // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
  1487. // let userMention = new UserMention(replyTextArea);
  1488. //
  1489. let userIgnore = new UserIgnore();
  1490. userIgnore.clearTopic();
  1491.  
  1492. let originalPost = new OriginalPost();
  1493. originalPost.init();
  1494.  
  1495. let deletedUsernames = new DeletedUsernames();
  1496. deletedUsernames.init(); // yep ignore async we don't care, the train is rolling
  1497.  
  1498. let twitchEmotes = new TwitchEmotes();
  1499. twitchEmotes.init();
  1500. }
  1501. }
  1502.  
  1503. //--- pages/editPage.js
  1504. class EditPage {
  1505. init () {
  1506. // let replyTextArea = document.querySelector('textarea') as HTMLTextAreaElement;
  1507. // let userMention = new UserMention(replyTextArea);
  1508. //
  1509. let userIgnore = new UserIgnore();
  1510. userIgnore.clearReply();
  1511.  
  1512. let twitchEmotes = new TwitchEmotes();
  1513. twitchEmotes.init();
  1514. }
  1515. }
  1516.  
  1517. //--- main.js
  1518. class BetterLM {
  1519. static Init() {
  1520. if (BetterLM.IsReply)
  1521. new ReplyPage().init();
  1522.  
  1523. else if (BetterLM.IsReplyQuote)
  1524. new ReplyQuotePage().init();
  1525.  
  1526. else if (BetterLM.IsViewTopic)
  1527. new ViewTopicPage().init();
  1528.  
  1529. else if (BetterLM.IsForum)
  1530. new ForumPage().init();
  1531.  
  1532. else if (BetterLM.IsForumView)
  1533. new ForumViewPage().init();
  1534.  
  1535. else if (BetterLM.IsUserDetails)
  1536. new UserDetailsPage().init();
  1537.  
  1538. else if (BetterLM.IsUserHistory)
  1539. new UserHistoryPage().init();
  1540.  
  1541. else if (BetterLM.IsTorrentDetail)
  1542. new TorrentDetailPage().init();
  1543.  
  1544. else if (BetterLM.IsProfile)
  1545. new ProfilePage().init();
  1546.  
  1547. else if (BetterLM.IsTop)
  1548. new TopPage().init();
  1549.  
  1550. else if (BetterLM.IsEdit)
  1551. new EditPage().init();
  1552. }
  1553.  
  1554. static get IsReply() {
  1555. return BetterLM._test(/\/forums\.php\?action=reply&topicid=/);
  1556. }
  1557.  
  1558. static get IsViewTopic() {
  1559. return BetterLM._test(/\/forums\.php\?action=viewtopic/);
  1560. }
  1561.  
  1562. static get IsReplyQuote() {
  1563. return BetterLM._test(/\/forums\.php\?action=quotepost&topicid=/);
  1564. }
  1565.  
  1566. static get IsForum() {
  1567. return BetterLM._test(/\/forums\.php$/);
  1568. }
  1569.  
  1570. static get IsForumView() {
  1571. return BetterLM._test(/\/forums\.php\?action=viewforum/);
  1572. }
  1573.  
  1574. static get IsUserDetails() {
  1575. return BetterLM._test(/\/userdetails\.php\?id=/);
  1576. }
  1577.  
  1578. static get IsUserHistory() {
  1579. return BetterLM._test(/\/userhistory\.php\?action=viewposts&id=/);
  1580. }
  1581.  
  1582. static get IsTorrentDetail() {
  1583. return BetterLM._test(/\/details?/) || BetterLM._test(/\/torrent?/);
  1584. }
  1585.  
  1586. static get IsProfile() {
  1587. return BetterLM._test(/\/my.php(?:(\?edited=1)?)$/);
  1588. }
  1589.  
  1590. static get IsTop() {
  1591. return BetterLM._test(/\/forums\.php\?action=top$/);
  1592. }
  1593.  
  1594. static get IsEdit() {
  1595. return BetterLM._test(/\/forums\.php\?action=editpost/);
  1596. }
  1597.  
  1598. static _test(expr) {
  1599. return expr.test(location.href);
  1600. }
  1601. }
  1602.  
  1603. // here we go for the brighter tomorrow
  1604. BetterLM.Init();