From 980d3cef77ada2a714cb80b537ca1be8fba84d16 Mon Sep 17 00:00:00 2001 From: J Mor Date: Mon, 20 Jul 2026 12:02:50 -0400 Subject: [PATCH 01/14] Minor formatting improvements; deprecated unused utility --- src/js/models/structure/Form.ts | 191 ++++++++++++++++---------------- 1 file changed, 97 insertions(+), 94 deletions(-) diff --git a/src/js/models/structure/Form.ts b/src/js/models/structure/Form.ts index 599617d..83e48af 100644 --- a/src/js/models/structure/Form.ts +++ b/src/js/models/structure/Form.ts @@ -8,6 +8,9 @@ import PreparedStructure from "./PreparedStructure"; /** * Form Engine v.2.0 * Simplifies the process of creating complex, visually consistent forms. + * + * @deprecated Currently unused; until it's understood, keep it that way. + * @todo Figure this thing out & use it or remove it. */ export class Form implements PreparedStructure { @@ -23,12 +26,12 @@ export class Form implements PreparedStructure { private readonly inputList: Map>; /** - * Prepares the form structure and settings. - * Use `get()` to return an actual DOM element. - * @param options Form options - * @param content Form elements - * @param onSubmit Form submission callback - */ + * Prepares the form structure and settings. + * Use `get()` to return an actual DOM element. + * @param options Form options + * @param content Form elements + * @param onSubmit Form submission callback + */ public constructor( options: SectionOptions = {}, private readonly content: FormElement[] = [], @@ -59,9 +62,9 @@ export class Form implements PreparedStructure { } /** - * Builds and returns the form DOM element - * @param force If true, ignores any cached data and rebuilds the structure from scratch - */ + * Builds and returns the form DOM element + * @param force If true, ignores any cached data and rebuilds the structure from scratch + */ public render(force = false): JQuery { if (this.created && !force) { return this.element; } @@ -91,9 +94,9 @@ export class Form implements PreparedStructure { } /** - * Resets the elements to their default values. - * Does not include buttons and submit elements - */ + * Resets the elements to their default values. + * Does not include buttons and submit elements + */ public reset(): void { this.inputList.forEach((input) => { const defval = input.attr("defval"); @@ -105,10 +108,10 @@ export class Form implements PreparedStructure { } /** - * Returns a list of elements in the form. - * If no parameter is specified, returns all inputs - * @param names Names of inputs to return - */ + * Returns a list of elements in the form. + * If no parameter is specified, returns all inputs + * @param names Names of inputs to return + */ public getInputList(...names: string[]): Map> { if (names.length == 0) { return this.inputList; } const results: Map> = new Map(); @@ -121,9 +124,9 @@ export class Form implements PreparedStructure { } /** - * An empty, placeholder form. Used to properly space out and center modals - * @param width Form width - */ + * An empty, placeholder form. Used to properly space out and center modals + * @param width Form width + */ public static placeholder(width = 1): JQuery { return new Form( { columns: width, width: width }, @@ -132,10 +135,10 @@ export class Form implements PreparedStructure { } /** - * Creates a form section FormElement based on the provided parameters - * @param options Section configuration - * @param content Form elements - */ + * Creates a form section FormElement based on the provided parameters + * @param options Section configuration + * @param content Form elements + */ public static section(options: SectionOptions = {}, content?: FormElement[]): FormElement { if (!options.name) options.name = Util.ID.make(); if (!options.columns) options.columns = 1; @@ -159,10 +162,10 @@ export class Form implements PreparedStructure { } /** - * Creates a collapsible section FormElement based on the provided parameters - * @param options Section configuration - * @param content Form elements - */ + * Creates a collapsible section FormElement based on the provided parameters + * @param options Section configuration + * @param content Form elements + */ public static accordion(options: SectionOptions & { active?: boolean | number; collapsible?: boolean } = {}, content?: FormAccordionElement[]): FormElement { if (!options.name) options.name = Util.ID.make(); if (!options.columns) options.columns = 1; @@ -228,10 +231,10 @@ export class Form implements PreparedStructure { } /** - * Creates a collapsible section FormElement based on the provided parameters - * @param options Section configuration - * @param content Form elements - */ + * Creates a collapsible section FormElement based on the provided parameters + * @param options Section configuration + * @param content Form elements + */ public static collapse(options: SectionOptions & { title?: string; badge?: JQuery; collapsed?: boolean } = {}, content?: FormElement[], onActivate?: (id: string, state: boolean) => void): FormElement { if (!options.name) options.name = Util.ID.make(); if (!options.columns) options.columns = 1; @@ -281,10 +284,10 @@ export class Form implements PreparedStructure { } /** - * Creates an input FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates an input FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static input(options: InputElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -348,10 +351,10 @@ export class Form implements PreparedStructure { } /** - * Creates an input FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates an input FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static textarea(options: InputElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -416,9 +419,9 @@ export class Form implements PreparedStructure { } /** - * Creates a copy input FormElement based on the provided parameters - * @param options Element configuration - */ + * Creates a copy input FormElement based on the provided parameters + * @param options Element configuration + */ public static copy(options: ElementOptions = {}): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -484,9 +487,9 @@ export class Form implements PreparedStructure { } /** - * Creates a key input FormElement based on the provided parameters - * @param options Element configuration - */ + * Creates a key input FormElement based on the provided parameters + * @param options Element configuration + */ public static key(options: ElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -615,11 +618,11 @@ export class Form implements PreparedStructure { } /** - * Creates a file input FormElement based on the provided parameters - * @param options Element configuration - * TODO Changed function type - * IDEA: Type to accepted MIME types? - */ + * Creates a file input FormElement based on the provided parameters + * @param options Element configuration + * TODO Changed function type + * IDEA: Type to accepted MIME types? + */ public static file(options: ElementOptions & { accept: string }, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -726,10 +729,10 @@ export class Form implements PreparedStructure { } /** - * Creates a button FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates a button FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static button(options: ElementOptions & { "type"?: "submit" | "button" } = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -779,10 +782,10 @@ export class Form implements PreparedStructure { } /** - * Creates a checkbox FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates a checkbox FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static checkbox(options: ElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -843,11 +846,11 @@ export class Form implements PreparedStructure { } /** - * Creates a select FormElement based on the provided parameters - * @param options Element configuration - * @param content Select options data - * @param changed Input change callback - */ + * Creates a select FormElement based on the provided parameters + * @param options Element configuration + * @param content Select options data + * @param changed Input change callback + */ public static select(options: ElementOptions = {}, content?: SelectOptionSet | SelectOptionFunction, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -904,10 +907,10 @@ export class Form implements PreparedStructure { } /** - * Creates a header FormElement based on the provided parameters - * @param text Header text - * @param width Element span - */ + * Creates a header FormElement based on the provided parameters + * @param text Header text + * @param width Element span + */ public static header(text: string, width?: number): FormElement { const $element = FormUtils.makeInputWrapper(undefined, undefined, width); @@ -921,9 +924,9 @@ export class Form implements PreparedStructure { } /** - * Creates a div FormElement based on the provided parameters - * @param options Element configuration - */ + * Creates a div FormElement based on the provided parameters + * @param options Element configuration + */ public static div(options: ElementOptions = {}): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -957,10 +960,10 @@ export class Form implements PreparedStructure { } /** - * Alias for `Form.div` with streamlined options - * @param text Div contents - * @param width Wrapper width - */ + * Alias for `Form.div` with streamlined options + * @param text Div contents + * @param width Wrapper width + */ public static text(text: string, width = 1, wrapper?: string): FormElement { return Form.div({ value: text, width: width, wrapper: wrapper }); } @@ -970,19 +973,19 @@ export class Form implements PreparedStructure { } /** - * Alias for `Form.div` with some pre-built markup - * @param header First line of the header - * @param subheader Second line of the header - * @param width Wrapper width - */ + * Alias for `Form.div` with some pre-built markup + * @param header First line of the header + * @param subheader Second line of the header + * @param width Wrapper width + */ public static subheader(header: string, subheader: string, width = 1, name?: string, wrapper?: string): FormElement { return Form.div({ value: `${header}
${subheader}`, width: width, wrapper: "subheader" + (wrapper ? " " + wrapper : ""), name: name }); } /** - * Creates an hr FormElement based on the provided parameters - * @param width Element width - */ + * Creates an hr FormElement based on the provided parameters + * @param width Element width + */ public static hr(width?: number): FormElement { const $element = FormUtils.makeInputWrapper(undefined, undefined, width); @@ -995,9 +998,9 @@ export class Form implements PreparedStructure { } /** - * Creates an empty spacer element - * @param width Element width - */ + * Creates an empty spacer element + * @param width Element width + */ public static spacer(width?: number, unmargin = false): FormElement { const $element = FormUtils.makeInputWrapper(undefined, undefined, width); @@ -1035,15 +1038,15 @@ export class FormElement { private created = false; /** - * Constructs a form element based on provided data. - * Only the `wrapper` parameter is required, everything else is optional. - * @param wrapper Element that wraps the rest of the inputs - * @param input Input element, if there is one - * @param label Input label, if there is one - * @param content Contains other FormElements that need to be built and appended to this element - * @param container Container object for content. If omitted, defaults to wrapper - * @param postProcessing Function to run after the object has been built. Takes the wrapper as a parameter. - */ + * Constructs a form element based on provided data. + * Only the `wrapper` parameter is required, everything else is optional. + * @param wrapper Element that wraps the rest of the inputs + * @param input Input element, if there is one + * @param label Input label, if there is one + * @param content Contains other FormElements that need to be built and appended to this element + * @param container Container object for content. If omitted, defaults to wrapper + * @param postProcessing Function to run after the object has been built. Takes the wrapper as a parameter. + */ public constructor( private readonly wrapper: JQuery, private readonly input?: JQuery, From b9b186936739b88d57f941b2e9de7e161fc2ae57 Mon Sep 17 00:00:00 2001 From: J Mor Date: Mon, 20 Jul 2026 12:45:22 -0400 Subject: [PATCH 02/14] Made config more data-driven The domains, runtime dependency version numbers, & more are now derived from a universal source of truth, making downstream repositories for other sites more manageable & consistent. --- bin/userscript-header.js | 80 +++++++++++++++++++++++++++++---- package-lock.json | 4 +- package.json | 6 ++- src/REMT.ts | 2 +- src/js/models/api/XMConnect.ts | 10 ++--- src/js/models/data/Post.ts | 11 ++--- src/js/models/data/Records.ts | 6 ++- src/js/models/data/Script.ts | 35 ++++++++++++--- src/js/utilities/UtilNetwork.ts | 56 ++++++++++++++++++++--- 9 files changed, 174 insertions(+), 36 deletions(-) diff --git a/bin/userscript-header.js b/bin/userscript-header.js index 0eca786..e6f3fc0 100644 --- a/bin/userscript-header.js +++ b/bin/userscript-header.js @@ -1,4 +1,69 @@ const pkg = require('../package.json'); +const pkgLock = (() => { + try { + return require('../package-lock.json'); + } catch (error) { + return undefined; + } +})(); +const fs = require('node:fs'); +const bunLock = (() => { + try { + const contents = fs.readFileSync("../bun.lock", { encoding: "utf-8" }); + return JSON.parse(contents.replace(/,$(?=\s+[}\]])/gm, "")); + } catch (error) { + return undefined; + } +})(); +// It's an indirect dependency, but it's not required. +const chalk = (() => { + try { + return require("chalk"); + } catch (error) { + return undefined; + } +})(); +function tryChalk(funcName, text) { + return chalk?.[funcName]?.(text) ?? text; +} + +const fallbacks = { + "jquery": "3.6.0", + "jquery-ui": "1.12.1", + "@re621/zestyapi": "latest", + "jquery.hotkeys": "0.2.0", +} +/** + * Attempts to retrieve the version of the dependency directly from the package + * manager's lockfile. + * + * Ensures the dependency referenced by the script is exactly the same as the + * one used in development; prior to this, both the `jquery-ui` & + * `jquery.hotkeys` dependencies were actually referencing a newer & older + * version (respectively) in the development environment than the version + * requested by the userscript with the previously static `require` strings. + * + * Attempts to pull from both an NPM `package-lock.json` file and a Bun + * `bun.lock` file to make a potential future package manager change smoother. + * @param {string} key + * @returns {string} + */ +function getDependencyVersion(key) { + console.info("Attempting to retrieve version info for %s; starting with 'package-lock.json'...", key); + const fromPkgLock = pkgLock?.packages?.[`node_modules/${key}`]?.version; + const fromBunLock = bunLock?.packages?.[key]?.[0]?.match(new RegExp(`^${RegExp.escape(key)}@([0-9.]+)$`))?.[1]; + if (fromPkgLock) { + if (fromBunLock && fromPkgLock !== fromBunLock) console.warn(tryChalk("yellowBright", "[WARNING]") + ": version from 'package-lock.json' (%s) & 'bun.lock' (%s) are not the same; if you're using Bun for package installation, be sure to update 'package-lock.json' with NPM before submitting a PR.", fromPkgLock, fromBunLock); + return fromPkgLock; + } + console.warn("Failed to find version from 'package-lock.json'..."); + + if (fromBunLock) return fromBunLock; + console.warn("Failed to find version from 'bun.lock'..."); + const fromFallbacks = fallbacks[key]; + if (fromFallbacks) return fromFallbacks; + throw new Error(`Failed to find version for ${key}`); +} module.exports = { name: pkg.displayName, @@ -16,16 +81,13 @@ module.exports = { updateURL: pkg.homepage + "/releases/latest/download/script.meta.js", downloadURL: pkg.homepage + "/releases/latest/download/script.user.js", - match: [ - "https://e621.net/*", - "https://e926.net/*", - ], + match: pkg.config.domains.map(e => `https://${e}/*`), require: [ - "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js", - "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js", - "https://cdn.jsdelivr.net/npm/@re621/zestyapi@latest/dist/ZestyAPI.min.js", - "https://cdnjs.cloudflare.com/ajax/libs/jquery.hotkeys/0.2.0/jquery.hotkeys.min.js", + `https://cdnjs.cloudflare.com/ajax/libs/jquery/${getDependencyVersion("jquery")}/jquery.min.js`, + `https://cdnjs.cloudflare.com/ajax/libs/jqueryui/${getDependencyVersion("jquery-ui")}/jquery-ui.min.js`, + `https://cdn.jsdelivr.net/npm/@re621/zestyapi@${getDependencyVersion("@re621/zestyapi")}/dist/ZestyAPI.min.js`, + `https://cdnjs.cloudflare.com/ajax/libs/jquery.hotkeys/${getDependencyVersion("jquery.hotkeys")}/jquery.hotkeys.min.js`, ], grant: [ @@ -44,7 +106,7 @@ module.exports = { ], connect: [ - "static1.e621.net", + ...(pkg.config.staticDomains ?? pkg.config.domains.map(e => `static1.${e}`)), "*", ], diff --git a/package-lock.json b/package-lock.json index faac112..faadf38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mod621", - "version": "2.1.1", + "version": "6.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mod621", - "version": "2.1.1", + "version": "6.5.0", "license": "GPL-3.0", "dependencies": { "@re621/zestyapi": "^0.1.13", diff --git a/package.json b/package.json index 9506c85..47f10d0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mod621", "displayName": "RE621 Mod Tools", "namespace": "mod621.bitwolfy.com", - "version": "6.4.1", + "version": "6.5.0", "description": "Moderation toolkit for e621.net", "author": "bitWolfy", "contributors": [ @@ -14,6 +14,10 @@ "homepage": "https://github.com/re621/re621.ModTools", "funding": "https://ko-fi.com/bitwolfy", "repository": "github:re621/re621.ModTools", + "config": { + "domains": ["e621.net", "e926.net"], + "staticDomains": ["static1.e621.net", "static1.e926.net"] + }, "scripts": { "push": "npm version patch && git push --follow-tags", "push-minor": "npm version minor && git push --follow-tags", diff --git a/src/REMT.ts b/src/REMT.ts index 56b94fc..55f0be8 100644 --- a/src/REMT.ts +++ b/src/REMT.ts @@ -58,7 +58,7 @@ export default class REMT { // Set up the API connection // TODO: Temporary instantiation method REMT.API = window["ZestyAPI"].connect({ - userAgent: Script.userAgent, + userAgent: Util.Network.userAgent, debug: Debug.Connect, }); diff --git a/src/js/models/api/XMConnect.ts b/src/js/models/api/XMConnect.ts index b282053..5711722 100644 --- a/src/js/models/api/XMConnect.ts +++ b/src/js/models/api/XMConnect.ts @@ -1,4 +1,4 @@ -import Script from "../data/Script"; +import Util from "../../utilities/Util"; import Debug from "../Debug"; declare const GM_xmlhttpRequest: any; @@ -55,8 +55,8 @@ export default class XMConnect { private static validateXHRDetails(details: XMConnectRequest): XMConnectRequest { if (details.headers === undefined) details.headers = {}; if (details.headers["User-Agent"] === undefined) { - details.headers["User-Agent"] = Script.userAgent; - details.headers["X-User-Agent"] = Script.userAgent; + details.headers["User-Agent"] = Util.Network.userAgent; + details.headers["X-User-Agent"] = Util.Network.userAgent; } if (details.onabort === undefined) details.onabort = (): void => { return; }; @@ -82,8 +82,8 @@ export default class XMConnect { } if (a.headers === undefined) a.headers = { - "User-Agent": Script.userAgent, - "X-User-Agent": Script.userAgent, + "User-Agent": Util.Network.userAgent, + "X-User-Agent": Util.Network.userAgent, }; if (a.onerror === undefined) a.onerror = (): void => { return; } diff --git a/src/js/models/data/Post.ts b/src/js/models/data/Post.ts index 6d13855..4e76317 100644 --- a/src/js/models/data/Post.ts +++ b/src/js/models/data/Post.ts @@ -1,6 +1,7 @@ import APIPost from "@re621/zestyapi/dist/responses/APIPost"; import Util from "../../utilities/Util"; import { Tag } from "./Tag"; +import Script from "./Script"; export default class Post { @@ -293,7 +294,7 @@ export default class Post { md5: data.post.file.md5, original: data.post.file.url, sample: data.post.sample.url, - preview: `https://static1.e621.net/data/preview/${data.post.file.md5.substring(0, 2)}/${data.post.file.md5.substring(2, 4)}/${data.post.file.md5}.jpg`, + preview: `https://${Script.primaryStaticDomain}/data/preview/${data.post.file.md5.substring(0, 2)}/${data.post.file.md5.substring(2, 4)}/${data.post.file.md5}.jpg`, size: data.post.file.size, }, @@ -561,14 +562,14 @@ export default class Post { switch (type) { case "original": - return `https://static1.e621.net/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` + return `https://${Script.primaryStaticDomain}/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` case "sample": return ((options.width < 850 || options.height < 850 || options.extension == "gif") - ? `https://static1.e621.net/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` - : `https://static1.e621.net/data/sample/${parts[0]}/${parts[1]}/${parts[2]}.jpg`); + ? `https://${Script.primaryStaticDomain}/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` + : `https://${Script.primaryStaticDomain}/data/sample/${parts[0]}/${parts[1]}/${parts[2]}.jpg`); case "preview": default: { - return `https://static1.e621.net/data/preview/${parts[0]}/${parts[1]}/${parts[2]}.jpg`; + return `https://${Script.primaryStaticDomain}/data/preview/${parts[0]}/${parts[1]}/${parts[2]}.jpg`; } } } diff --git a/src/js/models/data/Records.ts b/src/js/models/data/Records.ts index 754a202..b49bc24 100644 --- a/src/js/models/data/Records.ts +++ b/src/js/models/data/Records.ts @@ -1,3 +1,5 @@ +import Script from "./Script"; + export default class Records { @@ -79,7 +81,7 @@ export default class Records { rules: "sitetools", }, underage: { - text: "You need to be 18 to access this page.\nThis ban won't expire, but if you have reached the sufficient age you can request your ban to be lifted by writing an email to management@e621.net.", + text: `You need to be 18 to access this page.\nThis ban won't expire, but if you have reached the sufficient age you can request your ban to be lifted by writing an email to management@${Script.primaryDomain}.`, rules: "underage", }, }; @@ -312,7 +314,7 @@ export default class Records { group: this.RuleGroups.illegal, preface: "By using e621 and/or registering an account, you agree that you are at least eighteen years of age. No individual under the age of eighteen may register an account, use our services, provide any personal information to e621, or otherwise upload or transmit personal information through our services.", rules: [], - postface: "Even though laws may vary depending on your location, e621.net's management and service office is located in Arizona, USA, and it is those laws that we are required to abide by. We cannot knowingly allow any minors to view pornographic content, so we must be proactive in preventing underage access. This includes accounts that are either legally or illegally accessed by minors. Furthermore, we require official, government-issued photo identification as proof of you turning eighteen in order to release access to your account. Please email us at `management@e621.net` for further details.", + postface: `Even though laws may vary depending on your location, ${Script.primaryDomain}'s management and service office is located in Arizona, USA, and it is those laws that we are required to abide by. We cannot knowingly allow any minors to view pornographic content, so we must be proactive in preventing underage access. This includes accounts that are either legally or illegally accessed by minors. Furthermore, we require official, government-issued photo identification as proof of you turning eighteen in order to release access to your account. Please email us at \`management@${Script.primaryDomain}\` for further details.`, }, }; diff --git a/src/js/models/data/Script.ts b/src/js/models/data/Script.ts index 66496ea..ad3e0f4 100644 --- a/src/js/models/data/Script.ts +++ b/src/js/models/data/Script.ts @@ -1,14 +1,16 @@ +import { UtilNetwork } from '../../utilities/UtilNetwork'; /** * Basic script parameters, both hard-coded and derived from the userscript header. * GM_info is used here because of XM.Connect initialization issues. */ export default class Script { - public static version = GM_info.script.version; - public static handler = { + public static readonly displayName = GM_info.script.name; + public static readonly version = GM_info.script.version; + public static readonly handler = Object.freeze({ name: GM_info.scriptHandler, version: GM_info.version, - } - public static url = { + }); + public static readonly url = { website: "https://re621.app", repo: "https://github.com/re621/re621.ModTools", issues: "https://github.com/re621/re621.ModTools/issues", @@ -16,11 +18,34 @@ export default class Script { latest: "https://api.github.com/repos/re621/re621.ModTools/releases/latest", kofi: "https://ko-fi.com/bitWolfy", } - public static userAgent = "re621.ModTools/" + this.trimVersion(Script.version); + /** + * Changes the script's name to match the pre-established User Agent format. + * E.g. If the package's name (& consequently script's name) is `RE621 Mod Tools`, that becomes `re621.ModTools`. + * @deprecated Use {@link UtilNetwork.projectNameFormatted} instead. + */ + private static projectNameFormatted = this.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, (e) => e.toUpperCase()) : e.toLowerCase() + "."), ""); + + /** + * + * @deprecated Use {@link UtilNetwork.userAgent} instead. + */ + public static readonly userAgent = this.projectNameFormatted + this.trimVersion(Script.version); + + /** + * @deprecated Use {@link UtilNetwork.trimVersion} instead + */ private static trimVersion(value: string): string { const match = value.match(/(\d\.\d+)\.\d+/); if (!match || !match[1]) return "0.0"; return match[1]; } + + public static readonly domains = GM_info.script.matches.map(e => e.match(/^https:\/\/(.+)\/\*/)?.[1]).reduce((p,e) => { + if (e) p.push(e); + return p; + }, []); + public static readonly primaryDomain = this.domains[0] ?? "e621.net"; + public static readonly staticDomains = GM_info.script.options.override.orig_connects.filter(e => e && e !== "*"); + public static readonly primaryStaticDomain = this.staticDomains[0] ?? `static1.${this.primaryDomain}`; } diff --git a/src/js/utilities/UtilNetwork.ts b/src/js/utilities/UtilNetwork.ts index d1d7027..3bb40c4 100644 --- a/src/js/utilities/UtilNetwork.ts +++ b/src/js/utilities/UtilNetwork.ts @@ -1,8 +1,15 @@ import REMT from "../../REMT"; import XM from "../models/api/XM"; +import Script from "../models/data/Script"; export class UtilNetwork { + /** + * + * @returns Promise resolving to true if connected to the internet and false otherwise. + * @deprecated Is unused here & was [removed from re621 itself + * ](https://github.com/re621/re621.Legacy/commit/273ce5d1dd2c5bd3e1e27f96f0c065175cd9a876). + */ public static async isOnline(): Promise { // Not terribly reliable. @@ -11,25 +18,62 @@ export class UtilNetwork { // not to the internet as a whole, this will not work. if (!navigator.onLine) return Promise.resolve(false); - // Fallback method - // Tries to make a HEAD request to e621.net, and checks if it works + // Fallback method: + // Tries to make a HEAD request to the site, and checks if it works. return new Promise((resolve) => { XM.Connect.xmlHttpRequest({ method: "HEAD", - url: window.location.host == "e621.net" ? "https://e621.net/" : "https://e926.net/", + url: (window.location.origin !== "null" ? + window.location.origin : + "https://" + window.location.host) + "/", onerror: () => { resolve(false); }, onload: () => { resolve(true); }, }); }) } + /** + * Changes the script's name to match the pre-established User Agent format. + * E.g. If the package's name (& consequently script's name) is `RE621 Mod Tools`, that becomes `re621.ModTools`. + */ + private static projectNameFormatted = Script.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, (e) => e.toUpperCase()) : e.toLowerCase() + "."), ""); + + public static userAgent = this.projectNameFormatted + this.trimVersion(Script.version); + + private static trimVersion(value: string): string { + const match = value.match(/(\d\.\d+)\.\d+/); + if (!match || !match[1]) return "0.0"; + return match[1]; + } + + public static get authToken() { + return REMT.API.getAuthToken() ?? document.querySelector("meta[name=csrf-token]")?.getAttribute("content") ?? "~~FAILED TO GET TOKEN~~"; + } + + /** + * The value for the `Authorization` header or undefined if not available. + * Will likely never be available in a browser context. + */ + public static get authHeader() { + const authLogin = REMT.API.getAuthLogin(); + return authLogin ? `Basic ${btoa(authLogin.username + ":" + authLogin.apiKey)}` : undefined; + } + /** * Gives a User Agent & the required header for an authenticated GET request. */ public static get simpleAuthHeaders() { - return { - "User-Agent": `${XM.Info.script.name}/${XM.Info.script.version} (by ${XM.Info.script.author} on e621)`, - "X-CSRF-Token": REMT.API.getAuthToken() ?? document.querySelector("meta[name=csrf-token]")?.getAttribute("content") ?? "~~FAILED TO GET TOKEN~~", + const base: { + "User-Agent": string, + "X-CSRF-Token": string, + [k: string]: string, + } = { + "User-Agent": this.userAgent, + "X-CSRF-Token": this.authToken, }; + const authHeader = this.authHeader; + if (authHeader) + base["Authorization"] = authHeader; + return base; } } From a4cc5d58c7a60a0dbc2b1516cd3ca6afd67843de Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 11:42:20 -0400 Subject: [PATCH 03/14] Added the icon to the package config --- bin/userscript-header.js | 4 ++-- package.json | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/userscript-header.js b/bin/userscript-header.js index e6f3fc0..08f14ce 100644 --- a/bin/userscript-header.js +++ b/bin/userscript-header.js @@ -75,8 +75,8 @@ module.exports = { homepageURL: pkg.homepage, supportURL: pkg.homepage + "/issues", - icon: "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", - icon64: "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", + icon: pkg.config.iconUri ?? pkg.config.icon64Uri, + icon64: pkg.config.icon64Uri ?? pkg.config.iconUri, updateURL: pkg.homepage + "/releases/latest/download/script.meta.js", downloadURL: pkg.homepage + "/releases/latest/download/script.user.js", diff --git a/package.json b/package.json index 47f10d0..b5371ce 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "funding": "https://ko-fi.com/bitwolfy", "repository": "github:re621/re621.ModTools", "config": { + "iconUri": "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", + "icon64Uri": "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", "domains": ["e621.net", "e926.net"], "staticDomains": ["static1.e621.net", "static1.e926.net"] }, From 9f821b2978866392171c316350aa70b01e97755e Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 11:44:59 -0400 Subject: [PATCH 04/14] Made the modified name used by the injector modify instead of overwrite the value from the config. --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 13a70a3..f00d3d5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,7 +64,7 @@ files.push({ }); if (isDev) { - metadata.name = "RE621 ModTools Injector"; + metadata.name += " Injector"; metadata.version = "10.0.0"; delete metadata.updateURL; delete metadata.downloadURL; From 97e718335a0857248e753d9adbdb2a85dc195dec Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 12:21:23 -0400 Subject: [PATCH 05/14] Added SemVer parser utility function --- src/js/utilities/Util.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/js/utilities/Util.ts b/src/js/utilities/Util.ts index 7704777..ae2ff41 100644 --- a/src/js/utilities/Util.ts +++ b/src/js/utilities/Util.ts @@ -265,6 +265,33 @@ export default class Util { return 0; } + /** + * Parses the given string according to the SemVer format into an object with + * convenient formatting & access fields. + * + * @todo Options for strictness + * @todo Option for `toString() { return this.raw; }` + */ + public static parseVersion(v: string) { + const m = /(\d+)\.(\d+)\.(\d+)(?:-([-a-zA-Z0-9]+))?(?:\+([-a-zA-Z0-9]+))?/.exec(v); + return { + raw: v, + full: m?.[0] ?? "0.0.0", + major: m?.[1] ?? "0", + minor: m?.[2] ?? "0", + patch: m?.[3] ?? "0", + preRelease: m?.[4] ?? "", + build: m?.[5] ?? "", + didParseFail: !m, + format(strings: TemplateStringsArray, ...values: readonly ("full" | "major" | "minor" | "patch" | "preRelease" | "build")[]) { + let r = strings[0]; + for (let i = 0; i < values.length; r += this[values[i]] + strings[++i]) {} + return r; + }, + }; + } + // #endregion Versioning + /** * Formats an array into a string via a non-standard join method. * Ex. ["one"] => "one" From ede839a2204167d1d97e99a99599ed9129147835 Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 12:30:14 -0400 Subject: [PATCH 06/14] Formatting, documentation, & editor config improvements --- .vscode/settings.json | 3 + .vscode/snippets.code-snippets | 2 +- README.md | 4 + src/js/components/AutoClickPosts.ts | 3 +- src/js/models/data/Page.ts | 75 +++++++-------- src/js/models/data/Post.ts | 8 +- src/js/models/data/Script.ts | 9 +- src/js/models/structure/Modal.ts | 40 ++++---- src/js/utilities/Util.ts | 141 ++++++++++++++-------------- src/js/utilities/UtilIcons.ts | 2 +- 10 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 README.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 0b35d26..5c257be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,8 @@ "dmail", "dmails", "dnpcache", + "downvotes", + "downvoting", "Dragonfruit", "dtext", "Earlopain", @@ -55,6 +57,7 @@ "onshift", "paysite", "Pixiv", + "postface", "postid", "postinghacks", "postingunreleasedcontent", diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index 5ca9d13..57ef74a 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -13,7 +13,7 @@ // "description": "Log output to console" // } "New Component File": { - "description": "A new re621.ModTools component", + "description": "A new Mod Tools Component", "isFileTemplate": true, "prefix": ["component"], "body": [ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c301d68 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +A collection of utilities for using and moderating the booru site e621. + +## Downstream projects +Take a careful look at `package.json`, `bin/userscript-header.js`, & `src/js/models/data/Script.ts`; these 3 points are the main source of project-specific configuration. If done properly, modifying these 3 files for your project's needs should minimize merge conflicts with this project. \ No newline at end of file diff --git a/src/js/components/AutoClickPosts.ts b/src/js/components/AutoClickPosts.ts index ab2f584..1a76884 100644 --- a/src/js/components/AutoClickPosts.ts +++ b/src/js/components/AutoClickPosts.ts @@ -4,7 +4,8 @@ import { html } from "../utilities/HtmlTemplate"; import Component from "./Component"; /** - * @todo What does this do? + * Automates clicking posts on the post search page; combined with the vanilla + * `Mode` selector, allows automating batch actions. */ export default class AutoClickPosts extends Component { private static readonly modeBoxSelector = "#mode-box"; diff --git a/src/js/models/data/Page.ts b/src/js/models/data/Page.ts index 6e56193..14f37b5 100644 --- a/src/js/models/data/Page.ts +++ b/src/js/models/data/Page.ts @@ -1,3 +1,8 @@ +/** + * Utility for parsing the current page's URL. + * + * @todo Refactor to kill singleton; just use `window.location` directly. + */ export default class Page { private static instance: Page; @@ -9,12 +14,12 @@ export default class Page { } /** - * Checks if the url the user is currently on satisfies the filter - * @param filter Pipe separated list of filters the current location has to satisfy - * Matches are by default on a startsWith basis, but if the url must match - * you can prepend = - * @returns true if at least on filter is fulfilled - */ + * Checks if the url the user is currently on satisfies the filter + * @param filter Pipe separated list of filters the current location has to satisfy + * Matches are by default on a startsWith basis, but if the url must match + * you can prepend = + * @returns true if at least on filter is fulfilled + */ public static matches(filter: RegExp | RegExp[]): boolean { if (filter instanceof RegExp) filter = [filter]; const pathname = this.getInstance().url.pathname.replace(/[/?]$/g, ""); @@ -31,35 +36,35 @@ export default class Page { } /** - * Returns the query parameter, or null if the key does not exist - * @return string Query parameter - */ + * Returns the query parameter, or null if the key does not exist + * @return string Query parameter + */ public static getQueryParameter(key: string): string | null { return this.getInstance().url.searchParams.get(key); } /** - * Returns true if the search parameters has the provided key, false otherwise - * @param key - */ + * Returns true if the search parameters has the provided key, false otherwise + * @param key + */ public static hasQueryParameter(key: string): boolean { return this.getInstance().url.searchParams.has(key); } /** - * Sets a query parameter in the current url - * If there is already one with the same key, it will get overridden - * @param key - * @param value - */ + * Sets a query parameter in the current url + * If there is already one with the same key, it will get overridden + * @param key + * @param value + */ public static setQueryParameter(key: string, value: string): void { this.getInstance().url.searchParams.set(key, value); this.refreshCurrentUrl(); } /** - * Removes a querystring from the url - */ + * Removes a querystring from the url + */ public static removeQueryParameter(keys: string | string[]): void { if (!Array.isArray(keys)) keys = [keys]; for (const key of keys) @@ -68,8 +73,8 @@ export default class Page { } /** - * Replaces the current url without reloading or pushing the old one to the history - */ + * Replaces the current url without reloading or pushing the old one to the history + */ private static refreshCurrentUrl(): void { const url = this.getInstance().url; const searchPrefix = url.searchParams.toString().length === 0 ? "" : "?"; @@ -77,33 +82,34 @@ export default class Page { } /** - * Returns the name of the current site - * @returns e621 or e926 - */ + * Returns the name of the current site + * @returns The current site's second level (& lower, if present) domain(s). + * E.g. `e621` or `e926`. + */ public static getSiteName(): string { return this.getInstance().url.hostname.replace(/\.net/g, ""); } /** - * Returns the ID from the second part of the pathname - */ + * Returns the ID from the second part of the pathname + */ public static getPageID(): string { return this.getInstance().url.pathname.split("/")[2]; } /** - * Returns a singleton instance of the class - * @returns Url instance - */ + * Returns a singleton instance of the class + * @returns Url instance + */ public static getInstance(): Page { if (this.instance === undefined) this.instance = new Page(); return this.instance; } /** - * Returns the type of the page, according to the definitions below - * @returns Page type, as a string - */ + * Returns the type of the page, according to the definitions below + * @returns Page type, as a string + */ public static getPageName(): string | null { for (const [name, value] of Object.entries(PageDefinition as RegExpList)) { if (typeof value.test == "function") @@ -237,11 +243,6 @@ export const PageDefinition = { post: /^\/post_votes\/?/, }, - // staff_notes: { - // comment: /^\/comment_votes\/?/, - // post: /^\/post_votes\/?/, - // }, - // Custom pluginSettings: /^\/plugins\/remt.*/, }; diff --git a/src/js/models/data/Post.ts b/src/js/models/data/Post.ts index 4e76317..150af97 100644 --- a/src/js/models/data/Post.ts +++ b/src/js/models/data/Post.ts @@ -771,10 +771,10 @@ interface PostDataTypeC { url: string, }, sample: { - has: true, - height: 637, - width: 850, - url: "https://static1.e621.net/data/sample/33/6e/336ec494422113f0aaf5e879470603ac.jpg", + has: boolean, + height: number, + width: number, + url: string, alternates?: { "720p"?: AlternateType, "480p"?: AlternateType, diff --git a/src/js/models/data/Script.ts b/src/js/models/data/Script.ts index ad3e0f4..acc54ae 100644 --- a/src/js/models/data/Script.ts +++ b/src/js/models/data/Script.ts @@ -1,7 +1,14 @@ import { UtilNetwork } from '../../utilities/UtilNetwork'; /** - * Basic script parameters, both hard-coded and derived from the userscript header. + * Basic script parameters (both hard-coded and derived from the userscript + * header), & miscellaneous site-specific configuration. + * * GM_info is used here because of XM.Connect initialization issues. + * + * If not directly modified by downstream maintainers, expects the following from the script metadata: + * * `GM_info.script.name`: At least 2 space-separated terms. + * * `GM_info.script.version`: A proper SemVer version; can have any prefix. + * * `GM_info.script.homepage`: A proper GitHub repository URL. */ export default class Script { public static readonly displayName = GM_info.script.name; diff --git a/src/js/models/structure/Modal.ts b/src/js/models/structure/Modal.ts index 066b8e9..1adfc10 100644 --- a/src/js/models/structure/Modal.ts +++ b/src/js/models/structure/Modal.ts @@ -100,10 +100,10 @@ export default class Modal { position: { my: "center", at: "center" }, }; */ /** - * Parses the configuration and sets the default values for missing entries - * @param config Configuration to parse - * @todo Convert to using `Object.assign` with a default object instead. - */ + * Parses the configuration and sets the default values for missing entries + * @param config Configuration to parse + * @todo Convert to using `Object.assign` with a default object instead. + */ private validateConfig(config: ModalConfig): RequiredModalConfig | Required { // const result: ModalConfig = Object.assign({}, Modal.defaultConfig, config); @@ -134,26 +134,26 @@ export default class Modal { } /** - * Appends more content to the modal - * @param $content Content to add - */ + * Appends more content to the modal + * @param $content Content to add + */ public addContent($content: JQuery): void { this.$modal.append($content); } /** - * Sets the modal content - * @param $content Content to add - */ + * Sets the modal content + * @param $content Content to add + */ public setContent($content: JQuery): void { this.$modal.html(""); this.$modal.append($content); } /** - * Listens to the specified element in order to trigger the modal - * @param trigger Element-event pair to listen to - */ + * Listens to the specified element in order to trigger the modal + * @param trigger Element-event pair to listen to + */ public registerTrigger(trigger: ModalTrigger | ModalTrigger[]): void { if (!trigger) return; else if (Array.isArray(trigger)) { @@ -194,23 +194,25 @@ export default class Modal { /** - * Completely and irreversibly destroys the modal window - */ + * Completely and irreversibly destroys the modal window + */ public destroy(): void { this.$modal.dialog("destroy"); this.$modal.remove(); } /** - * Returns the element that triggered the modal - * @returns JQuery trigger - */ + * Returns the element that triggered the modal + * @returns JQuery trigger + */ public getActiveTrigger(): JQuery | undefined { return this.activeTrigger; } /** - * This class doesn't work unless re621 has added the `#modal-container` element, so this manually ensures it's added to enable it to work w/o re621. + * This class doesn't work unless the companion script has added the + * `#modal-container` element, so this manually ensures it's added to enable + * it to work w/o the companion script. */ private static ensureModalContainer() { if (!document.querySelector("#modal-container")) { diff --git a/src/js/utilities/Util.ts b/src/js/utilities/Util.ts index ae2ff41..16e7599 100644 --- a/src/js/utilities/Util.ts +++ b/src/js/utilities/Util.ts @@ -24,10 +24,10 @@ export default class Util { public static SS = window.sessionStorage; /** - * Downloads the provided object as a JSON file - * @param data Object to download - * @param file File name - */ + * Downloads the provided object as a JSON file + * @param data Object to download + * @param file File name + */ public static downloadAsJSON(data: any, file: string): void { const tempLink = $("") .attr({ @@ -40,24 +40,24 @@ export default class Util { } /** - * Returns a promise that is fulfilled after the specified time period elapses - * @param time Time period, in milliseconds - */ + * Returns a promise that is fulfilled after the specified time period elapses + * @param time Time period, in milliseconds + */ public static async sleep(time: number): Promise { return new Promise((resolve) => { setTimeout(() => { resolve(); }, time) }); } /** - * Split the array into chunks of specified size. - * The `method` parameter defines how the array is split. - * - "balance": the chunks are at most `size` big, but otherwise of equal size - * - "chunk": all chunks except for the last one of equal size - * - "split": first chunk is `size` big, the second contains the remainder - * @param input Original array - * @param size Size of the resulting chunks - * @param method Method by which the array is split - * @returns Array of smaller arrays of specified size - */ + * Split the array into chunks of specified size. + * The `method` parameter defines how the array is split. + * - "balance": the chunks are at most `size` big, but otherwise of equal size + * - "chunk": all chunks except for the last one of equal size + * - "split": first chunk is `size` big, the second contains the remainder + * @param input Original array + * @param size Size of the resulting chunks + * @param method Method by which the array is split + * @returns Array of smaller arrays of specified size + */ public static chunkArray(input: T[] | Set, size: number, method: "balance" | "chunk" | "split" = "balance"): T[][] { if (!Array.isArray(input)) input = Array.from(input); const result: T[][] = []; @@ -83,11 +83,11 @@ export default class Util { } /** - * Returns the indexes of all instances of the specified value in an array - * @param input Array to search - * @param value Value to look for - * @returns Array of number indexes - */ + * Returns the indexes of all instances of the specified value in an array + * @param input Array to search + * @param value Value to look for + * @returns Array of number indexes + */ public static getArrayIndexes(input: T[], value: T): number[] { const indexes: number[] = []; let i = 0; @@ -98,11 +98,13 @@ export default class Util { } /** - * Convert markdown input into html. - * Very limited in scope, don't rely on this for anything important. - * @param input Markdown input - * @returns HTML output - */ + * Convert Markdown input into HTML. + * + * Very limited in scope, don't rely on this for anything important. + * @param input Markdown input + * @returns HTML output + * @todo Escape HTML characters? + */ public static quickParseMarkdown(input: string): string { if (input === undefined) return ""; return input @@ -115,10 +117,14 @@ export default class Util { } /** - * Parses the provided DText string, returning it as plain text - * @param input Input to process - * @param removeSections If true, removes `quote`, `code`, and `sections` blocks altogether - */ + * Parses the provided DText string, returning it as plain text + * @param input Input to process + * @param removeSections If true, removes `quote`, `code`, and `sections` blocks altogether + * @todo Properly account for `[color]` tags. + * @todo Body matcher is too broad; improve. + * @todo We don't have an `[o]` overline tag, nor is there an `` HTML tag; remove. + * @todo Leverage the independent parser? + */ public static stripDText(input: string, removeSections = true): string { if (removeSections) { input = input.replace(/\[quote\][\s\S]*\[\/quote\]/g, "") @@ -141,16 +147,16 @@ export default class Util { } /** - * Trims the thousands off a number and replaced them with a K. - * ex. 54321 -> 54.3k - * @param num Number to trim - */ + * Trims the thousands off a number and replaced them with a K. + * ex. 54321 -> 54.3k + * @param num Number to trim + */ public static formatK(num: number): string { return Math.abs(num) > 999 ? (Math.sign(num) * (Math.abs(num) / 1000)).toFixed(1) + "k" : Math.sign(num) * Math.abs(num) + ""; } - /* returns an array with the ratio */ + /* Returns an array with the ratio */ public static formatRatio(width: number, height: number): [number, number] { const d = gcd(width, height); return [width / d, height / d]; @@ -175,9 +181,10 @@ export default class Util { } /** - * Parses the textarea input specified in the parameter and returns a list of space-separated tags - * @param input Textarea to parse - */ + * Parses the textarea input specified in the parameter and returns a list of + * space-separated tags + * @param input Textarea to parse + */ public static getTagString(input: JQuery): string { return input.val()?.toString().trim() .toLowerCase() @@ -209,26 +216,27 @@ export default class Util { return new RegExp(result.join("|"), "gi"); } + // #region Versioning /** - * Compares two software version numbers (e.g. "1.7.1" or "1.2b"). - * - * This function was born in http://stackoverflow.com/a/6832721. - * - * @param v1 The first version to be compared. - * @param v2 The second version to be compared. - * @param [options] Optional flags that affect comparison behavior: - * `lexicographical` _boolean_ compares each part of the version strings lexicographically instead of - * naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than "1.2". - * `zeroExtend` _boolean_ changes the result if one version string has less parts than the other. In - * this case the shorter string will be padded with "zero" parts instead of being considered smaller. - * @returns {number|NaN} - * - 0 if the versions are equal - * - a negative integer iff v1 < v2 - * - a positive integer iff v1 > v2 - * - NaN if either version string is in the wrong format - * @copyright by Jon Papaioannou (`john.papaioannou@gmail.com`) - * @license This function is in the public domain. Do what you want with it, no strings attached. - */ + * Compares two software version numbers (e.g. "1.7.1" or "1.2b"). + * + * This function was born in http://stackoverflow.com/a/6832721. + * + * @param v1 The first version to be compared. + * @param v2 The second version to be compared. + * @param [options] Optional flags that affect comparison behavior: + * `lexicographical` _boolean_ compares each part of the version strings lexicographically instead of + * naturally; this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than "1.2". + * `zeroExtend` _boolean_ changes the result if one version string has less parts than the other. In + * this case the shorter string will be padded with "zero" parts instead of being considered smaller. + * @returns {number|NaN} + * - 0 if the versions are equal + * - a negative integer iff v1 < v2 + * - a positive integer iff v1 > v2 + * - NaN if either version string is in the wrong format + * @copyright by Jon Papaioannou (`john.papaioannou@gmail.com`) + * @license This function is in the public domain. Do what you want with it, no strings attached. + */ public static versionCompare(v1: string, v2: string, options?: { lexicographical?: boolean; zeroExtend?: boolean }): number { const lexicographical = options && options.lexicographical, zeroExtend = options && options.zeroExtend; @@ -293,13 +301,13 @@ export default class Util { // #endregion Versioning /** - * Formats an array into a string via a non-standard join method. - * Ex. ["one"] => "one" - * ["one", "two"] => "one and two" - * ["one", "two", "three"] => "one, two, and three" - * @param array Array to format - * @param delimiter Delimiter for the last element - */ + * Formats an array into a string via a non-standard join method. + * Ex. ["one"] => "one" + * ["one", "two"] => "one and two" + * ["one", "two", "three"] => "one, two, and three" + * @param array Array to format + * @param delimiter Delimiter for the last element + */ public static prettyPrintArray(array: string[], delimiter = "and"): string { if (array.length == 1) return array[0]; else if (array.length == 2) return array[0] + " " + delimiter + " " + array[1]; @@ -388,6 +396,3 @@ export default class Util { // #endregion replaceTemplateVariables } type StringReplacer = (key: string, ...args: any[]) => string; -// interface StringDictionary { -// [index: string]: B; -// } diff --git a/src/js/utilities/UtilIcons.ts b/src/js/utilities/UtilIcons.ts index 64bb071..69fc330 100644 --- a/src/js/utilities/UtilIcons.ts +++ b/src/js/utilities/UtilIcons.ts @@ -1,5 +1,5 @@ /** - * Lucide icons used by RE621 Mod Tools. + * Lucide icons used by this project. * Lucide is licensed under ISC. See https://lucide.dev */ From 814ce599d186dda216c5fcc635384661875c337f Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 12:40:07 -0400 Subject: [PATCH 07/14] Added utilities * URL normalization * Improved debug method params * Added debug option for prefixed messages --- src/REMT.ts | 14 ++++++------- src/js/components/RecordBuilder.ts | 2 +- src/js/models/Debug.ts | 23 +++++++++++++++++---- src/js/utilities/UtilNetwork.ts | 32 ++++++++++++++++++------------ 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/REMT.ts b/src/REMT.ts index 55f0be8..297ecdd 100644 --- a/src/REMT.ts +++ b/src/REMT.ts @@ -53,7 +53,7 @@ export default class REMT { if (Page.matches(IgnoredPages)) return; - console.log("%c[RE621.ModTools]%c v." + Script.version, "color: maroon", "color: unset"); + Debug.logPrefix("v." + Script.version); // Set up the API connection // TODO: Temporary instantiation method @@ -112,9 +112,9 @@ export default class REMT { for (const instance of Object.values(REMT._Registry)) promises.push(instance?.load() ?? Promise.resolve()); Promise.all(promises).then(() => { - console.log("%c[RE621.ModTools]%c loaded.", "color: maroon", "color: unset"); + Debug.logPrefix("Loaded."); this.buildSettingsButton(true); - console.log("%c[RE621.ModTools]%c fully loaded.", "color: maroon", "color: unset"); + Debug.logPrefix("Fully loaded."); }); } @@ -126,7 +126,7 @@ export default class REMT { */ private buildSettingsButton(addListeners = false) { this.settingsButton?.remove(); - console.log("%c[RE621.ModTools]%c checking for settings...", "color: maroon", "color: unset"); + Debug.logPrefix("Checking for settings..."); const configs = Object.values(REMT._Registry).reduce((p, e) => { if (!e) return p; if (addListeners) e.on("settingsConfigured", () => this.buildSettingsButton()); @@ -135,17 +135,17 @@ export default class REMT { return p; }, [] as SettingsDialogConfig[]); if (configs.length <= 0) { - console.log("%c[RE621.ModTools]%c no settings registered", "color: maroon", "color: unset"); + Debug.logPrefix("No settings registered"); return; } - console.log("%c[RE621.ModTools]%c loading %s settings...", "color: maroon", "color: unset", configs.length); + Debug.logPrefix("Loading %s settings...", configs.length); this.settingsButton = Util.DOM.addSettingsButton({ id: "remt-component-settings", icon: '', name: "REMT", onClick: () => MultiDialogForm.getRequestedInput(configs), }); - console.log("%c[RE621.ModTools]%c Settings button built.", "color: maroon", "color: unset"); + Debug.logPrefix("Settings button built."); } } diff --git a/src/js/components/RecordBuilder.ts b/src/js/components/RecordBuilder.ts index 6b11a0b..3b21d72 100644 --- a/src/js/components/RecordBuilder.ts +++ b/src/js/components/RecordBuilder.ts @@ -240,7 +240,7 @@ export default class RecordBuilder extends Component { } private generateRecordText() { - // console.log("%c[RE621.ModTools]%c Updating Record Text", "color: maroon", "color: unset"); + // Debug.logPrefix("Updating Record Text"); const result: string[] = []; diff --git a/src/js/models/Debug.ts b/src/js/models/Debug.ts index b323358..ea89593 100644 --- a/src/js/models/Debug.ts +++ b/src/js/models/Debug.ts @@ -1,5 +1,6 @@ import XM from "./api/XM"; +/** @todo Support granular log levels */ export default class Debug { public static get Enabled(): boolean { return XM.Storage.getValue("Debug.enabled", false); } @@ -28,14 +29,28 @@ export default class Debug { /** Logs the provided data into the console log if debug is enabled */ - public static log(...data: any[]): void { - if (Debug.Enabled) console.log(...data); + public static log(message?: any, ...optionalParams: any[]): void { + if (Debug.Enabled) console.log(message, ...optionalParams); + } + + public static prefixAlwaysPrints = true; + + /** + * Logs the provided data into the console log with the colored project name + * prefix if {@link Debug.Enabled} is enabled or {@link Debug.prefixAlwaysPrints} is true. + */ + public static logPrefix(message?: any, ...optionalParams: any[]): void { + if (!Debug.prefixAlwaysPrints && !Debug.Enabled) return; + if (typeof message === "string" && /%(?:(?:\.[0-9]+)?[dif]|[oOsc])/.test(message)) + console.log(`%c[RE621.ModTools]%c ` + message, "color: maroon", "color: unset", ...optionalParams); + else + console.log(`%c[RE621.ModTools]%c `, "color: maroon", "color: unset", message, ...optionalParams); } /** Logs the provided data as a table */ - public static table(obj: any): void { + public static table(obj: any, properties?: readonly string[]): void { if (!Debug.Enabled) return; - console.table(obj); + console.table(obj, properties); } /** Logs the provided data into the console log if connections logging is enabled */ diff --git a/src/js/utilities/UtilNetwork.ts b/src/js/utilities/UtilNetwork.ts index 3bb40c4..dc4af4d 100644 --- a/src/js/utilities/UtilNetwork.ts +++ b/src/js/utilities/UtilNetwork.ts @@ -32,19 +32,7 @@ export class UtilNetwork { }) } - /** - * Changes the script's name to match the pre-established User Agent format. - * E.g. If the package's name (& consequently script's name) is `RE621 Mod Tools`, that becomes `re621.ModTools`. - */ - private static projectNameFormatted = Script.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, (e) => e.toUpperCase()) : e.toLowerCase() + "."), ""); - - public static userAgent = this.projectNameFormatted + this.trimVersion(Script.version); - - private static trimVersion(value: string): string { - const match = value.match(/(\d\.\d+)\.\d+/); - if (!match || !match[1]) return "0.0"; - return match[1]; - } + public static userAgent = Script.projectNameCamelFormatted + Script.versionObj.format`${"major"}.${"minor"}`; public static get authToken() { return REMT.API.getAuthToken() ?? document.querySelector("meta[name=csrf-token]")?.getAttribute("content") ?? "~~FAILED TO GET TOKEN~~"; @@ -76,4 +64,22 @@ export class UtilNetwork { base["Authorization"] = authHeader; return base; } + + /** + * + * @param url + * @param options + * @returns The given URL with the given options resolved + */ + public static normalizeUrl(url: string, options = { + preserveQuery: false, + trailingPathSlash: false, + }) { + const parsed = new URL(url); + if (!options?.preserveQuery) parsed.search = ""; + url = parsed.href; + if (options?.trailingPathSlash && !url.endsWith("/")) url += "/"; + else if (!options?.trailingPathSlash && url.endsWith("/")) url = url.slice(0, url.length - 1); + return url; + } } From a899e8594bcb998d4616ba9b844a0598acfc0abe Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 13:16:18 -0400 Subject: [PATCH 08/14] Preempt circular dependency --- src/js/utilities/Util.ts | 1 - src/js/utilities/UtilNetwork.ts | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/js/utilities/Util.ts b/src/js/utilities/Util.ts index 16e7599..4baa5f4 100644 --- a/src/js/utilities/Util.ts +++ b/src/js/utilities/Util.ts @@ -1,4 +1,3 @@ -// import Debug from "../models/Debug"; import { UtilDOM } from "./UtilDOM"; import { UtilEvents } from "./UtilEvents"; import UtilID from "./UtilID"; diff --git a/src/js/utilities/UtilNetwork.ts b/src/js/utilities/UtilNetwork.ts index dc4af4d..df66ecd 100644 --- a/src/js/utilities/UtilNetwork.ts +++ b/src/js/utilities/UtilNetwork.ts @@ -32,7 +32,13 @@ export class UtilNetwork { }) } - public static userAgent = Script.projectNameCamelFormatted + Script.versionObj.format`${"major"}.${"minor"}`; + /** + * Backing store for {@link userAgent}. + * + * NOTE: Lazy init lets {@link Script} be safely initialized. + */ + static #userAgent: string; + public static get userAgent() { return this.#userAgent ??= Script.projectNameCamelFormatted + Script.versionObj.format`${"major"}.${"minor"}`; } public static get authToken() { return REMT.API.getAuthToken() ?? document.querySelector("meta[name=csrf-token]")?.getAttribute("content") ?? "~~FAILED TO GET TOKEN~~"; @@ -49,6 +55,7 @@ export class UtilNetwork { /** * Gives a User Agent & the required header for an authenticated GET request. + * @todo Add `X-User-Agent`? */ public static get simpleAuthHeaders() { const base: { From 423dc186bcc1adf1f7ffdd8956a2f0680c309b1c Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 13:19:45 -0400 Subject: [PATCH 09/14] Account for varying console spacing --- src/js/models/Debug.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/js/models/Debug.ts b/src/js/models/Debug.ts index ea89593..daf095b 100644 --- a/src/js/models/Debug.ts +++ b/src/js/models/Debug.ts @@ -34,17 +34,22 @@ export default class Debug { } public static prefixAlwaysPrints = true; + /** + * Some consoles add a delimiting space between trailing concatenated + * parameters to `console.log` et al; this accounts for that. + */ + public static omitSpaceWhenNonFormatted = true; /** * Logs the provided data into the console log with the colored project name - * prefix if {@link Debug.Enabled} is enabled or {@link Debug.prefixAlwaysPrints} is true. + * prefix if {@link Enabled} is enabled or {@link prefixAlwaysPrints} is true. */ public static logPrefix(message?: any, ...optionalParams: any[]): void { if (!Debug.prefixAlwaysPrints && !Debug.Enabled) return; if (typeof message === "string" && /%(?:(?:\.[0-9]+)?[dif]|[oOsc])/.test(message)) console.log(`%c[RE621.ModTools]%c ` + message, "color: maroon", "color: unset", ...optionalParams); else - console.log(`%c[RE621.ModTools]%c `, "color: maroon", "color: unset", message, ...optionalParams); + console.log(`%c[RE621.ModTools]%c${this.omitSpaceWhenNonFormatted ? "" : " "}`, "color: maroon", "color: unset", message, ...optionalParams); } /** Logs the provided data as a table */ From 3e5e7e5a9525dc79b9d8ea9bb0df3c7b90688b96 Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 13:26:55 -0400 Subject: [PATCH 10/14] Expanded `Script`'s project config members --- src/js/models/data/Script.ts | 95 ++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/src/js/models/data/Script.ts b/src/js/models/data/Script.ts index acc54ae..f0cea7c 100644 --- a/src/js/models/data/Script.ts +++ b/src/js/models/data/Script.ts @@ -1,4 +1,5 @@ -import { UtilNetwork } from '../../utilities/UtilNetwork'; +import Util from '../../utilities/Util'; + /** * Basic script parameters (both hard-coded and derived from the userscript * header), & miscellaneous site-specific configuration. @@ -9,38 +10,70 @@ import { UtilNetwork } from '../../utilities/UtilNetwork'; * * `GM_info.script.name`: At least 2 space-separated terms. * * `GM_info.script.version`: A proper SemVer version; can have any prefix. * * `GM_info.script.homepage`: A proper GitHub repository URL. + * + * # IMPORTANT + * If this is modified to utilize external resources from this library (e.g. the use of {@link Util.Network.normalizeUrl}), make certain to account for circular dependencies (e.g. {@link Util.Network.userAgent} is lazily initialized). */ export default class Script { public static readonly displayName = GM_info.script.name; + /** + * The raw version; expected to contain a + * [SemVer](https://semver.org/)-formatted string. + * + * @deprecated Will be replaced with {@link versionObj} next major release; use {@link versionObj.raw} instead. + * @todo Replace w/ copy of `versionObj`; deprecate current `versionObj`. + */ public static readonly version = GM_info.script.version; + /** + * An object containing the parsed version info. + */ + public static readonly versionObj = Object.freeze(Util.parseVersion(this.version)); public static readonly handler = Object.freeze({ name: GM_info.scriptHandler, version: GM_info.version, }); public static readonly url = { website: "https://re621.app", - repo: "https://github.com/re621/re621.ModTools", - issues: "https://github.com/re621/re621.ModTools/issues", + repo: GM_info.script.homepage ? Util.Network.normalizeUrl(GM_info.script.homepage) : "~~HOMEPAGE_MISSING~~", + issues: (GM_info.script.homepage ? Util.Network.normalizeUrl(GM_info.script.homepage) : "~~HOMEPAGE_MISSING~~") + "/issues", thread: "https://e621.net/forum_topics/25872", latest: "https://api.github.com/repos/re621/re621.ModTools/releases/latest", kofi: "https://ko-fi.com/bitWolfy", } + // #region Internal Project Identifiers + /** + * Changes the script's name to match the pre-established console logging format. + * + * E.g. If the package's name (& consequently script's name) is `RE621 Mod + * Tools`, that becomes `RE621.ModTools`. + */ + public static readonly projectNameFormatted = Script.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, e => e.toUpperCase()) : e + "."), ""); + /** * Changes the script's name to match the pre-established User Agent format. - * E.g. If the package's name (& consequently script's name) is `RE621 Mod Tools`, that becomes `re621.ModTools`. - * @deprecated Use {@link UtilNetwork.projectNameFormatted} instead. + * + * E.g. If the package's name (& consequently script's name) is `RE621 Mod + * Tools`, that becomes `re621.ModTools`. */ - private static projectNameFormatted = this.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, (e) => e.toUpperCase()) : e.toLowerCase() + "."), ""); + public static readonly projectNameCamelFormatted = Script.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, e => e.toUpperCase()) : e.toLowerCase() + "."), ""); + /** + * Changes the script's name to match an old pre-established event prefix format. + * + * E.g. If the package's name (& consequently script's name) is `RE621 Mod + * Tools`, that becomes `re621-mt`. + */ + public static readonly eventPrefixLegacy = Script.displayName.split(" ").reduce((p, e) => p + (p ? e[0].toLowerCase() : e.toLowerCase() + "-"), ""); + // #endregion Internal Project Identifiers /** * - * @deprecated Use {@link UtilNetwork.userAgent} instead. + * @deprecated Use {@link Util.Network.userAgent} instead. */ - public static readonly userAgent = this.projectNameFormatted + this.trimVersion(Script.version); + public static readonly userAgent = this.projectNameFormatted + this.trimVersion(Script.versionObj.raw); /** - * @deprecated Use {@link UtilNetwork.trimVersion} instead + * @deprecated Use {@link Util.parseVersion} instead */ private static trimVersion(value: string): string { const match = value.match(/(\d\.\d+)\.\d+/); @@ -48,11 +81,45 @@ export default class Script { return match[1]; } - public static readonly domains = GM_info.script.matches.map(e => e.match(/^https:\/\/(.+)\/\*/)?.[1]).reduce((p,e) => { - if (e) p.push(e); - return p; - }, []); + // #region Domains + /** E.g. For e621, should be `["e621.net", "e926.net"]`. */ + public static readonly domains = Object.freeze( + GM_info + .script + .matches + .map(e => e.match(/^https:\/\/(.+)\/\*/)?.[1]).reduce((p,e) => { + if (e) p.push(e); + return p; + }, []), + ); public static readonly primaryDomain = this.domains[0] ?? "e621.net"; - public static readonly staticDomains = GM_info.script.options.override.orig_connects.filter(e => e && e !== "*"); + + /** E.g. for e621, should be `["static1.e621.net", "static1.e926.net", "*"]` */ + public static readonly staticDomains = Object.freeze( + GM_info.script.options.override.orig_connects.filter(e => e && e !== "*"), + ); public static readonly primaryStaticDomain = this.staticDomains[0] ?? `static1.${this.primaryDomain}`; + + /** E.g. For e621, should be `["e621", "e926"]`. */ + public static readonly secondLevelDomains = Object.freeze( + this.domains.map(e => e.split(".").at(-2) ?? e.split(".")[0] ?? e), + ); + public static readonly primarySecondLevelDomain = this.secondLevelDomains[0] ?? "e621"; + + /** + * What do important wiki pages have as their prefix by convention? + * + * E.g. For e621 & e926, `e621` (as used for `[[e621:rules]]`, et. al.). + */ + public static readonly siteNameWikiPrefix = this.primarySecondLevelDomain; + + // #region Misc + /** + * Used to reference the name of the companion userscript; should one exist. + * + * This project started as a slimmer version of re621; this is used for log + * messages related to its expected presence or lack thereof. + */ + public static readonly companionProjectName = "re621"; + // #endregion Misc } From 0cf20b0519b6a5247220c44920f0c30309643464 Mon Sep 17 00:00:00 2001 From: J Mor Date: Tue, 28 Jul 2026 13:38:25 -0400 Subject: [PATCH 11/14] Replaced hardcoded strings with relevant `Script` members --- src/REMT.ts | 6 +++--- src/js/components/RecordBuilder.ts | 8 +++++--- src/js/components/ReportContentData.ts | 7 +------ src/js/models/Debug.ts | 5 +++-- src/js/models/data/Records.ts | 8 ++++---- src/js/models/structure/Modal.ts | 4 +++- src/js/utilities/ErrorHandler.ts | 4 ++-- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/REMT.ts b/src/REMT.ts index 297ecdd..69602f3 100644 --- a/src/REMT.ts +++ b/src/REMT.ts @@ -53,7 +53,7 @@ export default class REMT { if (Page.matches(IgnoredPages)) return; - Debug.logPrefix("v." + Script.version); + Debug.logPrefix("v." + Script.versionObj.full); // Set up the API connection // TODO: Temporary instantiation method @@ -77,7 +77,7 @@ export default class REMT { bodyLoaded = PageObserver.watch("body").then(() => { Debug.log("+ BODY is ready"); - $("body").attr("remt", Script.version); + $("body").attr("remt", Script.versionObj.full); Util.DOM.setupDialogContainer(); // TODO Move to the dialog class User.init(); }); @@ -105,7 +105,7 @@ export default class REMT { REMT._Registry[instance.getName()] = instance; await instance.bootstrapSettings(); } - Util.Events.trigger("re621-mt:bootstrap"); + Util.Events.trigger(`${Script.eventPrefixLegacy}:bootstrap`); // Load modules (asynchronous) const promises: Promise[] = []; diff --git a/src/js/components/RecordBuilder.ts b/src/js/components/RecordBuilder.ts index 3b21d72..95dcc94 100644 --- a/src/js/components/RecordBuilder.ts +++ b/src/js/components/RecordBuilder.ts @@ -1,5 +1,6 @@ import { PageDefinition } from "../models/data/Page"; import Records from "../models/data/Records"; +import Script from "../models/data/Script"; import Util from "../utilities/Util"; import Component from "./Component"; @@ -251,6 +252,8 @@ export default class RecordBuilder extends Component { this.input.val(result.join("\n")); } + private static readonly sourceMatcher = new RegExp(`https:\\/\\/(?:${Script.domains.join("|")})\\/`, "g"); + private processForm(form: JQuery): string { // console.log(form.data()); @@ -284,7 +287,7 @@ export default class RecordBuilder extends Component { (ruleData.preface ? (ruleData.preface + "\n\n") : "") + `${ruleLines.join("\n")}\n\n` + (ruleData.postface ? (ruleData.postface + "\n\n") : "") + - `"[Code of Conduct - ${ruleData.title}]":${ruleData.link ? ruleData.link : `/wiki_pages/e621:rules#${name.toLowerCase()}`}\n` + + `"[Code of Conduct - ${ruleData.title}]":${ruleData.link ? ruleData.link : `/wiki_pages/${Script.siteNameWikiPrefix}:rules#${name.toLowerCase()}`}\n` + `[/section]` ); } @@ -301,12 +304,11 @@ export default class RecordBuilder extends Component { /** * Convert a source link into a common format * @param {string} source Source link - * @todo Change to handle e6ai. */ function processSource(source: string): string { return decodeURI(source) .trim() - .replace(/https:\/\/e(?:621|926).net\//g, "/") // Make links relative + .replace(RecordBuilder.sourceMatcher, "/") // Make links relative .replace(/\/posts\/(\d+)#comment-(\d+)/g, "/comments/$2") // Convert comment links .replace(/\/forum_topics\/(\d+)(?:\?page=\d+)?#forum_post_(\d+)/g, "/forum_posts/$2") // Convert forum post links .replace(/\?lr=\d+&/, "?") // Trim the tag history links diff --git a/src/js/components/ReportContentData.ts b/src/js/components/ReportContentData.ts index 47bb77f..ab1d7af 100644 --- a/src/js/components/ReportContentData.ts +++ b/src/js/components/ReportContentData.ts @@ -1,4 +1,3 @@ -import XM from "../models/api/XM"; import { PageDefinition } from "../models/data/Page"; import Modal from "../models/structure/Modal"; import ErrorHandler from "../utilities/ErrorHandler"; @@ -150,11 +149,7 @@ export default class ReportContentData extends Component { ` as HTMLDivElement; replacer(flexBox); - const dText = await (await fetch(`/dtext_preview.json?body=${encodeURIComponent(data.body)}&allow_color="false"`, { method: "POST", "body": JSON.stringify({ "body": data.body, "allow_color": "false" }), headers: { - "User-Agent": `${XM.Info.script.name}/${XM.Info.script.version} (by ${XM.Info.script.author} on e621)`, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - "X-CSRF-Token": document.querySelector("meta[name=csrf-token]")!.getAttribute("content") ?? "", - } })).json(); + const dText = await (await fetch(`/dtext_preview.json?body=${encodeURIComponent(data.body)}&allow_color="false"`, { method: "POST", "body": JSON.stringify({ "body": data.body, "allow_color": "false" }), headers: Util.Network.simpleAuthHeaders })).json(); flexBox.removeChild(commentBox); if (dText["html"]) flexBox.appendChild(html`
Message:
${dText["html"]}
`); } diff --git a/src/js/models/Debug.ts b/src/js/models/Debug.ts index daf095b..201861d 100644 --- a/src/js/models/Debug.ts +++ b/src/js/models/Debug.ts @@ -1,4 +1,5 @@ import XM from "./api/XM"; +import Script from "./data/Script"; /** @todo Support granular log levels */ export default class Debug { @@ -47,9 +48,9 @@ export default class Debug { public static logPrefix(message?: any, ...optionalParams: any[]): void { if (!Debug.prefixAlwaysPrints && !Debug.Enabled) return; if (typeof message === "string" && /%(?:(?:\.[0-9]+)?[dif]|[oOsc])/.test(message)) - console.log(`%c[RE621.ModTools]%c ` + message, "color: maroon", "color: unset", ...optionalParams); + console.log(`%c[${Script.projectNameFormatted}]%c ` + message, "color: maroon", "color: unset", ...optionalParams); else - console.log(`%c[RE621.ModTools]%c${this.omitSpaceWhenNonFormatted ? "" : " "}`, "color: maroon", "color: unset", message, ...optionalParams); + console.log(`%c[${Script.projectNameFormatted}]%c${this.omitSpaceWhenNonFormatted ? "" : " "}`, "color: maroon", "color: unset", message, ...optionalParams); } /** Logs the provided data as a table */ diff --git a/src/js/models/data/Records.ts b/src/js/models/data/Records.ts index b49bc24..ebdc331 100644 --- a/src/js/models/data/Records.ts +++ b/src/js/models/data/Records.ts @@ -33,11 +33,11 @@ export default class Records { rules: "disruptive", }, blacklist: { - text: "Please use your [[e621:blacklist|blacklist]].", + text: `Please use your [[${Script.siteNameWikiPrefix}:blacklist|blacklist]].`, rules: "blacklist", }, blacklist2: { - text: "[[e621:blacklist|Blacklist]] quietly, please.", + text: `[[${Script.siteNameWikiPrefix}:blacklist|Blacklist]] quietly, please.`, rules: "blacklist", }, roleplay: { @@ -101,7 +101,7 @@ export default class Records { disruptive: { title: "Disruptive Behavior", group: this.RuleGroups.messages, - preface: `e621 is an open and friendly community that people visit to share and enjoy furry artwork.\nPeople can have different opinions, but that does not give them permission to make someone else feel uncomfortable or unwanted.`, + preface: `${Script.primarySecondLevelDomain} is an open and friendly community that people visit to share and enjoy furry artwork.\nPeople can have different opinions, but that does not give them permission to make someone else feel uncomfortable or unwanted.`, rules: [ "Do not make messages with the apparent purpose of upsetting someone. That includes picking fights, baiting arguments, calling names, or making remarks regarding personal grievances, quarrels, or malicious rumors.", "Do not purposefully or repeatedly spread false or defamatory information.", @@ -312,7 +312,7 @@ export default class Records { underage: { title: "Age Restrictions", group: this.RuleGroups.illegal, - preface: "By using e621 and/or registering an account, you agree that you are at least eighteen years of age. No individual under the age of eighteen may register an account, use our services, provide any personal information to e621, or otherwise upload or transmit personal information through our services.", + preface: `By using ${Script.primarySecondLevelDomain} and/or registering an account, you agree that you are at least eighteen years of age. No individual under the age of eighteen may register an account, use our services, provide any personal information to ${Script.primarySecondLevelDomain}, or otherwise upload or transmit personal information through our services.`, rules: [], postface: `Even though laws may vary depending on your location, ${Script.primaryDomain}'s management and service office is located in Arizona, USA, and it is those laws that we are required to abide by. We cannot knowingly allow any minors to view pornographic content, so we must be proactive in preventing underage access. This includes accounts that are either legally or illegally accessed by minors. Furthermore, we require official, government-issued photo identification as proof of you turning eighteen in order to release access to your account. Please email us at \`management@${Script.primaryDomain}\` for further details.`, }, diff --git a/src/js/models/structure/Modal.ts b/src/js/models/structure/Modal.ts index 1adfc10..ac5cc66 100644 --- a/src/js/models/structure/Modal.ts +++ b/src/js/models/structure/Modal.ts @@ -1,5 +1,7 @@ import Util from "../../utilities/Util"; import { makeIcon } from "../../utilities/UtilIcons"; +import Script from "../data/Script"; +import Debug from "../Debug"; import PreparedStructure from "./PreparedStructure"; export default class Modal { @@ -216,7 +218,7 @@ export default class Modal { */ private static ensureModalContainer() { if (!document.querySelector("#modal-container")) { - console.log("%c[RE621.ModTools]%c: no re621 detected; manually creating 'div#modal-container'...", "color: maroon", "color: unset"); + Debug.logPrefix(`No ${Script.companionProjectName} detected; manually creating 'div#modal-container'...`); $("
").attr("id", "modal-container").prependTo("div#page"); } } diff --git a/src/js/utilities/ErrorHandler.ts b/src/js/utilities/ErrorHandler.ts index 6db3809..fabc2d4 100644 --- a/src/js/utilities/ErrorHandler.ts +++ b/src/js/utilities/ErrorHandler.ts @@ -7,11 +7,11 @@ export default class ErrorHandler { public static async write(message: string, error?: Error | any): Promise { const notice = $("
").html([ - `

RE621.ModTools had encountered an error during script execution.

`, + `

${Script.projectNameFormatted} had encountered an error during script execution.

`, `

Please, report this message, including the error log below, through the issue tracker, or in the forum thread.

`, ].join("\n")); const textarea = $("