From 376fa23288d5a7c7183bdfcea2ac333678fac749 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Fri, 4 Apr 2025 22:40:15 +0200 Subject: [PATCH 01/78] Started working on --silent flag --- package.json | 2 +- src/core/api/defaults/flag.ts | 1 + src/core/startup/manageMigration.ts | 48 ++++++++++++++++------------- src/data/framework/flagLoader.ts | 1 + 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index ccb39b9..996a9ea 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "#opendiscord": "./dist/src/index.js", "#opendiscord-types": "./dist/src/core/api/api.js" }, - "funding":{ + "funding": { "type": "individual", "url": "https://github.com/sponsors/DJj123dj" } diff --git a/src/core/api/defaults/flag.ts b/src/core/api/defaults/flag.ts index bf2d5eb..218fae4 100644 --- a/src/core/api/defaults/flag.ts +++ b/src/core/api/defaults/flag.ts @@ -23,6 +23,7 @@ export interface ODFlagManagerIds_Default { "opendiscord:force-slash-update":ODFlag, "opendiscord:no-compile":ODFlag, "opendiscord:compile-only":ODFlag, + "opendiscord:silent":ODFlag, } /**## ODFlagManager_Default `default_class` diff --git a/src/core/startup/manageMigration.ts b/src/core/startup/manageMigration.ts index 496de92..d08d833 100644 --- a/src/core/startup/manageMigration.ts +++ b/src/core/startup/manageMigration.ts @@ -1,5 +1,24 @@ import {opendiscord, api, utilities} from "../../index" +/**Check if migration is required. Returns the last version used in the database. */ +async function isMigrationRequired(): Promise { + const rawVersion = await opendiscord.databases.get("opendiscord:global").get("opendiscord:last-version","opendiscord:version") + if (!rawVersion) return false + const version = api.ODVersion.fromString("opendiscord:last-version",rawVersion) + if (opendiscord.versions.get("opendiscord:version").compare(version) == "higher"){ + return version + }else return false +} + +/**Save all versions in `opendiscord.versions` to the global database. */ +async function saveAllVersionsToDatabase(){ + const globalDatabase = opendiscord.databases.get("opendiscord:global") + + await opendiscord.versions.loopAll(async (version,id) => { + await globalDatabase.set("opendiscord:last-version",id.value,version.toString()) + }) +} + export const loadVersionMigrationSystem = async () => { //ENTER MIGRATION CONTEXT await preloadMigrationContext() @@ -36,7 +55,8 @@ export const loadVersionMigrationSystem = async () => { return lastVersion } -const preloadMigrationContext = async () => { +/**Initialize the migration context by loading the built-in flags, configs & databases. */ +async function preloadMigrationContext(){ opendiscord.debug.debug("-- MIGRATION CONTEXT START --") await (await import("../../data/framework/flagLoader.js")).loadAllFlags() await opendiscord.flags.init() @@ -47,7 +67,8 @@ const preloadMigrationContext = async () => { opendiscord.debug.visible = true } -const unloadMigrationContext = async () => { +/**Unload the migration context to start the bot normally. */ +async function unloadMigrationContext(){ opendiscord.debug.visible = false await opendiscord.databases.loopAll((database,id) => {opendiscord.databases.remove(id)}) await opendiscord.configs.loopAll((config,id) => {opendiscord.configs.remove(id)}) @@ -55,16 +76,8 @@ const unloadMigrationContext = async () => { opendiscord.debug.debug("-- MIGRATION CONTEXT END --") } -const isMigrationRequired = async (): Promise => { - const rawVersion = await opendiscord.databases.get("opendiscord:global").get("opendiscord:last-version","opendiscord:version") - if (!rawVersion) return false - const version = api.ODVersion.fromString("opendiscord:last-version",rawVersion) - if (opendiscord.versions.get("opendiscord:version").compare(version) == "higher"){ - return version - }else return false -} - -const loadAllVersionMigrations = async (lastVersion:api.ODVersion) => { +/**Execute all version migration functions which are handled in the restricted migration context. */ +async function loadAllVersionMigrations(lastVersion:api.ODVersion){ const migrations = (await import("./migration.js")).migrations migrations.sort((a,b) => { const comparison = a.version.compare(b.version) @@ -83,7 +96,8 @@ const loadAllVersionMigrations = async (lastVersion:api.ODVersion) => { } } -export const loadAllAfterInitVersionMigrations = async (lastVersion:api.ODVersion) => { +/**Execute all version migration functions which are handled in the normal startup sequence. */ +export async function loadAllAfterInitVersionMigrations(lastVersion:api.ODVersion){ const migrations = (await import("./migration.js")).migrations migrations.sort((a,b) => { const comparison = a.version.compare(b.version) @@ -100,12 +114,4 @@ export const loadAllAfterInitVersionMigrations = async (lastVersion:api.ODVersio ]) } } -} - -const saveAllVersionsToDatabase = async () => { - const globalDatabase = opendiscord.databases.get("opendiscord:global") - - await opendiscord.versions.loopAll(async (version,id) => { - await globalDatabase.set("opendiscord:last-version",id.value,version.toString()) - }) } \ No newline at end of file diff --git a/src/data/framework/flagLoader.ts b/src/data/framework/flagLoader.ts index 8c4e5bc..6a70f40 100644 --- a/src/data/framework/flagLoader.ts +++ b/src/data/framework/flagLoader.ts @@ -15,4 +15,5 @@ export const loadAllFlags = async () => { opendiscord.flags.add(new api.ODFlag("opendiscord:force-slash-update","Force Slash Update","Force update all slash commands.","--force-slash",["-fs"])) opendiscord.flags.add(new api.ODFlag("opendiscord:no-compile","No Compile","Disable compilation of plugins & bot before starting.","--no-compile",[])) opendiscord.flags.add(new api.ODFlag("opendiscord:compile-only","Compile Only","This description will never be shown because the bot wouldn't run when this flag is enabled :)","--compile-only",[])) + opendiscord.flags.add(new api.ODFlag("opendiscord:silent","Silent Mode","Run the bot without displaying logs. The startscreen will still be displayed.","--silent",[])) } \ No newline at end of file From 831e120093637d1836a889112f5eb44189cd599f Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sat, 5 Apr 2025 22:18:45 +0200 Subject: [PATCH 02/78] Finished --silent flag --- src/core/api/modules/console.ts | 8 +++++--- src/core/api/modules/defaults.ts | 3 +++ src/core/startup/manageMigration.ts | 2 ++ src/index.ts | 16 ++++++++++++++++ src/livestatus.json | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/core/api/modules/console.ts b/src/core/api/modules/console.ts index fc634d6..f34399c 100644 --- a/src/core/api/modules/console.ts +++ b/src/core/api/modules/console.ts @@ -241,6 +241,8 @@ export class ODConsoleManager { historylength = 100 /**An alias to the debugfile manager. (`otdebug.txt`) */ debugfile: ODDebugFileManager + /**Is silent mode enabled? */ + silent: boolean = false constructor(historylength:number, debugfile:ODDebugFileManager){ this.historylength = historylength @@ -253,12 +255,12 @@ export class ODConsoleManager { log(message:string, type?:ODConsoleMessageTypes, params?:ODConsoleMessageParam[]): void log(message:ODConsoleMessage|ODError|string, type?:ODConsoleMessageTypes, params?:ODConsoleMessageParam[]){ if (message instanceof ODConsoleMessage){ - message.render() + if (!this.silent) message.render() if (this.debugfile) this.debugfile.writeConsoleMessage(message) this.history.push(message) }else if (message instanceof ODError){ - message.render() + if (!this.silent) message.render() if (this.debugfile) this.debugfile.writeErrorMessage(message) this.history.push(message) @@ -272,7 +274,7 @@ export class ODConsoleManager { else if (type == "error") newMessage = new ODConsoleErrorMessage(message,params) else newMessage = new ODConsoleSystemMessage(message,params) - newMessage.render() + if (!this.silent) newMessage.render() if (this.debugfile) this.debugfile.writeConsoleMessage(newMessage) this.history.push(newMessage) } diff --git a/src/core/api/modules/defaults.ts b/src/core/api/modules/defaults.ts index aaa0c47..63a9d68 100644 --- a/src/core/api/modules/defaults.ts +++ b/src/core/api/modules/defaults.ts @@ -13,6 +13,8 @@ export interface ODDefaults { crashOnError:boolean, /**Enable the system responsible for the `--debug` flag. */ debugLoading:boolean, + /**Enable the system responsible for the `--silent` flag. */ + silentLoading:boolean, /**When enabled, you're able to use the "!OPENTICKET:dump" command to send the OT debug file. This is only possible when you're the owner of the bot. */ allowDumpCommand:boolean, /**Enable loading all Open Ticket plugins, sadly enough is only useful for the system :) */ @@ -233,6 +235,7 @@ export class ODDefaultsManager { errorHandling:true, crashOnError:false, debugLoading:true, + silentLoading:true, allowDumpCommand:true, pluginLoading:true, softPluginLoading:false, diff --git a/src/core/startup/manageMigration.ts b/src/core/startup/manageMigration.ts index d08d833..8aeae49 100644 --- a/src/core/startup/manageMigration.ts +++ b/src/core/startup/manageMigration.ts @@ -48,6 +48,8 @@ export const loadVersionMigrationSystem = async () => { if (opendiscord.flags.exists("opendiscord:soft-plugins") && opendiscord.flags.get("opendiscord:soft-plugins").value) opendiscord.defaults.setDefault("softPluginLoading",true) if (opendiscord.flags.exists("opendiscord:crash") && opendiscord.flags.get("opendiscord:crash").value) opendiscord.defaults.setDefault("crashOnError",true) if (opendiscord.flags.exists("opendiscord:force-slash-update") && opendiscord.flags.get("opendiscord:force-slash-update").value) opendiscord.defaults.setDefault("forceSlashCommandRegistration",true) + if (opendiscord.flags.exists("opendiscord:silent") && opendiscord.flags.get("opendiscord:silent").value) opendiscord.console.silent = true + //LEAVE MIGRATION CONTEXT await unloadMigrationContext() diff --git a/src/index.ts b/src/index.ts index 92c80f2..33ed912 100644 --- a/src/index.ts +++ b/src/index.ts @@ -115,6 +115,17 @@ const main = async () => { opendiscord.debug.visible = (debugFlag) ? debugFlag.value : false } + //load silent mode + if (opendiscord.defaults.getDefault("silentLoading")){ + const silentFlag = opendiscord.flags.get("opendiscord:silent") + opendiscord.console.silent = (silentFlag) ? silentFlag.value : false + if (opendiscord.console.silent){ + opendiscord.console.silent = false + opendiscord.log("Silent mode is active! Logs won't be shown in the console.","warning") + opendiscord.console.silent = true + } + } + //load progress bar renderers opendiscord.log("Loading progress bars...","system") if (opendiscord.defaults.getDefault("progressBarRendererLoading")){ @@ -823,6 +834,11 @@ const main = async () => { opendiscord.log("Please help us improve the translation by contributing to our project!","warning") console.log("===================") } + if (opendiscord.console.silent){ + opendiscord.console.silent = false + opendiscord.log("Silent mode is active! Logs won't be shown in the console.","warning") + opendiscord.console.silent = true + } await opendiscord.events.get("afterStartScreensRendered").emit([opendiscord.startscreen]) } diff --git a/src/livestatus.json b/src/livestatus.json index fa02062..52efa22 100644 --- a/src/livestatus.json +++ b/src/livestatus.json @@ -47,7 +47,7 @@ "descriptionColor":"normal" }, "active":{ - "versions":["4.0.0"], + "versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4"], "languages":[], "allLanguages":true, "usingPlugins":true, From 5f79ae909b67fa05f741ba94be7bed544530cddf Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 6 Apr 2025 19:04:09 +0200 Subject: [PATCH 03/78] Added Interactive Setup CLI (Unfinished) Added the first part of the interactive setup CLI. You can explore the interface but some parts will not work yet. It will also not save to the config file yet. The "Quick Setup" & "Manage Plugins" parts of the CLI will arrive later this week. --- package.json | 8 +- src/core/api/defaults/checker.ts | 10 +- src/core/api/defaults/flag.ts | 1 + src/core/api/defaults/stat.ts | 1 - src/core/api/modules/checker.ts | 85 +++-- src/core/startup/cli.ts | 524 ++++++++++++++++++++++++++++ src/core/startup/init.ts | 10 +- src/data/framework/checkerLoader.ts | 75 ++-- src/data/framework/flagLoader.ts | 1 + src/index.ts | 13 +- 10 files changed, 649 insertions(+), 79 deletions(-) create mode 100644 src/core/startup/cli.ts diff --git a/package.json b/package.json index 996a9ea..430ef08 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "scripts": { "build": "node index.js --compile-only", "start": "node index.js", + "setup": "node index.js --cli", "startnc": "node index.js --no-compile", - "test": "node index.js --dev-config --dev-database", - "testnc": "node index.js --no-compile --dev-config --dev-database", + "test": "node index.js --dev-config --dev-database --soft-plugins", + "testsetup": "node index.js --cli --dev-config --dev-database --soft-plugins", + "testnc": "node index.js --no-compile --dev-config --dev-database --soft-plugins", "docs": "npx typedoc --options .docs/typedoc-config.json && node .docs/createDocs.js" }, "type": "commonjs", @@ -24,9 +26,11 @@ "dependencies": { "@discordjs/rest": "^2.4.2", "@types/node": "^22.5.0", + "@types/terminal-kit": "^2.5.7", "ansis": "^2.3.0", "discord.js": "^14.17.2", "formatted-json-stringify": "^1.1.0", + "terminal-kit": "^3.1.2", "typescript": "^5.5.4" }, "repository": { diff --git a/src/core/api/defaults/checker.ts b/src/core/api/defaults/checker.ts index db3bbe1..62a15fb 100644 --- a/src/core/api/defaults/checker.ts +++ b/src/core/api/defaults/checker.ts @@ -183,14 +183,8 @@ export class ODCheckerRenderer_Default extends ODCheckerRenderer { return finalComponents } /**Get the length of the longest string in the array. */ - #getLongestLength(text:string[]): number { - let finalLength = 0 - text.forEach((t) => { - const l = ansis.strip(t).length - if (l > finalLength) finalLength = l - }) - - return finalLength + #getLongestLength(texts:string[]): number { + return Math.max(...texts.map((t) => ansis.strip(t).length)) } /**Get a horizontal divider used between different parts of the config checker result. */ #getHorizontalDivider(width:number): string { diff --git a/src/core/api/defaults/flag.ts b/src/core/api/defaults/flag.ts index 218fae4..8830cc2 100644 --- a/src/core/api/defaults/flag.ts +++ b/src/core/api/defaults/flag.ts @@ -24,6 +24,7 @@ export interface ODFlagManagerIds_Default { "opendiscord:no-compile":ODFlag, "opendiscord:compile-only":ODFlag, "opendiscord:silent":ODFlag, + "opendiscord:cli":ODFlag, } /**## ODFlagManager_Default `default_class` diff --git a/src/core/api/defaults/stat.ts b/src/core/api/defaults/stat.ts index 35226c6..dd4c016 100644 --- a/src/core/api/defaults/stat.ts +++ b/src/core/api/defaults/stat.ts @@ -1,7 +1,6 @@ /////////////////////////////////////// //DEFAULT SESSION MODULE /////////////////////////////////////// -import { Guild, TextBasedChannel, User } from "discord.js" import { ODValidId } from "../modules/base" import { ODStatScope, ODStatGlobalScope, ODStatsManager, ODStat, ODBasicStat, ODDynamicStat, ODValidStatValue, ODStatScopeSetMode } from "../modules/stat" diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index 14b397b..cccc4e2 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -289,6 +289,16 @@ export class ODCheckerFunctionManager extends ODManager { */ export type ODCheckerLocationTrace = (string|number)[] +/**## ODCheckerOptions `interface` + * This interface contains all optional properties to customise in the `ODChecker` class. + */ +export interface ODCheckerOptions { + /**The name of this config in the Interactive Setup CLI. */ + cliDisplayName?:string + /**The description of this config in the Interactive Setup CLI. */ + cliDisplayDescription?:string +} + /**## ODChecker `class` * This is an Open Ticket config checker. * @@ -310,13 +320,16 @@ export class ODChecker extends ODManagerData { messages: ODCheckerMessage[] = [] /**Temporary storage for the quit status from the check() method (not recommended to use) */ quit: boolean = false + /**All additional properties of this config checker. */ + options: ODCheckerOptions - constructor(id:ODValidId, storage: ODCheckerStorage, priority:number, config:ODConfig, structure: ODCheckerStructure){ + constructor(id:ODValidId, storage: ODCheckerStorage, priority:number, config:ODConfig, structure:ODCheckerStructure, options?:ODCheckerOptions){ super(id) this.storage = storage this.priority = priority this.config = config this.structure = structure + this.options = options ?? {} } /**Run this checker. Returns all errors*/ @@ -391,7 +404,13 @@ export interface ODCheckerStructureOptions { /**Add a custom checker function. Returns `true` when valid. */ custom?:(checker:ODChecker, value:ODValidJsonType, locationTrace:ODCheckerLocationTrace, locationId:ODId, locationDocs:string|null) => boolean, /**Set the url to the documentation of this variable. */ - docs?:string + docs?:string, + /**The name of this config in the Interactive Setup CLI. */ + cliDisplayName?:string + /**The description of this config in the Interactive Setup CLI. */ + cliDisplayDescription?:string + /**The default value of this variable when creating it in the Interactive Setup CLI. When not specified, the user will be asked to insert a value. */ + cliInitDefaultValue?:string } /**## ODCheckerStructure `class` @@ -426,7 +445,13 @@ export class ODCheckerStructure { */ export interface ODCheckerObjectStructureOptions extends ODCheckerStructureOptions { /**Add a checker for a property in an object (can also be optional) */ - children?:{key:string, checker:ODCheckerStructure, priority:number, optional:boolean}[] + children?:{key:string, priority:number, optional:boolean, cliHideInEditMode?:boolean, checker:ODCheckerStructure}[], + /**A list of keys to skip when creating this object with the Interactive Setup CLI. The default value of these properties will be used instead. */ + cliInitSkipKeys?:string[], + /**The key of a (primitive) property in this object to show the value of in the Interactive Setup CLI when listed in an array. */ + cliDisplayKeyInParentArray?:string, + /**A list of additional (primitive) property keys in this object to show the value of in the Interactive Setup CLI when listed in an array. */ + cliDisplayAdditionalKeysInParentArray?:string[] } /**## ODCheckerObjectStructure `class` @@ -500,7 +525,9 @@ export interface ODCheckerStringStructureOptions extends ODCheckerStructureOptio /**You need to choose between ... */ choices?:string[], /**The string needs to match this regex */ - regex?:RegExp + regex?:RegExp, + /**Provide an optional list for autocomplete when using the Interactive Setup CLI. Defaults to the `choices` option. */ + cliAutocompleteList?:string[] } /**## ODCheckerStringStructure `class` @@ -718,7 +745,9 @@ export interface ODCheckerArrayStructureOptions extends ODCheckerStructureOption /**Allow double values (only for `string`, `number` & `boolean`) */ allowDoubles?:boolean /**Only allow these types in the array (for multi-type propertyCheckers) */ - allowedTypes?:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[] + allowedTypes?:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[], + /**The name of the properties inside this array. Used in the GUI of the Interactive Setup CLI. */ + cliDisplayPropertyName?:string } /**## ODCheckerArrayStructure `class` @@ -863,9 +892,9 @@ export interface ODCheckerTypeSwitchStructureOptions extends ODCheckerStructureO /**A checker when the property is a boolean */ boolean?:ODCheckerBooleanStructure, /**A checker when the property is null */ - null?:ODCheckerStructure, + null?:ODCheckerNullStructure, /**A checker when the property is an array */ - array?:ODCheckerStructure, + array?:ODCheckerArrayStructure, /**A checker when the property is an object */ object?:ODCheckerObjectStructure, /**A checker when the property is something else */ @@ -1390,32 +1419,26 @@ export class ODCheckerCustomStructure_UniqueIdArray extends ODCheckerArrayStruct constructor(id:ODValidId, source:string, scope:string, usedScope?:string, options?:ODCheckerArrayStructureOptions){ //add premade custom structure checker const newOptions = options ?? {} - newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { - const lt = checker.locationTraceDeref(locationTrace) + newOptions.propertyChecker = new ODCheckerStringStructure("opendiscord:unique-id",{minLength:1,custom:(checker,value,locationTrace,locationId,locationDocs) => { + if (typeof value != "string") return false + const localLt = checker.locationTraceDeref(locationTrace) + localLt.pop() - if (!Array.isArray(value)) return false - const uniqueArray: string[] = (checker.storage.get(source,scope) === null) ? [] : checker.storage.get(source,scope) - - let localQuit = false - value.forEach((id,index) => { - if (typeof id != "string") return - const localLt = checker.locationTraceDeref(lt) - localLt.push(index) - if (uniqueArray.includes(id)){ - //exists - if (usedScope){ - const current: string[] = checker.storage.get(source,usedScope) ?? [] - current.push(id) - checker.storage.set(source,usedScope,current) - } - }else{ - //doesn't exist - checker.createMessage("opendiscord:id-non-existent","error",`The id "${id}" doesn't exist!`,localLt,null,[`"${id}"`],this.id,(this.options.docs ?? null)) - localQuit = true + const uniqueArray: string[] = checker.storage.get(source,scope) ?? [] + if (uniqueArray.includes(value)){ + //exists + if (usedScope){ + const current: string[] = checker.storage.get(source,usedScope) ?? [] + current.push(value) + checker.storage.set(source,usedScope,current) } - }) - return !localQuit - } + return true + }else{ + //doesn't exist + checker.createMessage("opendiscord:id-non-existent","error",`The id "${value}" doesn't exist!`,localLt,null,[`"${value}"`],locationId,locationDocs) + return false + } + }}) super(id,newOptions) this.source = source this.scope = scope diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts new file mode 100644 index 0000000..acbd8f4 --- /dev/null +++ b/src/core/startup/cli.ts @@ -0,0 +1,524 @@ +import {opendiscord, api, utilities} from "../../index" +import {Terminal, terminal} from "terminal-kit" +import ansis from "ansis" + +const logo = [ + " ██████╗ ██████╗ ███████╗███╗ ██╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ ", + " ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ ", + " ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║██║ █████╔╝ █████╗ ██║ ", + " ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ", + " ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ", + " ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ " +] + +/**A utility function to center text to a certain width. */ +function centerText(text:string,width:number){ + if (width < text.length) return text + let newWidth = width-ansis.strip(text).length+1 + let final = " ".repeat(newWidth/2)+text + return final +} + +/**A utility function to terminate the interactive CLI. */ +async function terminate(){ + terminal.grabInput(false) + terminal.clear() + terminal.green("👋 Exited the Open Ticket Interactive Setup CLI.\n") + process.exit(0) +} +terminal.on("key",(name,matches,data) => { + if (name == "CTRL_C") terminate() +}) + +/**Render the header of the interactive CLI. */ +function renderHeader(path:(string|number)[]){ + terminal.grabInput(true) + terminal.clear().moveTo(1,1) + terminal(ansis.hex("#f8ba00")(logo.join("\n")+"\n")) + terminal.bold(centerText("Interactive Setup CLI - Version: "+opendiscord.versions.get("opendiscord:version").toString()+" - Support: https://discord.dj-dj.be\n",88)) + if (path.length < 1) terminal.cyan(centerText("👋 Hi! Welcome to the Open Ticket Interactive Setup CLI! 👋\n\n",88)) + else terminal.cyan(centerText("🌐 Current Location: "+path.map((v,i) => { + if (i == 0) return v.toString() + else if (typeof v == "string") return ".\""+v+"\"" + else if (typeof v == "number") return "."+v + }).join("")+"\n\n",88)) +} + +async function renderConfigSelector(backFn:(() => api.ODPromiseVoid)){ + renderHeader([]) + terminal(ansis.bold.green("Please select which config you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const checkerList = opendiscord.checkers.getAll() + const checkerNameList = checkerList.map((checker) => (checker.options.cliDisplayName ? checker.options.cliDisplayName+" ("+checker.config.file+")" : checker.config.file)) + const checkerNameLength = utilities.getLongestLength(checkerNameList) + const finalCheckerNameList = checkerNameList.map((name,index) => name.padEnd(checkerNameLength+5," ")+ansis.gray(checkerList[index].options.cliDisplayDescription ? "=> "+checkerList[index].options.cliDisplayDescription : "")) + + const answer = await terminal.singleColumnMenu(finalCheckerNameList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + const checker = checkerList[answer.selectedIndex] + const configData = checker.config.data as api.ODValidJsonType + await chooseConfigStructure(checker,async () => {await renderConfigSelector(backFn)},checker.structure,configData,{},NaN,["("+checker.config.path+")"]) +} + +async function chooseConfigStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStructure,data:api.ODValidJsonType,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + if (structure instanceof api.ODCheckerObjectStructure && typeof data == "object" && !Array.isArray(data) && data) await renderConfigObjectStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerEnabledObjectStructure && typeof data == "object" && !Array.isArray(data) && data) await renderConfigEnabledObjectStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerObjectSwitchStructure && typeof data == "object" && !Array.isArray(data) && data) await renderConfigObjectSwitchStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerArrayStructure && Array.isArray(data)) await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerBooleanStructure && typeof data == "boolean") await renderConfigBooleanStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerNumberStructure && typeof data == "number") await renderConfigNumberStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerStringStructure && typeof data == "string") await renderConfigStringStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerNullStructure && data === null) await renderConfigNullStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerTypeSwitchStructure) await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else terminal.red.bold("❌ Unable to detect type of variable! Please try to edit this property in the JSON file itself!") +} + +async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("Please select which variable you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + if (!structure.options.children) return await backFn() + + const list = structure.options.children.filter((child) => !child.cliHideInEditMode) + const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName+" ("+child.key+")" : child.key)) + const nameLength = utilities.getLongestLength(nameList) + const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray(list[index].checker.options.cliDisplayDescription ? "=> "+list[index].checker.options.cliDisplayDescription : "")) + + + const answer = await terminal.singleColumnMenu(finalnameList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + const subStructure = list[answer.selectedIndex] + const subData = data[subStructure.key] + await chooseConfigStructure(checker,async () => {await renderConfigObjectStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},subStructure.checker,subData,data,subStructure.key,[...path,subStructure.key]) +} + +async function renderConfigEnabledObjectStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + const enabledProperty = structure.options.property + const subStructure = structure.options.checker + if (!enabledProperty || !subStructure || !subStructure.options.children) return await backFn() + + if (!subStructure.options.children.find((child) => child.key === structure.options.property)){ + if (typeof structure.options.enabledValue == "string") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerStringStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + else if (typeof structure.options.enabledValue == "number") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerNumberStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + else if (typeof structure.options.enabledValue == "boolean") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + } + + await chooseConfigStructure(checker,backFn,subStructure,data,parent,parentIndex,path) +} + +async function renderConfigObjectSwitchStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (!structure.options.objects) return await backFn() + + let didMatch: boolean = false + for (const objectTemplate of structure.options.objects){ + if (objectTemplate.properties.every((prop) => data[prop.key] === prop.value)){ + //object template matches data + const subStructure = objectTemplate.checker + didMatch = true + await chooseConfigStructure(checker,backFn,subStructure,data,parent,parentIndex,path) + } + } + if (!didMatch) throw new Error("OT CLI => Unable to detect type of object in the object switch. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") +} + +async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerArrayStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + if (!Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'array'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("Please select what you would like to do.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + if (!structure.options.propertyChecker) return await backFn() + + const propertyName = structure.options.cliDisplayPropertyName ?? "index" + const answer = await terminal.singleColumnMenu([ + "Add "+propertyName, + "Edit "+propertyName, + "Move "+propertyName, + "Remove "+propertyName, + "Duplicate "+propertyName + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + if (answer.selectedIndex == 0){ + //TODO => add => trigger chooseConfigStructure() function but for "addition instead of editing" + }else if (answer.selectedIndex == 1) await renderConfigArrayStructureEditSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 2) await renderconfigArrayStructureMoveSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 3) await renderconfigArrayStructureRemoveSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 4){ + //TODO => duplicate trigger chooseConfigStructure() function but for "addition instead of editing" + } + +} + +async function renderConfigArrayStructureEditSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + const propertyName = arrayStructure.options.cliDisplayPropertyName ?? "index" + renderHeader(path) + terminal(ansis.bold.green("Please select the "+propertyName+" you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const longestDataListName = Math.max(...data.map((d,i) => getArrayPreviewStructureNameLength(structure,d,data,i))) + const dataList = data.map((d,i) => (i+1)+". "+getArrayPreviewFromStructure(structure,d,data,i,longestDataListName)) + const dataAnswer = await terminal.singleColumnMenu(dataList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (dataAnswer.canceled) return await backFn() + const subData = data[dataAnswer.selectedIndex] + await chooseConfigStructure(checker,async () => {await renderConfigArrayStructureEditSelector(checker,backFn,arrayStructure,structure,data,parent,parentIndex,path)},structure,subData,data,dataAnswer.selectedIndex,[...path,dataAnswer.selectedIndex]) +} + +async function renderconfigArrayStructureMoveSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + const propertyName = arrayStructure.options.cliDisplayPropertyName ?? "index" + renderHeader(path) + terminal(ansis.bold.green("Please select the "+propertyName+" you would like to move.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const longestDataListName = Math.max(...data.map((d,i) => getArrayPreviewStructureNameLength(structure,d,data,i))) + const dataList = data.map((d,i) => (i+1)+". "+getArrayPreviewFromStructure(structure,d,data,i,longestDataListName)) + const dataAnswer = await terminal.singleColumnMenu(dataList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (dataAnswer.canceled) return await backFn() + const subData = data[dataAnswer.selectedIndex] + + renderHeader([...path,dataAnswer.selectedIndex]) + terminal(ansis.bold.green("Please select the position you would like to move to.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const moveAnswer = await terminal.singleColumnMenu([...data.map((d,i) => "Position "+(i+1)),"Last Position"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (moveAnswer.canceled) return await renderconfigArrayStructureMoveSelector(checker,backFn,arrayStructure,structure,data,parent,parentIndex,path) + console.log("move from:",dataAnswer.selectedIndex,"to:",moveAnswer.selectedIndex) +} + +async function renderconfigArrayStructureRemoveSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + const propertyName = arrayStructure.options.cliDisplayPropertyName ?? "index" + renderHeader(path) + terminal(ansis.bold.green("Please select the "+propertyName+" you would like to delete.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const longestDataListName = Math.max(...data.map((d,i) => getArrayPreviewStructureNameLength(structure,d,data,i))) + const dataList = data.map((d,i) => (i+1)+". "+getArrayPreviewFromStructure(structure,d,data,i,longestDataListName)) + const dataAnswer = await terminal.singleColumnMenu(dataList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (dataAnswer.canceled) return await backFn() + const subData = data[dataAnswer.selectedIndex] + console.log("delete position:",dataAnswer.selectedIndex) +} + +async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,data:boolean,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "boolean") throw new Error("OT CLI => Property is not of the type 'boolean'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nCurrent value: "+ansis.bold[data ? "green" : "red"](data.toString())+"\n") + + const answer = await terminal.singleColumnMenu(["false (Disabled)","true (Enabled)"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run config checker + const newValue = (answer.selectedIndex == 0) ? false : true + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.gray("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigBooleanStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigNumberStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,data:number,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "number") throw new Error("OT CLI => Property is not of the type 'number'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + + terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") + + const answer = await terminal.inputField({ + style:terminal.cyan, + cancelable:true + }).promise + + if (typeof answer != "string") return await backFn() + + //run config checker + const newValue = Number(answer.replaceAll(",",".")) + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigNumberStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,data:string,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "string") throw new Error("OT CLI => Property is not of the type 'string'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + + terminal.gray("\nCurrent value:"+(data.includes("\n") ? "\n" : " ")+ansis.bold.blue(data)+"\n") + + const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices + const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { + style:terminal.white, + selectedStyle:terminal.bgBlue.white + } + + const answer = await terminal.inputField({ + style:terminal.cyan, + hintStyle:terminal.gray, + cancelable:true, + autoComplete:autocompleteList, + autoCompleteHint:(!!autocompleteList), + autoCompleteMenu:(autocompleteList) ? autoCompleteMenuOpts as Terminal.Autocompletion : false + }).promise + + if (typeof answer != "string") return await backFn() + + //run config checker + const newValue = answer.replaceAll("\\n","\n") + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigStringStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigNullStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerNullStructure,data:null,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (data !== null) throw new Error("OT CLI => Property is not of the type 'null'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nCurrent value:"+ansis.bold.blue("null")+"\n") + + const answer = await terminal.singleColumnMenu(["null"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run config checker + const newValue = null + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigNullStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigTypeSwitchStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerTypeSwitchStructure,data:any,parent:object,parentIndex:string|number,path:(string|number)[]){ + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") + + const actionsList: string[] = [] + if (structure.options.boolean) actionsList.push("Edit as boolean") + if (structure.options.string) actionsList.push("Edit as string") + if (structure.options.number) actionsList.push("Edit as number") + if (structure.options.object) actionsList.push("Edit as object") + if (structure.options.array) actionsList.push("Edit as array/list") + if (structure.options.null) actionsList.push("Edit as null") + + const answer = await terminal.singleColumnMenu(actionsList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run selected structure editor (untested) + if (answer.selectedText.startsWith("Edit as boolean") && structure.options.boolean) await renderConfigBooleanStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.boolean,false,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as string") && structure.options.string) await renderConfigStringStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.string,data.toString(),parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as number") && structure.options.number) await renderConfigNumberStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.number,0,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as object") && structure.options.object) await renderConfigObjectStructureSelector(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.object,data,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as array/list") && structure.options.array) await renderConfigArrayStructureSelector(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.array,data,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as null") && structure.options.null) await renderConfigNullStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.null,null,parent,parentIndex,path) +} + +function getArrayPreviewStructureNameLength(structure:api.ODCheckerStructure,data:api.ODValidJsonType,parent:object,parentIndex:string|number): number { + if (structure instanceof api.ODCheckerBooleanStructure && typeof data == "boolean") return data.toString().length + else if (structure instanceof api.ODCheckerNumberStructure && typeof data == "number") return data.toString().length + else if (structure instanceof api.ODCheckerStringStructure && typeof data == "string") return data.length + else if (structure instanceof api.ODCheckerNullStructure && data === null) return "Null".length + else if (structure instanceof api.ODCheckerArrayStructure && Array.isArray(data)) return "Array".length + else if (structure instanceof api.ODCheckerObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + if (!structure.options.cliDisplayKeyInParentArray) return "Object".length + else return data[structure.options.cliDisplayKeyInParentArray].toString().length + + }else if (structure instanceof api.ODCheckerEnabledObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + const subStructure = structure.options.checker + if (!subStructure) return "".length + return getArrayPreviewStructureNameLength(subStructure,data,parent,parentIndex) + + }else if (structure instanceof api.ODCheckerObjectSwitchStructure && typeof data == "object" && !Array.isArray(data) && data){ + for (const objectTemplate of (structure.options.objects ?? [])){ + if (objectTemplate.properties.every((prop) => data[prop.key] === prop.value)){ + //object template matches data + const subStructure = objectTemplate.checker + return getArrayPreviewStructureNameLength(subStructure,data,parent,parentIndex) + } + } + return "".length + + }else if (structure instanceof api.ODCheckerTypeSwitchStructure){ + if (typeof data == "boolean" && structure.options.boolean) return getArrayPreviewStructureNameLength(structure.options.boolean,data,parent,parentIndex) + else if (typeof data == "number" && structure.options.number) return getArrayPreviewStructureNameLength(structure.options.number,data,parent,parentIndex) + else if (typeof data == "string" && structure.options.string) return getArrayPreviewStructureNameLength(structure.options.string,data,parent,parentIndex) + else if (typeof data == "object" && !Array.isArray(data) && data && structure.options.object) return getArrayPreviewStructureNameLength(structure.options.object,data,parent,parentIndex) + else if (Array.isArray(data) && structure.options.array) return getArrayPreviewStructureNameLength(structure.options.array,data,parent,parentIndex) + else if (data === null && structure.options.null) return getArrayPreviewStructureNameLength(structure.options.null,data,parent,parentIndex) + else return "".length + }else return "".length +} + +function getArrayPreviewFromStructure(structure:api.ODCheckerStructure,data:api.ODValidJsonType,parent:object,parentIndex:string|number,nameLength:number): string { + if (structure instanceof api.ODCheckerBooleanStructure && typeof data == "boolean") return data.toString() + else if (structure instanceof api.ODCheckerNumberStructure && typeof data == "number") return data.toString() + else if (structure instanceof api.ODCheckerStringStructure && typeof data == "string") return data + else if (structure instanceof api.ODCheckerNullStructure && data === null) return "Null" + else if (structure instanceof api.ODCheckerArrayStructure && Array.isArray(data)) return "Array" + else if (structure instanceof api.ODCheckerObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + const additionalKeys = (structure.options.cliDisplayAdditionalKeysInParentArray ?? []).map((key) => key+": "+data[key].toString()).join(", ") + if (!structure.options.cliDisplayKeyInParentArray) return "Object" + else return data[structure.options.cliDisplayKeyInParentArray].toString().padEnd(nameLength+5," ")+ansis.gray(additionalKeys.length > 0 ? "("+additionalKeys+")" : "") + + }else if (structure instanceof api.ODCheckerEnabledObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + const subStructure = structure.options.checker + if (!subStructure) return "" + return getArrayPreviewFromStructure(subStructure,data,parent,parentIndex,nameLength) + + }else if (structure instanceof api.ODCheckerObjectSwitchStructure && typeof data == "object" && !Array.isArray(data) && data){ + for (const objectTemplate of (structure.options.objects ?? [])){ + if (objectTemplate.properties.every((prop) => data[prop.key] === prop.value)){ + //object template matches data + const subStructure = objectTemplate.checker + return getArrayPreviewFromStructure(subStructure,data,parent,parentIndex,nameLength) + } + } + return "" + + }else if (structure instanceof api.ODCheckerTypeSwitchStructure){ + if (typeof data == "boolean" && structure.options.boolean) return getArrayPreviewFromStructure(structure.options.boolean,data,parent,parentIndex,nameLength) + else if (typeof data == "number" && structure.options.number) return getArrayPreviewFromStructure(structure.options.number,data,parent,parentIndex,nameLength) + else if (typeof data == "string" && structure.options.string) return getArrayPreviewFromStructure(structure.options.string,data,parent,parentIndex,nameLength) + else if (typeof data == "object" && !Array.isArray(data) && data && structure.options.object) return getArrayPreviewFromStructure(structure.options.object,data,parent,parentIndex,nameLength) + else if (Array.isArray(data) && structure.options.array) return getArrayPreviewFromStructure(structure.options.array,data,parent,parentIndex,nameLength) + else if (data === null && structure.options.null) return getArrayPreviewFromStructure(structure.options.null,data,parent,parentIndex,nameLength) + else return "" + }else return "" +} + +export async function execute(){ + if (terminal.width < 100 || terminal.height < 35){ + terminal(ansis.red.bold("\n\nMake sure your console or cmd window has a "+ansis.cyan("minimum width & height")+" of "+ansis.cyan("100x35")+" characters.")) + terminal(ansis.red.bold("\nOtherwise the Open Ticket Interactive Setup CLI will be rendered incorrectly.")) + terminal(ansis.red.bold("\nThe current terminal dimensions are: "+ansis.cyan(terminal.width+"x"+terminal.height)+".")) + }else await renderConfigSelector(terminate) +} \ No newline at end of file diff --git a/src/core/startup/init.ts b/src/core/startup/init.ts index 0ae357e..66279de 100644 --- a/src/core/startup/init.ts +++ b/src/core/startup/init.ts @@ -31,12 +31,13 @@ moduleInstalled("discord.js",true) moduleInstalled("ansis",true) moduleInstalled("formatted-json-stringify",true) moduleInstalled("typescript",true) +moduleInstalled("terminal-kit",true) tempError() //init API import * as api from "../api/api" //import for local use export * as api from "../api/api" //export to other parts of bot - +import ansis from "ansis" //import ansis for usage in initialization export const opendiscord = new api.ODMain() console.log("\n--------------------------- OPEN TICKET STARTUP ---------------------------") @@ -103,6 +104,10 @@ export interface ODUtilities { * Same as `string.replace(search, value)` but with async compatibility */ asyncReplace(text:string, regex:RegExp, func:(value:string,...args:any[]) => Promise): Promise + /**## getLongestLength `utility function` + * Get the length of the longest string in the array. + */ + getLongestLength(text:string[]): number /**## easterEggs `utility object` * Object containing data for Open Ticket easter eggs. */ @@ -208,6 +213,9 @@ export const utilities: ODUtilities = { }) return result }, + getLongestLength(texts:string[]): number { + return Math.max(...texts.map((t) => ansis.strip(t).length)) + }, easterEggs:{ /* THANK YOU TO ALL OUR CONTRIBUTORS!!! */ creator:"779742674932072469", //DJj123dj diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 8955ae9..44d94ba 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -3,11 +3,11 @@ import {opendiscord, api, utilities} from "../../index" const generalConfig = opendiscord.configs.get("opendiscord:general") export const loadAllConfigCheckers = async () => { - opendiscord.checkers.add(new api.ODChecker("opendiscord:general",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:general"),defaultGeneralStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:options",opendiscord.checkers.storage,1,opendiscord.configs.get("opendiscord:options"),defaultOptionsStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:panels",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:panels"),defaultPanelsStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:questions",opendiscord.checkers.storage,2,opendiscord.configs.get("opendiscord:questions"),defaultQuestionsStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:transcripts",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:transcripts"),defaultTranscriptsStructure)) + opendiscord.checkers.add(new api.ODChecker("opendiscord:general",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:general"),defaultGeneralStructure,{cliDisplayName:"General Config",cliDisplayDescription:"Configure the bot token, status, colors, permissions & more."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:questions",opendiscord.checkers.storage,2,opendiscord.configs.get("opendiscord:questions"),defaultQuestionsStructure,{cliDisplayName:"Questions Config",cliDisplayDescription:"Create, modify & delete questions which are used in options."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:options",opendiscord.checkers.storage,1,opendiscord.configs.get("opendiscord:options"),defaultOptionsStructure,{cliDisplayName:"Options Config",cliDisplayDescription:"Create, modify & delete options which are used in panels."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:panels",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:panels"),defaultPanelsStructure,{cliDisplayName:"Panels Config",cliDisplayDescription:"Create, modify & delete panels which can be spawned in discord."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:transcripts",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:transcripts"),defaultTranscriptsStructure,{cliDisplayName:"Transcript Config",cliDisplayDescription:"Configure everything related to transcripts."})) } export const loadAllConfigCheckerFunctions = async () => { @@ -116,7 +116,7 @@ export const registerDefaultCheckerCustomTranslations = (tm:api.ODCheckerTransla tm.quickTranslate(lm,"checker.messages.dropdownOption","message","opendiscord:dropdown-option") // A panel with dropdown enabled can only contain options of the 'ticket' type! //TODO TRANSLATION!!! - //tm.quickTranslate(lm,"checker.messages.TODO","message","opendiscord:invalid-version") // The version specified in your config is invalid! Make sure you have updated it to the latest version! + //tm.quickTranslate(lm,"checker.messages.TODO","message","opendiscord:invalid-version") // The version specified in your config does not match! Make sure you have updated the config to the latest version! } //UTILITY FUNCTIONS @@ -134,7 +134,7 @@ const createTicketEmbedStructure = (id:api.ODValidId) => { {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, - {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-embed-fields",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-embed-fields",{children:[ + {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-embed-fields",{children:[ {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-name",{minLength:1,maxLength:256})}, {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-value",{minLength:1,maxLength:1024})}, {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-field-inline",{})} @@ -146,7 +146,7 @@ const createTicketPingStructure = (id:api.ODValidId) => { return new api.ODCheckerObjectStructure(id,{children:[ {key:"@here",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-here",{})}, {key:"@everyone",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-everyone",{})}, - {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false})}, + {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false,cliDisplayPropertyName:"custom role id"})}, ]}) } const createPanelEmbedStructure = (id:api.ODValidId) => { @@ -160,7 +160,7 @@ const createPanelEmbedStructure = (id:api.ODValidId) => { {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, {key:"footer",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-footer",{maxLength:2048})}, - {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:panel-embed-fields",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panel-embed-fields",{children:[ + {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:panel-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panel-embed-fields",{children:[ {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-name",{minLength:1,maxLength:256})}, {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-value",{minLength:1,maxLength:1024})}, {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-field-inline",{})} @@ -178,7 +178,7 @@ function loadFromEnv(){ //STRUCTURES export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendiscord:general",{children:[ //STATUS - {key:"_INFO",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:info",{children:[ + {key:"_INFO",optional:false,priority:0,cliHideInEditMode:true,checker:new api.ODCheckerObjectStructure("opendiscord:info",{children:[ {key:"support",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-support",{choices:["https://otdocs.dj-dj.be"]})}, {key:"discord",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-discord",{choices:["https://discord.dj-dj.be"]})}, {key:"version",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-version",{custom(checker,value,locationTrace,locationId,locationDocs) { @@ -186,14 +186,14 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis if (typeof value != "string") return false else if (value != "open-ticket-"+opendiscord.versions.get("opendiscord:version").toString()){ - checker.createMessage("opendiscord:invalid-version","warning","The version specified in your config is invalid! Make sure you have updated it to the latest version!",lt,null,[],locationId,locationDocs) + checker.createMessage("opendiscord:invalid-version","warning","The version specified in your config does not match! Make sure you have updated the config to the latest version!",lt,null,[],locationId,locationDocs) return false }else return true },})}, ]})}, //BASIC - {key:"token",optional:false,priority:0,checker:(loadFromEnv()) ? new api.ODCheckerStringStructure("opendiscord:token-disabled",{}) : new api.ODCheckerCustomStructure_DiscordToken("opendiscord:token")}, + {key:"token",optional:false,priority:0,checker:(loadFromEnv()) ? new api.ODCheckerStringStructure("opendiscord:token-disabled",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."}) : new api.ODCheckerCustomStructure_DiscordToken("opendiscord:token",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."})}, {key:"tokenFromENV",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:token-env",{})}, {key:"mainColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:main-color",true,false)}, {key:"language",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:language",{ @@ -206,10 +206,11 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis return false }else return true }, + cliAutocompleteList:opendiscord.defaults.getDefault("languageList") })}, {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:prefix",{minLength:1})}, {key:"serverId",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:server-id","server",false,[])}, - {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false})}, + {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role"})}, {key:"slashCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:slash-commands",{})}, {key:"textCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:text-commands",{})}, @@ -244,13 +245,9 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"enableTicketActionWithReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-action-with-reason",{})}, {key:"enableDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-delete-without-transcript",{})}, - {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{ - property:"enabled", - enabledValue:true, - checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:log-channel","channel",false,[])}, - ]}) - })}, + {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:log-channel","channel",false,[])}, + ]})})}, {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:limits",{children:[ {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})}, @@ -297,9 +294,9 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis ]})} ]}) -export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ +export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],cliDisplayPropertyName:"option",propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ //TICKET - {name:"ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{children:[ + {name:"ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-name",{minLength:2,maxLength:50})}, {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-description",{maxLength:256})}, @@ -321,10 +318,10 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc }})}, //TICKET ADMINS - {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false})}, - {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false})}, + {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role"})}, + {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"read-only ticket admin role"})}, {key:"allowCreationByBlacklistedUsers",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-allow-blacklisted-users",{})}, - {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5})}, + {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question"})}, //TICKET CHANNEL {key:"channel",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{children:[ @@ -334,7 +331,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-category","category",true,[])}, {key:"closedCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-closed-category","category",true,[])}, {key:"backupCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-backup-category","category",true,[])}, - {key:"claimedCategory",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel-claimed-category",{children:[ + {key:"claimedCategory",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],cliDisplayPropertyName:"claim category",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel-claimed-category",{children:[ {key:"user",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-user","user",false,[])}, {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-category","category",false,[])} ]})})}, @@ -383,7 +380,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc ]})}, //WEBSITE - {name:"website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-website",{children:[ + {name:"website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-website",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:website-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-name",{minLength:2,maxLength:50})}, {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-description",{maxLength:256})}, @@ -408,7 +405,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc ]})}, //REACTION ROLES - {name:"role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-role",{children:[ + {name:"role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-role",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:role-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-name",{minLength:2,maxLength:50})}, {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-description",{maxLength:256})}, @@ -430,18 +427,18 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc }})}, //ROLE SETTINGS - {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1})}, + {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1,cliDisplayPropertyName:"role"})}, {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-mode",{choices:["add","remove","add&remove"]})}, - {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false})}, + {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false,cliDisplayPropertyName:"role"})}, {key:"addOnMemberJoin",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:role-add-on-join",{})}, ]})}, ]})}) -export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendiscord:panels",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panels",{children:[ +export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendiscord:panels",{allowedTypes:["object"],cliDisplayPropertyName:"panel",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panels",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","dropdown"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-name",{minLength:3,maxLength:50})}, {key:"dropdown",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-dropdown",{})}, - {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25})}, + {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25,cliDisplayPropertyName:"option"})}, //EMBED & TEXT {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-text",{maxLength:4096})}, @@ -461,7 +458,7 @@ export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendisco ]})}, ]})}) -export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("opendiscord:questions",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:questions",{children:[ +export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("opendiscord:questions",{allowedTypes:["object"],cliDisplayPropertyName:"question",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:questions",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-name",{minLength:3,maxLength:45})}, {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-type",{choices:["short","paragraph"]})}, @@ -604,4 +601,14 @@ export const defaultDropdownOptionsFunction = (manager:api.ODCheckerManager, fun }) return {valid:(final.length < 1),messages:final} -} \ No newline at end of file +} + +/* TODO + * - add & duplicate functionality for arrays (complex) + * - move & remove functionality for arrays (simple) + * - add/duplicate generators + forms for each structure type + * - implement default value & skip values in all structures + * - when sub-arrays are required to be configured in an add/duplicate generator, it will have an extra "next" option in the menu to go back to the next property to configure. + * - when sub-objects are required to be configured in an add/duplicate generator, it will have an extra "next" option in the menu to go back to the next property to configure. + * - and more + */ \ No newline at end of file diff --git a/src/data/framework/flagLoader.ts b/src/data/framework/flagLoader.ts index 6a70f40..9c80a99 100644 --- a/src/data/framework/flagLoader.ts +++ b/src/data/framework/flagLoader.ts @@ -16,4 +16,5 @@ export const loadAllFlags = async () => { opendiscord.flags.add(new api.ODFlag("opendiscord:no-compile","No Compile","Disable compilation of plugins & bot before starting.","--no-compile",[])) opendiscord.flags.add(new api.ODFlag("opendiscord:compile-only","Compile Only","This description will never be shown because the bot wouldn't run when this flag is enabled :)","--compile-only",[])) opendiscord.flags.add(new api.ODFlag("opendiscord:silent","Silent Mode","Run the bot without displaying logs. The startscreen will still be displayed.","--silent",[])) + opendiscord.flags.add(new api.ODFlag("opendiscord:cli","Run CLI","This description will never be shown because the bot wouldn't run when this flag is enabled :)","--cli",[])) } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 33ed912..d2c0ca9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -269,9 +269,10 @@ const main = async () => { //render config checker const advancedCheckerFlag = opendiscord.flags.get("opendiscord:checker") const disableCheckerFlag = opendiscord.flags.get("opendiscord:no-checker") + const useCliFlag = opendiscord.flags.get("opendiscord:cli") await opendiscord.events.get("onCheckerRender").emit([opendiscord.checkers.renderer,opendiscord.checkers]) - if (opendiscord.defaults.getDefault("checkerRendering") && !(disableCheckerFlag ? disableCheckerFlag.value : false)){ + if (opendiscord.defaults.getDefault("checkerRendering") && !(disableCheckerFlag ? disableCheckerFlag.value : false) && !(useCliFlag ? useCliFlag.value : false)){ //check if there is a result (otherwise throw minor error) const result = opendiscord.checkers.lastResult if (!result) return opendiscord.log("Failed to render Config Checker! (couldn't fetch result)","error") @@ -290,7 +291,7 @@ const main = async () => { } //quit config checker (when required) - if (opendiscord.checkers.lastResult && !opendiscord.checkers.lastResult.valid && !(disableCheckerFlag ? disableCheckerFlag.value : false)){ + if (opendiscord.checkers.lastResult && !opendiscord.checkers.lastResult.valid && !(disableCheckerFlag ? disableCheckerFlag.value : false) && !(useCliFlag ? useCliFlag.value : false)){ await opendiscord.events.get("onCheckerQuit").emit([opendiscord.checkers]) if (opendiscord.defaults.getDefault("checkerQuit")){ process.exit(1) @@ -298,6 +299,14 @@ const main = async () => { } } + //switch to CLI context instead of running the bot + if (useCliFlag && useCliFlag.value){ + await (await (import("./core/startup/cli.js"))).execute() + await utilities.timer(1000) + console.log("\n\n"+ansis.red("❌ Something went wrong in the Interactive Setup CLI. Please try again or report a bug in our discord server.")) + process.exit(0) + } + //plugin loading before client await opendiscord.events.get("onPluginBeforeClientLoad").emit([]) await opendiscord.events.get("afterPluginBeforeClientLoaded").emit([]) From d545d21bb19cdee66aedc41c7bd9e0150d2dd3a0 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:47:57 +0200 Subject: [PATCH 04/78] Update cli.ts --- src/core/startup/cli.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index acbd8f4..2eb184c 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -146,7 +146,7 @@ async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:( if (!structure.options.propertyChecker) return await backFn() const propertyName = structure.options.cliDisplayPropertyName ?? "index" - const answer = await terminal.singleColumnMenu([ + const answer = await terminal.singleColumnMenu(data.length < 1 ? ["Add "+propertyName] : [ "Add "+propertyName, "Edit "+propertyName, "Move "+propertyName, @@ -211,12 +211,11 @@ async function renderconfigArrayStructureMoveSelector(checker:api.ODChecker,back }).promise if (dataAnswer.canceled) return await backFn() - const subData = data[dataAnswer.selectedIndex] renderHeader([...path,dataAnswer.selectedIndex]) terminal(ansis.bold.green("Please select the position you would like to move to.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) - const moveAnswer = await terminal.singleColumnMenu([...data.map((d,i) => "Position "+(i+1)),"Last Position"],{ + const moveAnswer = await terminal.singleColumnMenu(data.map((d,i) => "Position "+(i+1)),{ leftPadding:"> ", style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, @@ -226,7 +225,14 @@ async function renderconfigArrayStructureMoveSelector(checker:api.ODChecker,back }).promise if (moveAnswer.canceled) return await renderconfigArrayStructureMoveSelector(checker,backFn,arrayStructure,structure,data,parent,parentIndex,path) - console.log("move from:",dataAnswer.selectedIndex,"to:",moveAnswer.selectedIndex) + + const subData = data[dataAnswer.selectedIndex] + const slicedData = [...data.slice(0,dataAnswer.selectedIndex),...data.slice(dataAnswer.selectedIndex+1)] + const insertedData = [...slicedData.slice(0,moveAnswer.selectedIndex),subData,...slicedData.slice(moveAnswer.selectedIndex)] + insertedData.forEach((d,i) => data[i] = d) + terminal.bold.blue("\n\n✅ Property moved succesfully!") + await utilities.timer(400) + await backFn() } async function renderconfigArrayStructureRemoveSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ @@ -246,8 +252,10 @@ async function renderconfigArrayStructureRemoveSelector(checker:api.ODChecker,ba }).promise if (dataAnswer.canceled) return await backFn() - const subData = data[dataAnswer.selectedIndex] - console.log("delete position:",dataAnswer.selectedIndex) + data.splice(dataAnswer.selectedIndex,1) + terminal.bold.blue("\n\n✅ Property deleted succesfully!") + await utilities.timer(400) + await backFn() } async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,data:boolean,parent:object,parentIndex:string|number,path:(string|number)[]){ From 1fddad1e96e9c24e6f03c98b1b62e2ae323d2969 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:49:59 +0200 Subject: [PATCH 05/78] Improved TS interfaces for the config checker --- src/core/api/modules/checker.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index cccc4e2..c452391 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -410,7 +410,7 @@ export interface ODCheckerStructureOptions { /**The description of this config in the Interactive Setup CLI. */ cliDisplayDescription?:string /**The default value of this variable when creating it in the Interactive Setup CLI. When not specified, the user will be asked to insert a value. */ - cliInitDefaultValue?:string + cliInitDefaultValue?:ODValidJsonType } /**## ODCheckerStructure `class` @@ -445,7 +445,7 @@ export class ODCheckerStructure { */ export interface ODCheckerObjectStructureOptions extends ODCheckerStructureOptions { /**Add a checker for a property in an object (can also be optional) */ - children?:{key:string, priority:number, optional:boolean, cliHideInEditMode?:boolean, checker:ODCheckerStructure}[], + children:{key:string, priority:number, optional:boolean, cliHideInEditMode?:boolean, checker:ODCheckerStructure}[], /**A list of keys to skip when creating this object with the Interactive Setup CLI. The default value of these properties will be used instead. */ cliInitSkipKeys?:string[], /**The key of a (primitive) property in this object to show the value of in the Interactive Setup CLI when listed in an array. */ @@ -900,7 +900,7 @@ export interface ODCheckerTypeSwitchStructureOptions extends ODCheckerStructureO /**A checker when the property is something else */ other?:ODCheckerStructure, /**A list of allowed types */ - allowedTypes?:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[] + allowedTypes:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[] } /**## ODCheckerTypeSwitchStructure `class` @@ -954,9 +954,9 @@ export class ODCheckerTypeSwitchStructure extends ODCheckerStructure { */ export interface ODCheckerObjectSwitchStructureOptions extends ODCheckerStructureOptions { /**An array of object checkers with their name, properties & priority. */ - objects?:{ + objects:{ /**The properties to match for this checker to be used. */ - properties:{key:string, value:any}[], + properties:{key:string, value:boolean|string|number}[], /**The name for this object type (used in rendering) */ name:string, /**The higher the priority, the earlier this checker will be tested. */ @@ -1022,11 +1022,11 @@ export class ODCheckerObjectSwitchStructure extends ODCheckerStructure { */ export interface ODCheckerEnabledObjectStructureOptions extends ODCheckerStructureOptions { /**The name of the property to match the `enabledValue`. */ - property?:string, - /**The value of the property to be enabled. Defaults to `true` */ - enabledValue?:any, + property:string, + /**The value of the property to be enabled. (e.g. `true`) */ + enabledValue:boolean|string|number, /**The object checker to use once the property has been matched. */ - checker?:ODCheckerObjectStructure + checker:ODCheckerObjectStructure } /**## ODCheckerEnabledObjectStructure `class` From 30352b570ac1d1b55add53d2172db10aa0da4e3c Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:51:01 +0200 Subject: [PATCH 06/78] Updated Interactive Setup CLI (Unfinished Part 2) --- src/core/startup/cli.ts | 334 +++++++++++++++++++++++++--- src/data/framework/checkerLoader.ts | 12 +- 2 files changed, 307 insertions(+), 39 deletions(-) diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 2eb184c..4d24424 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -82,7 +82,7 @@ async function chooseConfigStructure(checker:api.ODChecker,backFn:(() => api.ODP } async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ - if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (typeof data != "object" || Array.isArray(data)) throw new api.ODSystemError("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") renderHeader(path) terminal(ansis.bold.green("Please select which variable you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) if (!structure.options.children) return await backFn() @@ -91,12 +91,12 @@ async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn: const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName+" ("+child.key+")" : child.key)) const nameLength = utilities.getLongestLength(nameList) const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray(list[index].checker.options.cliDisplayDescription ? "=> "+list[index].checker.options.cliDisplayDescription : "")) - + const answer = await terminal.singleColumnMenu(finalnameList,{ leftPadding:"> ", style:terminal.cyan, - selectedStyle:terminal.bgDefaultColor.bold, + selectedStyle:terminal.bgDefaultColor.bold.defaultColor, submittedStyle:terminal.bgBlue, extraLines:2, cancelable:true @@ -109,7 +109,7 @@ async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn: } async function renderConfigEnabledObjectStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ - if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (typeof data != "object" || Array.isArray(data)) throw new api.ODSystemError("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") const enabledProperty = structure.options.property const subStructure = structure.options.checker if (!enabledProperty || !subStructure || !subStructure.options.children) return await backFn() @@ -124,7 +124,7 @@ async function renderConfigEnabledObjectStructureSelector(checker:api.ODChecker, } async function renderConfigObjectSwitchStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ - if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (typeof data != "object" || Array.isArray(data)) throw new api.ODSystemError("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") if (!structure.options.objects) return await backFn() let didMatch: boolean = false @@ -136,11 +136,11 @@ async function renderConfigObjectSwitchStructureSelector(checker:api.ODChecker,b await chooseConfigStructure(checker,backFn,subStructure,data,parent,parentIndex,path) } } - if (!didMatch) throw new Error("OT CLI => Unable to detect type of object in the object switch. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (!didMatch) throw new api.ODSystemError("OT CLI => Unable to detect type of object in the object switch. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") } async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerArrayStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ - if (!Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'array'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (!Array.isArray(data)) throw new api.ODSystemError("OT CLI => Property is not of the type 'array'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") renderHeader(path) terminal(ansis.bold.green("Please select what you would like to do.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) if (!structure.options.propertyChecker) return await backFn() @@ -161,12 +161,16 @@ async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:( cancelable:true }).promise + const backFnFunc = async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)} + if (answer.canceled) return await backFn() - if (answer.selectedIndex == 0){ - //TODO => add => trigger chooseConfigStructure() function but for "addition instead of editing" - }else if (answer.selectedIndex == 1) await renderConfigArrayStructureEditSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) - else if (answer.selectedIndex == 2) await renderconfigArrayStructureMoveSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) - else if (answer.selectedIndex == 3) await renderconfigArrayStructureRemoveSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) + if (answer.selectedIndex == 0) await chooseAdditionConfigStructure(checker,backFnFunc,async (newData) => { + data[data.length] = newData + await backFnFunc() + },structure.options.propertyChecker,data,data.length,path) + else if (answer.selectedIndex == 1) await renderConfigArrayStructureEditSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 2) await renderconfigArrayStructureMoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 3) await renderconfigArrayStructureRemoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) else if (answer.selectedIndex == 4){ //TODO => duplicate trigger chooseConfigStructure() function but for "addition instead of editing" } @@ -226,7 +230,7 @@ async function renderconfigArrayStructureMoveSelector(checker:api.ODChecker,back if (moveAnswer.canceled) return await renderconfigArrayStructureMoveSelector(checker,backFn,arrayStructure,structure,data,parent,parentIndex,path) - const subData = data[dataAnswer.selectedIndex] + const subData = data[dataAnswer.selectedIndex] const slicedData = [...data.slice(0,dataAnswer.selectedIndex),...data.slice(dataAnswer.selectedIndex+1)] const insertedData = [...slicedData.slice(0,moveAnswer.selectedIndex),subData,...slicedData.slice(moveAnswer.selectedIndex)] insertedData.forEach((d,i) => data[i] = d) @@ -259,9 +263,9 @@ async function renderconfigArrayStructureRemoveSelector(checker:api.ODChecker,ba } async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,data:boolean,parent:object,parentIndex:string|number,path:(string|number)[]){ - if (typeof data != "boolean") throw new Error("OT CLI => Property is not of the type 'boolean'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (typeof data != "boolean") throw new api.ODSystemError("OT CLI => Property is not of the type 'boolean'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") renderHeader(path) - terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) terminal.gray("\nCurrent value: "+ansis.bold[data ? "green" : "red"](data.toString())+"\n") @@ -297,14 +301,15 @@ async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:( } } -async function renderConfigNumberStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,data:number,parent:object,parentIndex:string|number,path:(string|number)[]){ - if (typeof data != "number") throw new Error("OT CLI => Property is not of the type 'number'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") +async function renderConfigNumberStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,data:number,parent:object,parentIndex:string|number,path:(string|number)[],prefillValue?:string){ + if (typeof data != "number") throw new api.ODSystemError("OT CLI => Property is not of the type 'number'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") renderHeader(path) - terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") const answer = await terminal.inputField({ + default:prefillValue, style:terminal.cyan, cancelable:true }).promise @@ -328,16 +333,16 @@ async function renderConfigNumberStructureEditor(checker:api.ODChecker,backFn:(( terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") terminal.red("\n"+messages) await utilities.timer(1000+(2000*checker.messages.length)) - await renderConfigNumberStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + await renderConfigNumberStructureEditor(checker,backFn,structure,data,parent,parentIndex,path,answer) } } -async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,data:string,parent:object,parentIndex:string|number,path:(string|number)[]){ - if (typeof data != "string") throw new Error("OT CLI => Property is not of the type 'string'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") +async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,data:string,parent:object,parentIndex:string|number,path:(string|number)[],prefillValue?:string){ + if (typeof data != "string") throw new api.ODSystemError("OT CLI => Property is not of the type 'string'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") renderHeader(path) - terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) - terminal.gray("\nCurrent value:"+(data.includes("\n") ? "\n" : " ")+ansis.bold.blue(data)+"\n") + terminal.gray("\nCurrent value:"+(data.includes("\n") ? "\n" : " \"")+ansis.bold.blue(data)+ansis.gray(!data.includes("\n") ? "\"\n" : "\n")) const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { @@ -346,6 +351,7 @@ async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(( } const answer = await terminal.inputField({ + default:prefillValue, style:terminal.cyan, hintStyle:terminal.gray, cancelable:true, @@ -373,16 +379,16 @@ async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(( terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") terminal.red("\n"+messages) await utilities.timer(1000+(2000*checker.messages.length)) - await renderConfigStringStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + await renderConfigStringStructureEditor(checker,backFn,structure,data,parent,parentIndex,path,answer) } } async function renderConfigNullStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerNullStructure,data:null,parent:object,parentIndex:string|number,path:(string|number)[]){ - if (data !== null) throw new Error("OT CLI => Property is not of the type 'null'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (data !== null) throw new api.ODSystemError("OT CLI => Property is not of the type 'null'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") renderHeader(path) - terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) - terminal.gray("\nCurrent value:"+ansis.bold.blue("null")+"\n") + terminal.gray("\nCurrent value: "+ansis.bold.blue("null")+"\n") const answer = await terminal.singleColumnMenu(["null"],{ leftPadding:"> ", @@ -418,7 +424,7 @@ async function renderConfigNullStructureEditor(checker:api.ODChecker,backFn:(() async function renderConfigTypeSwitchStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerTypeSwitchStructure,data:any,parent:object,parentIndex:string|number,path:(string|number)[]){ renderHeader(path) - terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") @@ -523,6 +529,278 @@ function getArrayPreviewFromStructure(structure:api.ODCheckerStructure,data:api. }else return "" } +async function chooseAdditionConfigStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + if (structure instanceof api.ODCheckerObjectStructure) await renderAdditionConfigObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerBooleanStructure) await renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerNumberStructure) await renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerStringStructure) await renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerNullStructure) await renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerEnabledObjectStructure) await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerObjectSwitchStructure) await renderAdditionConfigObjectSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + //TODO: array, type switch, ... + else await backFn() +} + +async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + const localData = {} + const children = structure.options.children ?? [] + const skipKeys = (structure.options.cliInitSkipKeys ?? []) + //add skipped properties + for (const key of skipKeys){ + const childStructure = children.find((c) => c.key == key) + if (childStructure){ + const defaultValue = childStructure.checker.options.cliInitDefaultValue + if (childStructure.checker instanceof api.ODCheckerBooleanStructure) localData[key] = (typeof defaultValue == "boolean" ? defaultValue : false) + else if (childStructure.checker instanceof api.ODCheckerNumberStructure) localData[key] = (typeof defaultValue == "number" ? defaultValue : 0) + else if (childStructure.checker instanceof api.ODCheckerStringStructure) localData[key] = (typeof defaultValue == "string" ? defaultValue : "") + else if (childStructure.checker instanceof api.ODCheckerNullStructure) localData[key] = (defaultValue === null ? defaultValue : null) + else if (childStructure.checker instanceof api.ODCheckerArrayStructure) localData[key] = (Array.isArray(defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : []) + else if (childStructure.checker instanceof api.ODCheckerObjectStructure) localData[key] = ((typeof defaultValue == "object" && !Array.isArray(defaultValue) && defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : {}) + else if (childStructure.checker instanceof api.ODCheckerObjectSwitchStructure) localData[key] = ((typeof defaultValue == "object" && !Array.isArray(defaultValue) && defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : {}) + else if (childStructure.checker instanceof api.ODCheckerEnabledObjectStructure) localData[key] = ((typeof defaultValue == "object" && !Array.isArray(defaultValue) && defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : {}) + else if (childStructure.checker instanceof api.ODCheckerTypeSwitchStructure && typeof defaultValue != "undefined") localData[key] = JSON.parse(JSON.stringify(defaultValue)) + else throw new api.ODSystemError("OT CLI => Object skip key has an invalid checker structure! key: "+key) + } + } + + //add properties that need to be configured + const configChildren = children.filter((c) => !skipKeys.includes(c.key)).map((c) => {return {key:c.key,checker:c.checker}}) + await configureAdditionObjectProperties(checker,configChildren,0,localData,[...path,parentIndex],async () => { + //go back to previous screen + await backFn() + },async () => { + //finish setup + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await nextFn(localData) + }) +} + +async function configureAdditionObjectProperties(checker:api.ODChecker,children:{key:string,checker:api.ODCheckerStructure}[],currentIndex:number,localData:object,path:(string|number)[],backFn:(() => api.ODPromiseVoid),nextFn:(() => api.ODPromiseVoid)){ + if (children.length < 1) return await nextFn() + + const child = children[currentIndex] + await chooseAdditionConfigStructure(checker,async () => { + if (children[currentIndex-1]) await configureAdditionObjectProperties(checker,children,currentIndex-1,localData,path,backFn,nextFn) + else await backFn() + },async (data) => { + localData[child.key] = data + if (children[currentIndex+1]) await configureAdditionObjectProperties(checker,children,currentIndex+1,localData,path,backFn,nextFn) + else await nextFn() + },child.checker,localData,child.key,path) +} + +async function renderAdditionConfigEnabledObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + const enabledProperty = structure.options.property + const subStructure = structure.options.checker + if (!enabledProperty || !subStructure || !subStructure.options.children) return await backFn() + + if (!subStructure.options.children.find((child) => child.key === structure.options.property)){ + if (typeof structure.options.enabledValue == "string") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerStringStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + else if (typeof structure.options.enabledValue == "number") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerNumberStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + else if (typeof structure.options.enabledValue == "boolean") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + } + + await chooseAdditionConfigStructure(checker,backFn,nextFn,subStructure,parent,parentIndex,path) +} + +async function renderAdditionConfigObjectSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + renderHeader([...path,parentIndex]) + terminal(ansis.bold.green("What type of object would you like to add?\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const answer = await terminal.singleColumnMenu(structure.options.objects.map((obj) => obj.name),{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + const objectTemplate = structure.options.objects[answer.selectedIndex] + + //copy old object checker to new object checker => all options get de-referenced (this is needed for the new object switch properties which are temporary) + const oldStructure = objectTemplate.checker + const newStructure = new api.ODCheckerObjectStructure(oldStructure.id,{children:[]}) + + //copy all options over to the new checker + newStructure.options.children = [...oldStructure.options.children] + newStructure.options.cliInitSkipKeys = [...(oldStructure.options.cliInitSkipKeys ?? [])] + for (const key of Object.keys(oldStructure.options)){ + if (key != "children" && key != "cliInitSkipKeys") newStructure.options[key] = oldStructure.options[key] + } + + //add the keys of the object switch properties to the 'cliInitSkipKeys' because they need to be skipped. + objectTemplate.properties.map((p) => p.key).forEach((p) => { + if (!newStructure.options.cliInitSkipKeys) newStructure.options.cliInitSkipKeys = [p] + else if (!newStructure.options.cliInitSkipKeys.includes(p)) newStructure.options.cliInitSkipKeys.push(p) + }) + + //add structure checkers for all properties + for (const prop of objectTemplate.properties){ + if (!newStructure.options.children.find((child) => child.key === prop.key)){ + if (typeof prop.value == "string") newStructure.options.children.unshift({key:prop.key,optional:false,priority:1,checker:new api.ODCheckerStringStructure("opendiscord:CLI-checker-object-switch-structure",{cliInitDefaultValue:prop.value})}) + else if (typeof prop.value == "number") newStructure.options.children.unshift({key:prop.key,optional:false,priority:1,checker:new api.ODCheckerNumberStructure("opendiscord:CLI-checker-object-switch-structure",{cliInitDefaultValue:prop.value})}) + else if (typeof prop.value == "boolean") newStructure.options.children.unshift({key:prop.key,optional:false,priority:1,checker:new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-object-switch-structure",{cliInitDefaultValue:prop.value})}) + } + } + + await chooseAdditionConfigStructure(checker,backFn,nextFn,newStructure,parent,parentIndex,path) +} + +async function renderAdditionConfigBooleanStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + renderHeader([...path,parentIndex]) + terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nProperty: "+ansis.bold.blue((typeof parentIndex == "number") ? "#"+(parentIndex+1) : (structure.options.cliDisplayName ?? parentIndex))+"\n") + + const answer = await terminal.singleColumnMenu(["false (Disabled)","true (Enabled)"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run config checker + const newValue = (answer.selectedIndex == 0) ? false : true + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await nextFn(newValue) + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.gray("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + } +} + +async function renderAdditionConfigNumberStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],prefillValue?:string){ + renderHeader([...path,parentIndex]) + terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + + const answer = await terminal.inputField({ + default:prefillValue, + style:terminal.cyan, + cancelable:true + }).promise + + if (typeof answer != "string") return await backFn() + + //run config checker + const newValue = Number(answer.replaceAll(",",".")) + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await nextFn(newValue) + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,answer) + } +} + +async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],prefillValue?:string){ + renderHeader([...path,parentIndex]) + terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + + const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices + const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { + style:terminal.white, + selectedStyle:terminal.bgBlue.white + } + + const answer = await terminal.inputField({ + default:prefillValue, + style:terminal.cyan, + hintStyle:terminal.gray, + cancelable:true, + autoComplete:autocompleteList, + autoCompleteHint:(!!autocompleteList), + autoCompleteMenu:(autocompleteList) ? autoCompleteMenuOpts as Terminal.Autocompletion : false + }).promise + + if (typeof answer != "string") return await backFn() + + //run config checker + const newValue = answer.replaceAll("\\n","\n") + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await nextFn(newValue) + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,answer) + } +} + +async function renderAdditionConfigNullStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNullStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + renderHeader([...path,parentIndex]) + terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + + const answer = await terminal.singleColumnMenu(["null"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run config checker + const newValue = null + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await nextFn(newValue) + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + } +} + export async function execute(){ if (terminal.width < 100 || terminal.height < 35){ terminal(ansis.red.bold("\n\nMake sure your console or cmd window has a "+ansis.cyan("minimum width & height")+" of "+ansis.cyan("100x35")+" characters.")) diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 44d94ba..059ea51 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -601,14 +601,4 @@ export const defaultDropdownOptionsFunction = (manager:api.ODCheckerManager, fun }) return {valid:(final.length < 1),messages:final} -} - -/* TODO - * - add & duplicate functionality for arrays (complex) - * - move & remove functionality for arrays (simple) - * - add/duplicate generators + forms for each structure type - * - implement default value & skip values in all structures - * - when sub-arrays are required to be configured in an add/duplicate generator, it will have an extra "next" option in the menu to go back to the next property to configure. - * - when sub-objects are required to be configured in an add/duplicate generator, it will have an extra "next" option in the menu to go back to the next property to configure. - * - and more - */ \ No newline at end of file +} \ No newline at end of file From 92533777665c57ce7f2aaa975e996145fb5e75f8 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sat, 12 Apr 2025 21:05:52 +0200 Subject: [PATCH 07/78] CLI improvements --- src/core/startup/cli.ts | 81 +++++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 4d24424..1aec309 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -350,18 +350,28 @@ async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(( selectedStyle:terminal.bgBlue.white } - const answer = await terminal.inputField({ + const input = terminal.inputField({ default:prefillValue, style:terminal.cyan, hintStyle:terminal.gray, - cancelable:true, + cancelable:false, autoComplete:autocompleteList, autoCompleteHint:(!!autocompleteList), autoCompleteMenu:(autocompleteList) ? autoCompleteMenuOpts as Terminal.Autocompletion : false - }).promise - - if (typeof answer != "string") return await backFn() - + }) + + terminal.on("key",async (name:string,matches:string[],data:object) => { + if (name == "ESCAPE"){ + terminal.removeListener("key","cli-render-string-structure-edit") + input.abort() + await backFn() + } + },({id:"cli-render-string-structure-edit"} as any)) + + const answer = await input.promise + terminal.removeListener("key","cli-render-string-structure-edit") + if (typeof answer != "string") return + //run config checker const newValue = answer.replaceAll("\\n","\n") const newPath = [...path] @@ -541,8 +551,7 @@ async function chooseAdditionConfigStructure(checker:api.ODChecker,backFn:(() => else await backFn() } -async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ - const localData = {} +async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localData:object={}){ const children = structure.options.children ?? [] const skipKeys = (structure.options.cliInitSkipKeys ?? []) //add skipped properties @@ -592,16 +601,36 @@ async function configureAdditionObjectProperties(checker:api.ODChecker,children: async function renderAdditionConfigEnabledObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ const enabledProperty = structure.options.property + const enabledValue = structure.options.enabledValue const subStructure = structure.options.checker if (!enabledProperty || !subStructure || !subStructure.options.children) return await backFn() + + let propertyStructure: api.ODCheckerBooleanStructure|api.ODCheckerNumberStructure|api.ODCheckerStringStructure + if (typeof enabledValue == "string") propertyStructure = new api.ODCheckerStringStructure("opendiscord:CLI-checker-enabled-object-structure",{}) + else if (typeof enabledValue == "number") propertyStructure = new api.ODCheckerNumberStructure("opendiscord:CLI-checker-enabled-object-structure",{}) + else if (typeof enabledValue == "boolean") propertyStructure = new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-enabled-object-structure",{}) + else throw new Error("OT CLI => enabled object structure has an invalid type of enabledProperty. It must be a primitive boolean/number/string.") - if (!subStructure.options.children.find((child) => child.key === structure.options.property)){ - if (typeof structure.options.enabledValue == "string") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerStringStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) - else if (typeof structure.options.enabledValue == "number") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerNumberStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) - else if (typeof structure.options.enabledValue == "boolean") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) - } + const localData = {} + await chooseAdditionConfigStructure(checker,backFn,async (data) => { + if (data === enabledValue) await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path)},nextFn,subStructure,parent,parentIndex,path,localData) + else{ + localData[enabledProperty] = data + //copy old object checker to new object checker => all options get de-referenced (this is needed for the new object skip keys are temporary) + const newStructure = new api.ODCheckerObjectStructure(subStructure.id,{children:[]}) + + //copy all options over to the new checker + newStructure.options.children = [...subStructure.options.children] + newStructure.options.cliInitSkipKeys = subStructure.options.children.map((child) => child.key) + for (const key of Object.keys(subStructure.options)){ + if (key != "children" && key != "cliInitSkipKeys") newStructure.options[key] = subStructure.options[key] + } - await chooseAdditionConfigStructure(checker,backFn,nextFn,subStructure,parent,parentIndex,path) + //adds all properties to object as "skipKeys", then continues to next function + await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path)},nextFn,newStructure,parent,parentIndex,path,localData) + await nextFn(localData) + } + },propertyStructure,localData,enabledProperty,[...path,parentIndex]) } async function renderAdditionConfigObjectSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ @@ -654,6 +683,7 @@ async function renderAdditionConfigBooleanStructure(checker:api.ODChecker,backFn terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) terminal.gray("\nProperty: "+ansis.bold.blue((typeof parentIndex == "number") ? "#"+(parentIndex+1) : (structure.options.cliDisplayName ?? parentIndex))+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.singleColumnMenu(["false (Disabled)","true (Enabled)"],{ leftPadding:"> ", @@ -691,6 +721,7 @@ async function renderAdditionConfigNumberStructure(checker:api.ODChecker,backFn: terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.inputField({ default:prefillValue, @@ -725,6 +756,7 @@ async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn: terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { @@ -732,17 +764,27 @@ async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn: selectedStyle:terminal.bgBlue.white } - const answer = await terminal.inputField({ + const input = terminal.inputField({ default:prefillValue, style:terminal.cyan, hintStyle:terminal.gray, - cancelable:true, + cancelable:false, autoComplete:autocompleteList, autoCompleteHint:(!!autocompleteList), autoCompleteMenu:(autocompleteList) ? autoCompleteMenuOpts as Terminal.Autocompletion : false - }).promise - - if (typeof answer != "string") return await backFn() + }) + + terminal.on("key",async (name:string,matches:string[],data:object) => { + if (name == "ESCAPE"){ + terminal.removeListener("key","cli-render-string-structure-add") + input.abort() + await backFn() + } + },({id:"cli-render-string-structure-add"} as any)) + + const answer = await input.promise + terminal.removeListener("key","cli-render-string-structure-add") + if (typeof answer != "string") return //run config checker const newValue = answer.replaceAll("\\n","\n") @@ -769,6 +811,7 @@ async function renderAdditionConfigNullStructure(checker:api.ODChecker,backFn:(( terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.singleColumnMenu(["null"],{ leftPadding:"> ", From 1f3894141a79ddd826781a068f390eb9cc2ba0b2 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 13 Apr 2025 10:33:01 +0200 Subject: [PATCH 08/78] Finished the 'edit config' module from the CLI --- src/core/startup/cli.ts | 173 +++++++++++++++++++++++++++++++--------- 1 file changed, 135 insertions(+), 38 deletions(-) diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 1aec309..1928d29 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -167,14 +167,11 @@ async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:( if (answer.selectedIndex == 0) await chooseAdditionConfigStructure(checker,backFnFunc,async (newData) => { data[data.length] = newData await backFnFunc() - },structure.options.propertyChecker,data,data.length,path) + },structure.options.propertyChecker,data,data.length,path,[]) else if (answer.selectedIndex == 1) await renderConfigArrayStructureEditSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) else if (answer.selectedIndex == 2) await renderconfigArrayStructureMoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) else if (answer.selectedIndex == 3) await renderconfigArrayStructureRemoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) - else if (answer.selectedIndex == 4){ - //TODO => duplicate trigger chooseConfigStructure() function but for "addition instead of editing" - } - + else if (answer.selectedIndex == 4) await renderConfigArrayStructureDuplicateSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) } async function renderConfigArrayStructureEditSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ @@ -262,6 +259,29 @@ async function renderconfigArrayStructureRemoveSelector(checker:api.ODChecker,ba await backFn() } +async function renderConfigArrayStructureDuplicateSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + const propertyName = arrayStructure.options.cliDisplayPropertyName ?? "index" + renderHeader(path) + terminal(ansis.bold.green("Please select the "+propertyName+" you would like to duplicate.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const longestDataListName = Math.max(...data.map((d,i) => getArrayPreviewStructureNameLength(structure,d,data,i))) + const dataList = data.map((d,i) => (i+1)+". "+getArrayPreviewFromStructure(structure,d,data,i,longestDataListName)) + const dataAnswer = await terminal.singleColumnMenu(dataList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (dataAnswer.canceled) return await backFn() + data.push(JSON.parse(JSON.stringify(data[dataAnswer.selectedIndex]))) + terminal.bold.blue("\n\n✅ Property duplicated succesfully!") + await utilities.timer(400) + await backFn() +} + async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,data:boolean,parent:object,parentIndex:string|number,path:(string|number)[]){ if (typeof data != "boolean") throw new api.ODSystemError("OT CLI => Property is not of the type 'boolean'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") renderHeader(path) @@ -539,19 +559,20 @@ function getArrayPreviewFromStructure(structure:api.ODCheckerStructure,data:api. }else return "" } -async function chooseAdditionConfigStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ - if (structure instanceof api.ODCheckerObjectStructure) await renderAdditionConfigObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) - else if (structure instanceof api.ODCheckerBooleanStructure) await renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) - else if (structure instanceof api.ODCheckerNumberStructure) await renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) - else if (structure instanceof api.ODCheckerStringStructure) await renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) - else if (structure instanceof api.ODCheckerNullStructure) await renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) - else if (structure instanceof api.ODCheckerEnabledObjectStructure) await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) - else if (structure instanceof api.ODCheckerObjectSwitchStructure) await renderAdditionConfigObjectSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) - //TODO: array, type switch, ... +async function chooseAdditionConfigStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){ + if (structure instanceof api.ODCheckerObjectStructure) await renderAdditionConfigObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerBooleanStructure) await renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerNumberStructure) await renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerStringStructure) await renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerNullStructure) await renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerEnabledObjectStructure) await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerObjectSwitchStructure) await renderAdditionConfigObjectSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerArrayStructure) await renderAdditionConfigArrayStructureSelector(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) + else if (structure instanceof api.ODCheckerTypeSwitchStructure) await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) else await backFn() } -async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localData:object={}){ +async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],localData:object={}){ const children = structure.options.children ?? [] const skipKeys = (structure.options.cliInitSkipKeys ?? []) //add skipped properties @@ -574,7 +595,7 @@ async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn: //add properties that need to be configured const configChildren = children.filter((c) => !skipKeys.includes(c.key)).map((c) => {return {key:c.key,checker:c.checker}}) - await configureAdditionObjectProperties(checker,configChildren,0,localData,[...path,parentIndex],async () => { + await configureAdditionObjectProperties(checker,configChildren,0,localData,[...path,parentIndex],(typeof parentIndex == "number") ? [...localPath] : [...localPath,parentIndex],async () => { //go back to previous screen await backFn() },async () => { @@ -585,21 +606,21 @@ async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn: }) } -async function configureAdditionObjectProperties(checker:api.ODChecker,children:{key:string,checker:api.ODCheckerStructure}[],currentIndex:number,localData:object,path:(string|number)[],backFn:(() => api.ODPromiseVoid),nextFn:(() => api.ODPromiseVoid)){ +async function configureAdditionObjectProperties(checker:api.ODChecker,children:{key:string,checker:api.ODCheckerStructure}[],currentIndex:number,localData:object,path:(string|number)[],localPath:(string|number)[],backFn:(() => api.ODPromiseVoid),nextFn:(() => api.ODPromiseVoid)){ if (children.length < 1) return await nextFn() const child = children[currentIndex] await chooseAdditionConfigStructure(checker,async () => { - if (children[currentIndex-1]) await configureAdditionObjectProperties(checker,children,currentIndex-1,localData,path,backFn,nextFn) + if (children[currentIndex-1]) await configureAdditionObjectProperties(checker,children,currentIndex-1,localData,path,localPath,backFn,nextFn) else await backFn() },async (data) => { localData[child.key] = data - if (children[currentIndex+1]) await configureAdditionObjectProperties(checker,children,currentIndex+1,localData,path,backFn,nextFn) + if (children[currentIndex+1]) await configureAdditionObjectProperties(checker,children,currentIndex+1,localData,path,localPath,backFn,nextFn) else await nextFn() - },child.checker,localData,child.key,path) + },child.checker,localData,child.key,path,localPath) } -async function renderAdditionConfigEnabledObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ +async function renderAdditionConfigEnabledObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){ const enabledProperty = structure.options.property const enabledValue = structure.options.enabledValue const subStructure = structure.options.checker @@ -613,7 +634,7 @@ async function renderAdditionConfigEnabledObjectStructure(checker:api.ODChecker, const localData = {} await chooseAdditionConfigStructure(checker,backFn,async (data) => { - if (data === enabledValue) await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path)},nextFn,subStructure,parent,parentIndex,path,localData) + if (data === enabledValue) await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,subStructure,parent,parentIndex,path,localPath,localData) else{ localData[enabledProperty] = data //copy old object checker to new object checker => all options get de-referenced (this is needed for the new object skip keys are temporary) @@ -627,13 +648,13 @@ async function renderAdditionConfigEnabledObjectStructure(checker:api.ODChecker, } //adds all properties to object as "skipKeys", then continues to next function - await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path)},nextFn,newStructure,parent,parentIndex,path,localData) + await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,newStructure,parent,parentIndex,path,localPath,localData) await nextFn(localData) } - },propertyStructure,localData,enabledProperty,[...path,parentIndex]) + },propertyStructure,localData,enabledProperty,[...path,parentIndex],[...localPath,parentIndex]) } -async function renderAdditionConfigObjectSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ +async function renderAdditionConfigObjectSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){ renderHeader([...path,parentIndex]) terminal(ansis.bold.green("What type of object would you like to add?\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) @@ -675,14 +696,14 @@ async function renderAdditionConfigObjectSwitchStructure(checker:api.ODChecker,b } } - await chooseAdditionConfigStructure(checker,backFn,nextFn,newStructure,parent,parentIndex,path) + await chooseAdditionConfigStructure(checker,backFn,nextFn,newStructure,parent,parentIndex,path,localPath) } -async function renderAdditionConfigBooleanStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ +async function renderAdditionConfigBooleanStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){ renderHeader([...path,parentIndex]) terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) - terminal.gray("\nProperty: "+ansis.bold.blue((typeof parentIndex == "number") ? "#"+(parentIndex+1) : (structure.options.cliDisplayName ?? parentIndex))+"\n") + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n") terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.singleColumnMenu(["false (Disabled)","true (Enabled)"],{ @@ -712,15 +733,15 @@ async function renderAdditionConfigBooleanStructure(checker:api.ODChecker,backFn terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") terminal.gray("\n"+messages) await utilities.timer(1000+(2000*checker.messages.length)) - await renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + await renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) } } -async function renderAdditionConfigNumberStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],prefillValue?:string){ +async function renderAdditionConfigNumberStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],prefillValue?:string){ renderHeader([...path,parentIndex]) terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) - terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n") terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.inputField({ @@ -747,15 +768,15 @@ async function renderAdditionConfigNumberStructure(checker:api.ODChecker,backFn: terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") terminal.red("\n"+messages) await utilities.timer(1000+(2000*checker.messages.length)) - await renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,answer) + await renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath,answer) } } -async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],prefillValue?:string){ +async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],prefillValue?:string){ renderHeader([...path,parentIndex]) terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) - terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n") terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices @@ -802,15 +823,15 @@ async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn: terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") terminal.red("\n"+messages) await utilities.timer(1000+(2000*checker.messages.length)) - await renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,answer) + await renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath,answer) } } -async function renderAdditionConfigNullStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNullStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ +async function renderAdditionConfigNullStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNullStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){ renderHeader([...path,parentIndex]) terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) - terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? (typeof parentIndex == "number" ? "#"+(parentIndex+1) : parentIndex))+"\n") + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n") terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.singleColumnMenu(["null"],{ @@ -840,10 +861,86 @@ async function renderAdditionConfigNullStructure(checker:api.ODChecker,backFn:(( terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") terminal.red("\n"+messages) await utilities.timer(1000+(2000*checker.messages.length)) - await renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path) + await renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath) } } +async function renderAdditionConfigArrayStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerArrayStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],localData:any[]=[]){ + renderHeader([...path,parentIndex]) + terminal(ansis.bold.green("Please select what you would like to do with the new array.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + if (!structure.options.propertyChecker) return await backFn() + + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") + + const propertyName = structure.options.cliDisplayPropertyName ?? "index" + const answer = await terminal.singleColumnMenu(localData.length < 1 ? [ansis.magenta("-> Continue to next variable"),"Add "+propertyName] : [ + ansis.magenta("-> Continue to next variable"), + "Add "+propertyName, + "Edit "+propertyName, + "Move "+propertyName, + "Remove "+propertyName, + "Duplicate "+propertyName, + + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + const backFnFunc = async () => {await renderAdditionConfigArrayStructureSelector(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath,localData)} + + if (answer.canceled) return await backFn() + if (answer.selectedIndex == 0) await nextFn(localData) + else if (answer.selectedIndex == 1) await chooseAdditionConfigStructure(checker,backFnFunc,async (newData) => { + localData[localData.length] = newData + await backFnFunc() + },structure.options.propertyChecker,localData,localData.length,path,[]) + else if (answer.selectedIndex == 2) await renderConfigArrayStructureEditSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path) + else if (answer.selectedIndex == 3) await renderconfigArrayStructureMoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path) + else if (answer.selectedIndex == 4) await renderconfigArrayStructureRemoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path) + else if (answer.selectedIndex == 5) await renderConfigArrayStructureDuplicateSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path) +} + + +async function renderAdditionConfigTypeSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerTypeSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){ + renderHeader(path) + terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") + + const actionsList: string[] = [] + if (structure.options.boolean) actionsList.push("Create as boolean") + if (structure.options.string) actionsList.push("Create as string") + if (structure.options.number) actionsList.push("Create as number") + if (structure.options.object) actionsList.push("Create as object") + if (structure.options.array) actionsList.push("Create as array/list") + if (structure.options.null) actionsList.push("Create as null") + + const answer = await terminal.singleColumnMenu(actionsList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run selected structure editor (untested) + if (answer.selectedText.startsWith("Create as boolean") && structure.options.boolean) await renderAdditionConfigBooleanStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.boolean,parent,parentIndex,path,localPath) + else if (answer.selectedText.startsWith("Create as string") && structure.options.string) await renderAdditionConfigStringStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.string,parent,parentIndex,path,localPath) + else if (answer.selectedText.startsWith("Create as number") && structure.options.number) await renderAdditionConfigNumberStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.number,parent,parentIndex,path,localPath) + else if (answer.selectedText.startsWith("Create as object") && structure.options.object) await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.object,parent,parentIndex,path,localPath) + else if (answer.selectedText.startsWith("Create as array/list") && structure.options.array) await renderAdditionConfigArrayStructureSelector(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.array,parent,parentIndex,path,localPath) + else if (answer.selectedText.startsWith("Create as null") && structure.options.null) await renderAdditionConfigNullStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.null,parent,parentIndex,path,localPath) +} + export async function execute(){ if (terminal.width < 100 || terminal.height < 35){ terminal(ansis.red.bold("\n\nMake sure your console or cmd window has a "+ansis.cyan("minimum width & height")+" of "+ansis.cyan("100x35")+" characters.")) From 6ec228df537f21321aa006e28767aed84ab91fe1 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:03:45 +0200 Subject: [PATCH 09/78] Added 'save()' feature to configs + improvements --- src/core/api/modules/config.ts | 73 ++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src/core/api/modules/config.ts b/src/core/api/modules/config.ts index 4d34b2d..c59b11a 100644 --- a/src/core/api/modules/config.ts +++ b/src/core/api/modules/config.ts @@ -5,6 +5,7 @@ import { ODId, ODManager, ODManagerData, ODPromiseVoid, ODSystemError, ODValidId import nodepath from "path" import { ODDebugger } from "./console" import fs from "fs" +import * as fjs from "formatted-json-stringify" /**## ODConfigManager `class` * This is an Open Ticket config manager. @@ -14,8 +15,17 @@ import fs from "fs" * You can use this class to get/change/add a config file (`ODConfig`) in your plugin! */ export class ODConfigManager extends ODManager { + /**Alias to Open Ticket debugger. */ + #debug: ODDebugger + constructor(debug:ODDebugger){ super(debug,"config") + this.#debug = debug + } + add(data:ODConfig|ODConfig[],overwrite?:boolean): boolean { + if (Array.isArray(data)) data.forEach((d) => d.useDebug(this.#debug)) + else data.useDebug(this.#debug) + return super.add(data,overwrite) } /**Init all config files. */ async init(){ @@ -42,15 +52,45 @@ export class ODConfig extends ODManagerData { path: string = "" /**An object/array of the entire config file! Variables inside it can be edited while the bot is running! */ data: any + /**Is this config already initiated? */ + initiated: boolean = false + /**An array of listeners to run when the config gets reloaded. These are not executed on the initial loading. */ + protected reloadListeners: Function[] = [] + /**Alias to Open Ticket debugger. */ + protected debug: ODDebugger|null = null constructor(id:ODValidId, data:any){ super(id) this.data = data } + /**Use the Open Ticket debugger for logs. */ + useDebug(debug:ODDebugger|null){ + this.debug = debug + } /**Init the config. */ init(): ODPromiseVoid { - //nothing + this.initiated = true + if (this.debug) this.debug.debug("Initiated config '"+this.file+"' in ODConfigManager.",[{key:"id",value:this.id.value}]) + //please implement this feature in your own config extension & extend this function. + } + /**Reload the config. Be aware that this doesn't update the config data everywhere in the bot! */ + reload(): ODPromiseVoid { + if (this.debug) this.debug.debug("Reloaded config '"+this.file+"' in ODConfigManager.",[{key:"id",value:this.id.value}]) + //please implement this feature in your own config extension & extend this function. + } + /**Save the edited config to the filesystem. This is used by the Interactive Setup CLI. It's not recommended to use this while the bot is running. */ + save(): ODPromiseVoid { + if (this.debug) this.debug.debug("Saved config '"+this.file+"' in ODConfigManager.",[{key:"id",value:this.id.value}]) + //please implement this feature in your own config extension & extend this function. + } + /**Listen for a reload of this JSON file! */ + onReload(cb:Function){ + this.reloadListeners.push(cb) + } + /**Remove all reload listeners. Not recommended! */ + removeAllReloadListeners(){ + this.reloadListeners = [] } } @@ -65,13 +105,13 @@ export class ODConfig extends ODManagerData { * const config = new api.ODJsonConfig("plugin-config","test.json","./plugins/testplugin/") */ export class ODJsonConfig extends ODConfig { - /**An array of listeners to run when the config gets reloaded. These are not executed on the initial loading. */ - #reloadListeners: Function[] = [] + formatter: fjs.custom.BaseFormatter - constructor(id:ODValidId, file:string, customPath?:string){ + constructor(id:ODValidId, file:string, customPath?:string, formatter?:fjs.custom.BaseFormatter){ super(id,{}) this.file = (file.endsWith(".json")) ? file : file+".json" this.path = customPath ? nodepath.join("./",customPath,this.file) : nodepath.join("./config/",this.file) + this.formatter = formatter ?? new fjs.DefaultFormatter(null,true," ") } /**Init the config. */ @@ -79,17 +119,20 @@ export class ODJsonConfig extends ODConfig { if (!fs.existsSync(this.path)) throw new ODSystemError("Unable to parse config \""+nodepath.join("./",this.path)+"\", the file doesn't exist!") try{ this.data = JSON.parse(fs.readFileSync(this.path).toString()) + super.init() }catch(err){ process.emit("uncaughtException",err) throw new ODSystemError("Unable to parse config \""+nodepath.join("./",this.path)+"\"!") } } - /**Reload the JSON file. Be aware that this doesn't update classes that used individual parts of the config data! */ + /**Reload the config. Be aware that this doesn't update the config data everywhere in the bot! */ reload(){ + if (!this.initiated) throw new ODSystemError("Unable to reload config \""+nodepath.join("./",this.path)+"\", the file hasn't been initiated yet!") if (!fs.existsSync(this.path)) throw new ODSystemError("Unable to reload config \""+nodepath.join("./",this.path)+"\", the file doesn't exist!") try{ this.data = JSON.parse(fs.readFileSync(this.path).toString()) - this.#reloadListeners.forEach((cb) => { + super.reload() + this.reloadListeners.forEach((cb) => { try{ cb() }catch(err){ @@ -101,12 +144,16 @@ export class ODJsonConfig extends ODConfig { throw new ODSystemError("Unable to reload config \""+nodepath.join("./",this.path)+"\"!") } } - /**Listen for a reload of this JSON file! */ - onReload(cb:Function){ - this.#reloadListeners.push(cb) - } - /**Remove all reload listeners. Not recommended! */ - removeAllReloadListeners(){ - this.#reloadListeners = [] + /**Save the edited config to the filesystem. This is used by the Interactive Setup CLI. It's not recommended to use this while the bot is running. */ + save(): ODPromiseVoid { + if (!this.initiated) throw new ODSystemError("Unable to save config \""+nodepath.join("./",this.path)+"\", the file hasn't been initiated yet!") + try{ + const contents = this.formatter.stringify(this.data) + fs.writeFileSync(this.path,contents) + super.save() + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("Unable to save config \""+nodepath.join("./",this.path)+"\"!") + } } } \ No newline at end of file From 5dcbcbfabc83981a1a8fbbc466c4f6572edc2b4d Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:03:55 +0200 Subject: [PATCH 10/78] More CLI improvements + layout changes --- src/core/startup/cli.ts | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 1928d29..4473195 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -145,6 +145,9 @@ async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:( terminal(ansis.bold.green("Please select what you would like to do.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) if (!structure.options.propertyChecker) return await backFn() + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? parentIndex.toString())+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") + const propertyName = structure.options.cliDisplayPropertyName ?? "index" const answer = await terminal.singleColumnMenu(data.length < 1 ? ["Add "+propertyName] : [ "Add "+propertyName, @@ -166,6 +169,7 @@ async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:( if (answer.canceled) return await backFn() if (answer.selectedIndex == 0) await chooseAdditionConfigStructure(checker,backFnFunc,async (newData) => { data[data.length] = newData + await checker.config.save() await backFnFunc() },structure.options.propertyChecker,data,data.length,path,[]) else if (answer.selectedIndex == 1) await renderConfigArrayStructureEditSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path) @@ -231,6 +235,8 @@ async function renderconfigArrayStructureMoveSelector(checker:api.ODChecker,back const slicedData = [...data.slice(0,dataAnswer.selectedIndex),...data.slice(dataAnswer.selectedIndex+1)] const insertedData = [...slicedData.slice(0,moveAnswer.selectedIndex),subData,...slicedData.slice(moveAnswer.selectedIndex)] insertedData.forEach((d,i) => data[i] = d) + + await checker.config.save() terminal.bold.blue("\n\n✅ Property moved succesfully!") await utilities.timer(400) await backFn() @@ -254,6 +260,8 @@ async function renderconfigArrayStructureRemoveSelector(checker:api.ODChecker,ba if (dataAnswer.canceled) return await backFn() data.splice(dataAnswer.selectedIndex,1) + + await checker.config.save() terminal.bold.blue("\n\n✅ Property deleted succesfully!") await utilities.timer(400) await backFn() @@ -277,6 +285,8 @@ async function renderConfigArrayStructureDuplicateSelector(checker:api.ODChecker if (dataAnswer.canceled) return await backFn() data.push(JSON.parse(JSON.stringify(data[dataAnswer.selectedIndex]))) + + await checker.config.save() terminal.bold.blue("\n\n✅ Property duplicated succesfully!") await utilities.timer(400) await backFn() @@ -288,6 +298,7 @@ async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:( terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) terminal.gray("\nCurrent value: "+ansis.bold[data ? "green" : "red"](data.toString())+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.singleColumnMenu(["false (Disabled)","true (Enabled)"],{ leftPadding:"> ", @@ -309,6 +320,8 @@ async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:( if (isDataValid){ parent[parentIndex] = newValue + + await checker.config.save() terminal.bold.blue("\n\n✅ Variable saved succesfully!") await utilities.timer(400) await backFn() @@ -327,6 +340,7 @@ async function renderConfigNumberStructureEditor(checker:api.ODChecker,backFn:(( terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.inputField({ default:prefillValue, @@ -345,6 +359,8 @@ async function renderConfigNumberStructureEditor(checker:api.ODChecker,backFn:(( if (isDataValid){ parent[parentIndex] = newValue + + await checker.config.save() terminal.bold.blue("\n\n✅ Variable saved succesfully!") await utilities.timer(400) await backFn() @@ -363,8 +379,10 @@ async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(( terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) terminal.gray("\nCurrent value:"+(data.includes("\n") ? "\n" : " \"")+ansis.bold.blue(data)+ansis.gray(!data.includes("\n") ? "\"\n" : "\n")) + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") - const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices + const customExtraOptions = (structure instanceof api.ODCheckerCustomStructure_DiscordId) ? structure.extraOptions : undefined + const autocompleteList = (structure.options.cliAutocompleteList ?? customExtraOptions) ?? structure.options.choices const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, selectedStyle:terminal.bgBlue.white @@ -401,6 +419,8 @@ async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(( if (isDataValid){ parent[parentIndex] = newValue + + await checker.config.save() terminal.bold.blue("\n\n✅ Variable saved succesfully!") await utilities.timer(400) await backFn() @@ -419,6 +439,7 @@ async function renderConfigNullStructureEditor(checker:api.ODChecker,backFn:(() terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) terminal.gray("\nCurrent value: "+ansis.bold.blue("null")+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const answer = await terminal.singleColumnMenu(["null"],{ leftPadding:"> ", @@ -440,6 +461,8 @@ async function renderConfigNullStructureEditor(checker:api.ODChecker,backFn:(() if (isDataValid){ parent[parentIndex] = newValue + + await checker.config.save() terminal.bold.blue("\n\n✅ Variable saved succesfully!") await utilities.timer(400) await backFn() @@ -457,6 +480,7 @@ async function renderConfigTypeSwitchStructureEditor(checker:api.ODChecker,backF terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const actionsList: string[] = [] if (structure.options.boolean) actionsList.push("Edit as boolean") @@ -779,7 +803,8 @@ async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn: terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n") terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") - const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices + const customExtraOptions = (structure instanceof api.ODCheckerCustomStructure_DiscordId) ? structure.extraOptions : undefined + const autocompleteList = (structure.options.cliAutocompleteList ?? customExtraOptions) ?? structure.options.choices const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, selectedStyle:terminal.bgBlue.white From 49504bc3dea1b28eb4cc064dfba35e551dc9ed6c Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:22:09 +0200 Subject: [PATCH 11/78] Added formatters to all built-in config files. Added formatters to all built-in JSON config files for the Open Ticket interactive CLI. The new save() feature from the ODJsonConfig class will be using this formatter to save the config to the filesystem. --- package.json | 2 +- src/core/startup/cli.ts | 8 +- src/data/framework/configLoader.ts | 374 ++++++++++++++++++++++++++++- 3 files changed, 374 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e083b9c..812f2d8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@types/terminal-kit": "^2.5.7", "ansis": "^2.3.0", "discord.js": "^14.17.2", - "formatted-json-stringify": "^1.1.0", + "formatted-json-stringify": "^1.2.0", "terminal-kit": "^3.1.2", "typescript": "^5.5.4" }, diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 4473195..6764dc4 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -88,7 +88,7 @@ async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn: if (!structure.options.children) return await backFn() const list = structure.options.children.filter((child) => !child.cliHideInEditMode) - const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName+" ("+child.key+")" : child.key)) + const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName : child.key)) const nameLength = utilities.getLongestLength(nameList) const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray(list[index].checker.options.cliDisplayDescription ? "=> "+list[index].checker.options.cliDisplayDescription : "")) @@ -145,8 +145,10 @@ async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:( terminal(ansis.bold.green("Please select what you would like to do.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) if (!structure.options.propertyChecker) return await backFn() - terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? parentIndex.toString())+"\n") - terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") + if (typeof parentIndex == "string" || !isNaN(parentIndex)){ + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? parentIndex.toString())+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") + } const propertyName = structure.options.cliDisplayPropertyName ?? "index" const answer = await terminal.singleColumnMenu(data.length < 1 ? ["Add "+propertyName] : [ diff --git a/src/data/framework/configLoader.ts b/src/data/framework/configLoader.ts index 597f9c5..db85d51 100644 --- a/src/data/framework/configLoader.ts +++ b/src/data/framework/configLoader.ts @@ -1,12 +1,374 @@ import {opendiscord, api, utilities} from "../../index" +import * as fjs from "formatted-json-stringify" export const loadAllConfigs = async () => { const devconfigFlag = opendiscord.flags.get("opendiscord:dev-config") const isDevconfig = devconfigFlag ? devconfigFlag.value : false + + /** How to add more config variables? + * - Add the variable to the config files in `./config/` & `./devconfig/`. + * - Add the variable to the config in ./src/core/api/defaults/config.ts (interfaces + types) + * - Add the variable to the config checker in ./src/data/framework/checkerLoader.ts + * - Make sure it's compatible with the Interactive Setup CLI. + * - Make sure the Migration Manager automatically adds the variable when missing. + * - Add the variable to the formatters in this file. + * - Update the documentation reference. + */ - opendiscord.configs.add(new api.ODJsonConfig("opendiscord:general","general.json",(isDevconfig) ? "./devconfig/" : "./config/")) - opendiscord.configs.add(new api.ODJsonConfig("opendiscord:options","options.json",(isDevconfig) ? "./devconfig/" : "./config/")) - opendiscord.configs.add(new api.ODJsonConfig("opendiscord:panels","panels.json",(isDevconfig) ? "./devconfig/" : "./config/")) - opendiscord.configs.add(new api.ODJsonConfig("opendiscord:questions","questions.json",(isDevconfig) ? "./devconfig/" : "./config/")) - opendiscord.configs.add(new api.ODJsonConfig("opendiscord:transcripts","transcripts.json",(isDevconfig) ? "./devconfig/" : "./config/")) -} \ No newline at end of file + opendiscord.configs.add(new api.ODJsonConfig("opendiscord:general","general.json",(isDevconfig) ? "./devconfig/" : "./config/",defaultGeneralFormatter)) + opendiscord.configs.add(new api.ODJsonConfig("opendiscord:questions","questions.json",(isDevconfig) ? "./devconfig/" : "./config/",defaultQuestionsFormatter)) + opendiscord.configs.add(new api.ODJsonConfig("opendiscord:options","options.json",(isDevconfig) ? "./devconfig/" : "./config/",defaultOptionsFormatter)) + opendiscord.configs.add(new api.ODJsonConfig("opendiscord:panels","panels.json",(isDevconfig) ? "./devconfig/" : "./config/",defaultPanelsFormatter)) + opendiscord.configs.add(new api.ODJsonConfig("opendiscord:transcripts","transcripts.json",(isDevconfig) ? "./devconfig/" : "./config/",defaultTranscriptsFormatter)) +} + +//FORMATTERS +export const defaultGeneralFormatter = new fjs.ObjectFormatter(null,true,[ + new fjs.ObjectFormatter("_INFO",true,[ + new fjs.PropertyFormatter("support"), + new fjs.PropertyFormatter("discord"), + new fjs.PropertyFormatter("version"), + ]), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("token"), + new fjs.PropertyFormatter("tokenFromENV"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("mainColor"), + new fjs.PropertyFormatter("language"), + new fjs.PropertyFormatter("prefix"), + new fjs.PropertyFormatter("serverId"), + new fjs.ArrayFormatter("globalAdmins",false,new fjs.PropertyFormatter(null)), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("slashCommands"), + new fjs.PropertyFormatter("textCommands"), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("status",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("type"), + new fjs.PropertyFormatter("text"), + new fjs.PropertyFormatter("status"), + ]), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("system",true,[ + new fjs.PropertyFormatter("removeParticipantsOnClose"), + new fjs.PropertyFormatter("replyOnTicketCreation"), + new fjs.PropertyFormatter("replyOnReactionRole"), + new fjs.PropertyFormatter("useTranslatedConfigChecker"), + new fjs.PropertyFormatter("preferSlashOverText"), + new fjs.PropertyFormatter("sendErrorOnUnknownCommand"), + new fjs.PropertyFormatter("questionFieldsInCodeBlock"), + new fjs.PropertyFormatter("disableVerifyBars"), + new fjs.PropertyFormatter("useRedErrorEmbeds"), + new fjs.PropertyFormatter("emojiStyle"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("enableTicketClaimButtons"), + new fjs.PropertyFormatter("enableTicketCloseButtons"), + new fjs.PropertyFormatter("enableTicketPinButtons"), + new fjs.PropertyFormatter("enableTicketDeleteButtons"), + new fjs.PropertyFormatter("enableTicketActionWithReason"), + new fjs.PropertyFormatter("enableDeleteWithoutTranscript"), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("logs",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("channel"), + ]), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("limits",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("globalMaximum"), + new fjs.PropertyFormatter("userMaximum"), + ]), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("permissions",true,[ + new fjs.PropertyFormatter("help"), + new fjs.PropertyFormatter("panel"), + new fjs.PropertyFormatter("ticket"), + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("delete"), + new fjs.PropertyFormatter("reopen"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.PropertyFormatter("move"), + new fjs.PropertyFormatter("rename"), + new fjs.PropertyFormatter("add"), + new fjs.PropertyFormatter("remove"), + new fjs.PropertyFormatter("blacklist"), + new fjs.PropertyFormatter("stats"), + new fjs.PropertyFormatter("clear"), + new fjs.PropertyFormatter("autoclose"), + new fjs.PropertyFormatter("autodelete"), + ]), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("messages",true,[ + new fjs.DefaultFormatter("creation",false), + new fjs.DefaultFormatter("closing",false), + new fjs.DefaultFormatter("deleting",false), + new fjs.DefaultFormatter("reopening",false), + new fjs.DefaultFormatter("claiming",false), + new fjs.DefaultFormatter("pinning",false), + new fjs.DefaultFormatter("adding",false), + new fjs.DefaultFormatter("removing",false), + new fjs.DefaultFormatter("renaming",false), + new fjs.DefaultFormatter("moving",false), + new fjs.DefaultFormatter("blacklisting",false), + new fjs.DefaultFormatter("roleAdding",false), + new fjs.DefaultFormatter("roleRemoving",false), + ]), + ]), +]) + +export const defaultQuestionsFormatter = new fjs.ArrayFormatter(null,true,new fjs.ObjectSwitchFormatter(null,[ + {key:"type",value:"short",formatter:new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("type"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("required"), + new fjs.PropertyFormatter("placeholder"), + new fjs.ObjectFormatter("length",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("min"), + new fjs.PropertyFormatter("max"), + ]), + ])}, + {key:"type",value:"paragraph",formatter:new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("type"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("required"), + new fjs.PropertyFormatter("placeholder"), + new fjs.ObjectFormatter("length",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("min"), + new fjs.PropertyFormatter("max"), + ]), + ])} +])) + + +export const defaultOptionsFormatter = new fjs.ArrayFormatter(null,true,new fjs.ObjectSwitchFormatter(null,[ + {key:"type",value:"ticket",formatter:new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("description"), + new fjs.PropertyFormatter("type"), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("button",true,[ + new fjs.PropertyFormatter("emoji"), + new fjs.PropertyFormatter("label"), + new fjs.PropertyFormatter("color"), + ]), + new fjs.TextFormatter(""), + new fjs.ArrayFormatter("ticketAdmins",false,new fjs.PropertyFormatter(null)), + new fjs.ArrayFormatter("readonlyAdmins",false,new fjs.PropertyFormatter(null)), + new fjs.PropertyFormatter("allowCreationByBlacklistedUsers"), + new fjs.ArrayFormatter("questions",false,new fjs.PropertyFormatter(null)), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("channel",true,[ + new fjs.PropertyFormatter("prefix"), + new fjs.PropertyFormatter("suffix"), + new fjs.PropertyFormatter("category"), + new fjs.PropertyFormatter("backupCategory"), + new fjs.PropertyFormatter("closedCategory"), + new fjs.ArrayFormatter("claimedCategory",true,new fjs.ObjectFormatter(null,false,[ + new fjs.PropertyFormatter("user"), + new fjs.PropertyFormatter("category"), + ])), + new fjs.PropertyFormatter("description"), + ]), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("dmMessage",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("text"), + new fjs.ObjectFormatter("embed",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("title"), + new fjs.PropertyFormatter("description"), + new fjs.PropertyFormatter("customColor"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("image"), + new fjs.PropertyFormatter("thumbnail"), + new fjs.ArrayFormatter("fields",true,new fjs.ObjectFormatter(null,false,[ + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("value"), + new fjs.PropertyFormatter("inline"), + ])), + new fjs.PropertyFormatter("timestamp"), + ]), + ]), + new fjs.ObjectFormatter("ticketMessage",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("text"), + new fjs.ObjectFormatter("embed",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("title"), + new fjs.PropertyFormatter("description"), + new fjs.PropertyFormatter("customColor"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("image"), + new fjs.PropertyFormatter("thumbnail"), + new fjs.ArrayFormatter("fields",true,new fjs.ObjectFormatter(null,false,[ + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("value"), + new fjs.PropertyFormatter("inline"), + ])), + new fjs.PropertyFormatter("timestamp"), + ]), + new fjs.ObjectFormatter("ping",true,[ + new fjs.PropertyFormatter("@here"), + new fjs.PropertyFormatter("@everyone"), + new fjs.ArrayFormatter("custom",true,new fjs.PropertyFormatter(null)), + ]), + ]), + new fjs.ObjectFormatter("autoclose",true,[ + new fjs.PropertyFormatter("enableInactiveHours"), + new fjs.PropertyFormatter("inactiveHours"), + new fjs.PropertyFormatter("enableUserLeave"), + new fjs.PropertyFormatter("disableOnClaim"), + ]), + new fjs.ObjectFormatter("autodelete",true,[ + new fjs.PropertyFormatter("enableInactiveDays"), + new fjs.PropertyFormatter("inactiveDays"), + new fjs.PropertyFormatter("enableUserLeave"), + new fjs.PropertyFormatter("disableOnClaim"), + ]), + new fjs.ObjectFormatter("cooldown",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("cooldownMinutes"), + ]), + new fjs.ObjectFormatter("limits",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("globalMaximum"), + new fjs.PropertyFormatter("userMaximum"), + ]), + ])}, + {key:"type",value:"website",formatter:new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("description"), + new fjs.PropertyFormatter("type"), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("button",true,[ + new fjs.PropertyFormatter("emoji"), + new fjs.PropertyFormatter("label"), + ]), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("url"), + ])}, + {key:"type",value:"role",formatter:new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("description"), + new fjs.PropertyFormatter("type"), + new fjs.TextFormatter(""), + new fjs.ObjectFormatter("button",true,[ + new fjs.PropertyFormatter("emoji"), + new fjs.PropertyFormatter("label"), + new fjs.PropertyFormatter("color"), + ]), + new fjs.TextFormatter(""), + new fjs.ArrayFormatter("roles",false,new fjs.PropertyFormatter(null)), + new fjs.PropertyFormatter("mode"), + new fjs.ArrayFormatter("removeRolesOnAdd",false,new fjs.PropertyFormatter(null)), + new fjs.PropertyFormatter("addOnMemberJoin"), + ])} +])) + +export const defaultPanelsFormatter = new fjs.ArrayFormatter(null,true,new fjs.ObjectFormatter(null,true,[ + new fjs.PropertyFormatter("id"), + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("dropdown"), + new fjs.ArrayFormatter("options",false,new fjs.PropertyFormatter(null)), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("text"), + new fjs.ObjectFormatter("embed",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("title"), + new fjs.PropertyFormatter("description"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("customColor"), + new fjs.PropertyFormatter("url"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("image"), + new fjs.PropertyFormatter("thumbnail"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("footer"), + new fjs.ArrayFormatter("fields",true,new fjs.ObjectFormatter(null,false,[ + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("value"), + new fjs.PropertyFormatter("inline"), + ])), + new fjs.PropertyFormatter("timestamp"), + ]), + new fjs.ObjectFormatter("settings",true,[ + new fjs.PropertyFormatter("dropdownPlaceholder"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("enableMaxTicketsWarningInText"), + new fjs.PropertyFormatter("enableMaxTicketsWarningInEmbed"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("describeOptionsLayout"), + new fjs.PropertyFormatter("describeOptionsCustomTitle"), + new fjs.PropertyFormatter("describeOptionsInText"), + new fjs.PropertyFormatter("describeOptionsInEmbedFields"), + new fjs.PropertyFormatter("describeOptionsInEmbedDescription"), + ]), +])) + +export const defaultTranscriptsFormatter = new fjs.ObjectFormatter(null,true,[ + new fjs.ObjectFormatter("general",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("enableChannel"), + new fjs.PropertyFormatter("enableCreatorDM"), + new fjs.PropertyFormatter("enableParticipantDM"), + new fjs.PropertyFormatter("enableActiveAdminDM"), + new fjs.PropertyFormatter("enableEveryAdminDM"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("channel"), + new fjs.PropertyFormatter("mode"), + ]), + new fjs.ObjectFormatter("embedSettings",true,[ + new fjs.PropertyFormatter("customColor"), + new fjs.PropertyFormatter("listAllParticipants"), + new fjs.PropertyFormatter("includeTicketStats"), + ]), + new fjs.ObjectFormatter("textTranscriptStyle",true,[ + new fjs.PropertyFormatter("layout"), + new fjs.PropertyFormatter("includeStats"), + new fjs.PropertyFormatter("includeIds"), + new fjs.PropertyFormatter("includeEmbeds"), + new fjs.PropertyFormatter("includeFiles"), + new fjs.PropertyFormatter("includeBotMessages"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("fileMode"), + new fjs.PropertyFormatter("customFileName"), + ]), + new fjs.ObjectFormatter("htmlTranscriptStyle",true,[ + new fjs.ObjectFormatter("background",true,[ + new fjs.PropertyFormatter("enableCustomBackground"), + new fjs.PropertyFormatter("backgroundColor"), + new fjs.PropertyFormatter("backgroundImage"), + ]), + new fjs.ObjectFormatter("header",true,[ + new fjs.PropertyFormatter("enableCustomHeader"), + new fjs.PropertyFormatter("backgroundColor"), + new fjs.PropertyFormatter("decoColor"), + new fjs.PropertyFormatter("textColor"), + ]), + new fjs.ObjectFormatter("stats",true,[ + new fjs.PropertyFormatter("enableCustomStats"), + new fjs.PropertyFormatter("backgroundColor"), + new fjs.PropertyFormatter("keyTextColor"), + new fjs.PropertyFormatter("valueTextColor"), + new fjs.PropertyFormatter("hideBackgroundColor"), + new fjs.PropertyFormatter("hideTextColor"), + ]), + new fjs.ObjectFormatter("favicon",true,[ + new fjs.PropertyFormatter("enableCustomFavicon"), + new fjs.PropertyFormatter("imageUrl"), + ]), + ]), +]) \ No newline at end of file From e129724c12dd1af72060556c50ba78fa83ea0adb Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:53:31 +0200 Subject: [PATCH 12/78] Added display values for all general.json settings --- src/core/api/modules/checker.ts | 2 + src/core/startup/cli.ts | 2 +- src/data/framework/checkerLoader.ts | 155 ++++++++++++++-------------- 3 files changed, 83 insertions(+), 76 deletions(-) diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index c452391..cd1eab3 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -409,6 +409,8 @@ export interface ODCheckerStructureOptions { cliDisplayName?:string /**The description of this config in the Interactive Setup CLI. */ cliDisplayDescription?:string + /**Hide the description of this config in the Interactive Setup CLI parent view/list. */ + cliHideDescriptionInParent?:boolean /**The default value of this variable when creating it in the Interactive Setup CLI. When not specified, the user will be asked to insert a value. */ cliInitDefaultValue?:ODValidJsonType } diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 6764dc4..2fe8999 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -90,7 +90,7 @@ async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn: const list = structure.options.children.filter((child) => !child.cliHideInEditMode) const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName : child.key)) const nameLength = utilities.getLongestLength(nameList) - const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray(list[index].checker.options.cliDisplayDescription ? "=> "+list[index].checker.options.cliDisplayDescription : "")) + const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray((!list[index].checker.options.cliHideDescriptionInParent && list[index].checker.options.cliDisplayDescription) ? "=> "+list[index].checker.options.cliDisplayDescription : "")) const answer = await terminal.singleColumnMenu(finalnameList,{ diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index d5d608e..5487382 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -120,11 +120,11 @@ export const registerDefaultCheckerCustomTranslations = (tm:api.ODCheckerTransla } //UTILITY FUNCTIONS -const createMsgStructure = (id:api.ODValidId) => { +const createMsgStructure = (id:api.ODValidId,displayName:string) => { return new api.ODCheckerObjectStructure(id,{children:[ - {key:"dm",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-dm",{})}, - {key:"logs",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-logs",{})}, - ]}) + {key:"dm",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-dm",{cliInitDefaultValue:false,cliDisplayName:"DM Enabled",cliDisplayDescription:"Will this action be sent in DM to the creator of the ticket?"})}, + {key:"logs",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-logs",{cliInitDefaultValue:true,cliDisplayName:"Logs Enabled",cliDisplayDescription:"Will this action be sent in the Discord log channel?"})}, + ],cliDisplayName:displayName}) } const createTicketEmbedStructure = (id:api.ODValidId) => { return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ @@ -140,14 +140,14 @@ const createTicketEmbedStructure = (id:api.ODValidId) => { {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-field-inline",{})} ]})})}, {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-timestamp",{})} - ]})}) + ]}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",image:"",thumbnail:"",fields:[],timestamp:false}}) } const createTicketPingStructure = (id:api.ODValidId) => { return new api.ODCheckerObjectStructure(id,{children:[ {key:"@here",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-here",{})}, {key:"@everyone",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-everyone",{})}, {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false,cliDisplayPropertyName:"custom role id"})}, - ]}) + ],cliInitDefaultValue:{"@here":true,"@everyone":false,custom:[]}}) } const createPanelEmbedStructure = (id:api.ODValidId) => { return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ @@ -166,7 +166,7 @@ const createPanelEmbedStructure = (id:api.ODValidId) => { {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-field-inline",{})} ]})})}, {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-timestamp",{})} - ]})}) + ]}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",url:"",image:"",thumbnail:"",footer:"",fields:[],timestamp:false}}) } function loadFromEnv(){ @@ -194,8 +194,8 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis //BASIC {key:"token",optional:false,priority:0,checker:(loadFromEnv()) ? new api.ODCheckerStringStructure("opendiscord:token-disabled",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."}) : new api.ODCheckerCustomStructure_DiscordToken("opendiscord:token",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."})}, - {key:"tokenFromENV",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:token-env",{})}, - {key:"mainColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:main-color",true,false)}, + {key:"tokenFromENV",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:token-env",{cliDisplayName:"Token From ENV",cliDisplayDescription:"Use the token from the .env file instead of general.json."})}, + {key:"mainColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:main-color",true,false,{cliDisplayName:"Main Color",cliDisplayDescription:"The main color of your bot, used in almost all embeds."})}, {key:"language",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:language",{ custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) @@ -206,92 +206,97 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis return false }else return true }, - cliAutocompleteList:opendiscord.defaults.getDefault("languageList") + cliAutocompleteList:opendiscord.defaults.getDefault("languageList"), + cliDisplayName:"Language", + cliDisplayDescription:"The language of the bot. Visit README.md for a list of available translations." })}, - {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:prefix",{minLength:1})}, - {key:"serverId",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:server-id","server",false,[])}, - {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role"})}, - {key:"slashCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:slash-commands",{})}, - {key:"textCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:text-commands",{})}, + {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:prefix",{minLength:1,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix used for the text-commands from the bot."})}, + {key:"serverId",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:server-id","server",false,[],{cliDisplayName:"Server Id",cliDisplayDescription:"The ID of the discord server you will be using this bot in."})}, + {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role",cliDisplayName:"Global Admin Roles",cliDisplayDescription:"A list of role IDs that are able to interact with all commands and tickets."})}, + {key:"slashCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:slash-commands",{cliDisplayName:"Enable Slash Commands",cliDisplayDescription:"Enable/disable slash commands in the bot. When disabled, the commands will not be displayed."})}, + {key:"textCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:text-commands",{cliDisplayName:"Enable Text Commands",cliDisplayDescription:"Enable/disable text commands in the bot. (Disabling is recommended in large servers)"})}, //STATUS {key:"status",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:status",{ property:"enabled", enabledValue:true, checker:new api.ODCheckerObjectStructure("opendiscord:status",{children:[ - {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-type",{choices:["listening","watching","playing","custom"]})}, - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-text",{minLength:1,maxLength:128})}, - {key:"status",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-type",{choices:["online","invisible","idle","dnd"]})}, - ]}) + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:status-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the status. When disabled, the bot will be online without any status."})}, + {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-type",{choices:["listening","watching","playing","custom"],cliDisplayName:"Type",cliDisplayDescription:"The type of status: Listening, Watching, Playing or Custom."})}, + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-text",{minLength:1,maxLength:128,cliDisplayName:"Text",cliDisplayDescription:"The text displayed in the status."})}, + {key:"status",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-status",{choices:["online","invisible","idle","dnd"],cliDisplayName:"Status",cliDisplayDescription:"The profile status of the bot: Online, Invisible, Idle or Do Not Disturb."})}, + ]}), + cliDisplayName:"Bot Status", + cliDisplayDescription:"Manage the status of the bot." })}, //SYSTEM {key:"system",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system",{children:[ - {key:"removeParticipantsOnClose",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:remove-participants-on-close",{})}, - {key:"replyOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-ticket-creation",{})}, - {key:"replyOnReactionRole",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-reaction-role",{})}, - {key:"useTranslatedConfigChecker",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-translated-config-checker",{})}, - {key:"preferSlashOverText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:prefer-slash-over-text",{})}, - {key:"sendErrorOnUnknownCommand",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:send-error-on-unknown-command",{})}, - {key:"questionFieldsInCodeBlock",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-fields-in-code-block",{})}, - {key:"disableVerifyBars",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:disable-verify-bars",{})}, - {key:"useRedErrorEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-red-error-embeds",{})}, - {key:"emojiStyle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:emoji-style",{choices:["before","after","double","disabled"]})}, + {key:"removeParticipantsOnClose",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:remove-participants-on-close",{cliDisplayName:"Remove Participants On Close",cliDisplayDescription:"When enabled, all participants except admins will be removed from the ticket."})}, + {key:"replyOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-ticket-creation",{cliDisplayName:"Reply On Ticket Creation",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when creating a ticket."})}, + {key:"replyOnReactionRole",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-reaction-role",{cliDisplayName:"Reply On Reaction Role",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when using a role button."})}, + {key:"useTranslatedConfigChecker",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-translated-config-checker",{cliDisplayName:"Use Translated Config Checker",cliDisplayDescription:"Use a translated config checker to better understand the errors the bot gives."})}, + {key:"preferSlashOverText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:prefer-slash-over-text",{cliDisplayName:"Prefer Slash Over Text",cliDisplayDescription:"Prefer displaying slash commands over text commands in help menus."})}, + {key:"sendErrorOnUnknownCommand",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:send-error-on-unknown-command",{cliDisplayName:"Send Error On Unknown Command",cliDisplayDescription:"Send an error when using the text-command prefix without a valid command."})}, + {key:"questionFieldsInCodeBlock",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-fields-in-code-block",{cliDisplayName:"Questions Fields In Code Blocks",cliDisplayDescription:"Display question fields in code blocks instead of plain text."})}, + {key:"disableVerifyBars",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:disable-verify-bars",{cliDisplayName:"Disable Verifybars",cliDisplayDescription:"Disable the (✅/❌) verify buttons in all commands. (Not recommended)"})}, + {key:"useRedErrorEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-red-error-embeds",{cliDisplayName:"Use Red Error Embeds",cliDisplayDescription:"Display all error messages with a red border instead of the default color of the bot."})}, + {key:"emojiStyle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:emoji-style",{choices:["before","after","double","disabled"],cliDisplayName:"Emoji Style",cliDisplayDescription:"Choose how the bot will display emojis in message titles. (Visit docs for more info)"})}, - {key:"enableTicketClaimButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-claim-buttons",{})}, - {key:"enableTicketCloseButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-close-buttons",{})}, - {key:"enableTicketPinButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-pin-buttons",{})}, - {key:"enableTicketDeleteButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-delete-buttons",{})}, - {key:"enableTicketActionWithReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-action-with-reason",{})}, - {key:"enableDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-delete-without-transcript",{})}, + {key:"enableTicketClaimButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-claim-buttons",{cliDisplayName:"Enable Ticket Claim Buttons",cliDisplayDescription:"Enable/disable buttons for claiming a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketCloseButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-close-buttons",{cliDisplayName:"Enable Ticket Close Buttons",cliDisplayDescription:"Enable/disable buttons for closing a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketPinButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-pin-buttons",{cliDisplayName:"Enable Ticket Pin Buttons",cliDisplayDescription:"Enable/disable buttons for pinning a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketDeleteButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-delete-buttons",{cliDisplayName:"Enable Ticket Delete Buttons",cliDisplayDescription:"Enable/disable buttons for deleting a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketActionWithReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-action-with-reason",{cliDisplayName:"Enable Ticket Action With Reason",cliDisplayDescription:"Enable/disable buttons to write an additional reason for all ticket actions."})}, + {key:"enableDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-delete-without-transcript",{cliDisplayName:"Enable Delete Without Transcript",cliDisplayDescription:"Enable/disable the ability to delete tickets without a transcript."})}, {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:log-channel","channel",false,[])}, - ]})})}, + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:log-channel","channel",false,[],{cliDisplayName:"Log Channel",cliDisplayDescription:"The ID of the discord channel to log messages to. You can configure the messages somewhere else."})}, + ]}),cliDisplayName:"Discord Logs",cliDisplayDescription:"Manage everything related to logs in a discord channel."})}, {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:limits",{children:[ - {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})}, - {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})} - ]})})}, + {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets that are able to exist in the server at the same time."})}, + {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets from a specific user that are able to exist in the server at the same time."})} + ]}),cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."})}, {key:"permissions",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ - {key:"help",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-help","role",false,["admin","everyone","none"])}, - {key:"panel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-panel","role",false,["admin","everyone","none"])}, - {key:"ticket",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-ticket","role",false,["admin","everyone","none"])}, - {key:"close",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-close","role",false,["admin","everyone","none"])}, - {key:"delete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-delete","role",false,["admin","everyone","none"])}, - {key:"reopen",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-reopen","role",false,["admin","everyone","none"])}, - {key:"claim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-claim","role",false,["admin","everyone","none"])}, - {key:"unclaim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unclaim","role",false,["admin","everyone","none"])}, - {key:"pin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-pin","role",false,["admin","everyone","none"])}, - {key:"unpin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unpin","role",false,["admin","everyone","none"])}, - {key:"move",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-move","role",false,["admin","everyone","none"])}, - {key:"rename",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-rename","role",false,["admin","everyone","none"])}, - {key:"add",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-add","role",false,["admin","everyone","none"])}, - {key:"remove",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-remove","role",false,["admin","everyone","none"])}, - {key:"blacklist",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-blacklist","role",false,["admin","everyone","none"])}, - {key:"stats",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-stats","role",false,["admin","everyone","none"])}, - {key:"clear",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-clear","role",false,["admin","everyone","none"])}, - {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autoclose","role",false,["admin","everyone","none"])}, - {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autodelete","role",false,["admin","everyone","none"])} - ]})}, + {key:"help",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-help","role",false,["admin","everyone","none"],{cliDisplayName:"Help",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"panel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-panel","role",false,["admin","everyone","none"],{cliDisplayName:"Panel",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"ticket",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-ticket","role",false,["admin","everyone","none"],{cliDisplayName:"Ticket",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"close",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-close","role",false,["admin","everyone","none"],{cliDisplayName:"Close",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"delete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-delete","role",false,["admin","everyone","none"],{cliDisplayName:"Delete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"reopen",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-reopen","role",false,["admin","everyone","none"],{cliDisplayName:"Reopen",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"claim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-claim","role",false,["admin","everyone","none"],{cliDisplayName:"Claim",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"unclaim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unclaim","role",false,["admin","everyone","none"],{cliDisplayName:"Unclaim",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"pin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-pin","role",false,["admin","everyone","none"],{cliDisplayName:"Pin",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"unpin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unpin","role",false,["admin","everyone","none"],{cliDisplayName:"Unpin",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"move",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-move","role",false,["admin","everyone","none"],{cliDisplayName:"Move",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"rename",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-rename","role",false,["admin","everyone","none"],{cliDisplayName:"Rename",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"add",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-add","role",false,["admin","everyone","none"],{cliDisplayName:"Add User",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"remove",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-remove","role",false,["admin","everyone","none"],{cliDisplayName:"Remove User",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"blacklist",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-blacklist","role",false,["admin","everyone","none"],{cliDisplayName:"Blacklist",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"stats",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-stats","role",false,["admin","everyone","none"],{cliDisplayName:"Stats",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"clear",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-clear","role",false,["admin","everyone","none"],{cliDisplayName:"Clear Tickets",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autoclose","role",false,["admin","everyone","none"],{cliDisplayName:"Autoclose",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autodelete","role",false,["admin","everyone","none"],{cliDisplayName:"Autodelete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})} + ],cliDisplayName:"Permissions",cliDisplayDescription:"Manage all button & command permissions in the bot. (Visit docs for more info)"})}, {key:"messages",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ - {key:"creation",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-creation")}, - {key:"closing",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-closing")}, - {key:"deleting",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-deleting")}, - {key:"reopening",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-reopening")}, - {key:"claiming",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-claiming")}, - {key:"pinning",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-pinning")}, - {key:"adding",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-adding")}, - {key:"removing",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-removing")}, - {key:"renaming",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-renaming")}, - {key:"moving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-moving")}, - {key:"blacklisting",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-blacklisting")}, - {key:"roleAdding",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-role-adding")}, - {key:"roleRemoving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-role-removing")} - ]})}, - ]})} + {key:"creation",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-creation","Ticket Created")}, + {key:"closing",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-closing","Ticket Closed")}, + {key:"deleting",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-deleting","Ticket Deleted")}, + {key:"reopening",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-reopening","Ticket Reopened")}, + {key:"claiming",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-claiming","Ticket Claimed")}, + {key:"pinning",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-pinning","Ticket Pinned")}, + {key:"adding",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-adding","User Added")}, + {key:"removing",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-removing","User Removed")}, + {key:"renaming",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-renaming","Ticket Renamed")}, + {key:"moving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-moving","Ticket Moved")}, + {key:"blacklisting",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-blacklisting","User Blacklisted")}, + {key:"roleAdding",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-role-adding","Role Added")}, + {key:"roleRemoving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-role-removing","Role Removed")} + ],cliDisplayName:"Messages",cliDisplayDescription:"Manage all messages & DM's for each action of the bot. (Visit docs for more info)"})}, + ],cliDisplayName:"System",cliDisplayDescription:"Configure everything related to the ticket system."})} ]}) export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],cliDisplayPropertyName:"option",propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ From 605173e788246c225e88e3c2cb62b99ba3a4dc11 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:36:58 +0200 Subject: [PATCH 13/78] Added more display names + descriptions (Part 2) --- src/core/api/modules/checker.ts | 4 +- src/core/startup/cli.ts | 8 +- src/data/framework/checkerLoader.ts | 114 ++++++++++++++-------------- 3 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index cd1eab3..e220fe5 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -1418,10 +1418,10 @@ export class ODCheckerCustomStructure_UniqueIdArray extends ODCheckerArrayStruct /**The scope to push unique ids when used in this array! */ readonly usedScope: string|null - constructor(id:ODValidId, source:string, scope:string, usedScope?:string, options?:ODCheckerArrayStructureOptions){ + constructor(id:ODValidId, source:string, scope:string, usedScope?:string, options?:ODCheckerArrayStructureOptions, idOptions?:Omit){ //add premade custom structure checker const newOptions = options ?? {} - newOptions.propertyChecker = new ODCheckerStringStructure("opendiscord:unique-id",{minLength:1,custom:(checker,value,locationTrace,locationId,locationDocs) => { + newOptions.propertyChecker = new ODCheckerStringStructure("opendiscord:unique-id",{...(idOptions ?? {}),minLength:1,custom:(checker,value,locationTrace,locationId,locationDocs) => { if (typeof value != "string") return false const localLt = checker.locationTraceDeref(locationTrace) localLt.pop() diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 2fe8999..62b5c4f 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -87,12 +87,16 @@ async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn: terminal(ansis.bold.green("Please select which variable you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) if (!structure.options.children) return await backFn() + if (structure.options.cliDisplayName){ + terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName)+"\n") + terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") + } + const list = structure.options.children.filter((child) => !child.cliHideInEditMode) const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName : child.key)) const nameLength = utilities.getLongestLength(nameList) const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray((!list[index].checker.options.cliHideDescriptionInParent && list[index].checker.options.cliDisplayDescription) ? "=> "+list[index].checker.options.cliDisplayDescription : "")) - const answer = await terminal.singleColumnMenu(finalnameList,{ leftPadding:"> ", style:terminal.cyan, @@ -145,7 +149,7 @@ async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:( terminal(ansis.bold.green("Please select what you would like to do.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) if (!structure.options.propertyChecker) return await backFn() - if (typeof parentIndex == "string" || !isNaN(parentIndex)){ + if (structure.options.cliDisplayName || typeof parentIndex == "string" || !isNaN(parentIndex)){ terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? parentIndex.toString())+"\n") terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") } diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 5487382..15ca67a 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -124,49 +124,51 @@ const createMsgStructure = (id:api.ODValidId,displayName:string) => { return new api.ODCheckerObjectStructure(id,{children:[ {key:"dm",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-dm",{cliInitDefaultValue:false,cliDisplayName:"DM Enabled",cliDisplayDescription:"Will this action be sent in DM to the creator of the ticket?"})}, {key:"logs",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-logs",{cliInitDefaultValue:true,cliDisplayName:"Logs Enabled",cliDisplayDescription:"Will this action be sent in the Discord log channel?"})}, - ],cliDisplayName:displayName}) + ],cliDisplayName:displayName,cliDisplayDescription:"Configure which places this action gets logged/sent to."}) } const createTicketEmbedStructure = (id:api.ODValidId) => { return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ - {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-text",{maxLength:256})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-description",{maxLength:4096})}, - {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:ticket-embed-color",true,true)}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the embed of this message."})}, + {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-text",{maxLength:256,cliDisplayName:"Title",cliDisplayDescription:"The title of this embed."})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-description",{maxLength:4096,cliDisplayName:"Description",cliDisplayDescription:"The description of this embed."})}, + {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:ticket-embed-color",true,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Set a custom color for this embed. When empty, the default bot color will be used."})}, - {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, - {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, + {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Image",cliDisplayDescription:"Add an image to the embed using an image URL."})}, + {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Thumbnail",cliDisplayDescription:"Add a thumbnail to the embed using an image URL."})}, {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-embed-fields",{children:[ - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-name",{minLength:1,maxLength:256})}, - {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-value",{minLength:1,maxLength:1024})}, - {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-field-inline",{})} - ]})})}, - {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-timestamp",{})} - ]}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",image:"",thumbnail:"",fields:[],timestamp:false}}) + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-name",{minLength:1,maxLength:256,cliDisplayName:"Field Name",cliDisplayDescription:"The name/title of this embed field."})}, + {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-value",{minLength:1,maxLength:1024,cliDisplayName:"Field Value",cliDisplayDescription:"The value/description of this embed field."})}, + {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-field-inline",{cliDisplayName:"Field Inline",cliDisplayDescription:"Should this field be displayed inline with other fields?"})} + ],cliDisplayName:"Field",cliDisplayDescription:"Customise and configure an embed field."}),cliDisplayName:"Fields",cliDisplayDescription:"Customise and configure embed fields."})}, + {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-timestamp",{cliDisplayName:"Timestamp",cliDisplayDescription:"Add a timestamp to the embed."})} + ],cliDisplayName:"Message Embed",cliDisplayDescription:"Configure the embed of this message."}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",image:"",thumbnail:"",fields:[],timestamp:false},cliDisplayName:"Message Embed",cliDisplayDescription:"Configure the embed of this message."}) } const createTicketPingStructure = (id:api.ODValidId) => { return new api.ODCheckerObjectStructure(id,{children:[ - {key:"@here",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-here",{})}, - {key:"@everyone",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-everyone",{})}, - {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false,cliDisplayPropertyName:"custom role id"})}, - ],cliInitDefaultValue:{"@here":true,"@everyone":false,custom:[]}}) + {key:"@here",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-here",{cliDisplayName:"@here Ping",cliDisplayDescription:"Enable/disable an '@here' ping."})}, + {key:"@everyone",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-everyone",{cliDisplayName:"@everyone Ping",cliDisplayDescription:"Enable/disable an '@everyone' ping."})}, + {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false,cliDisplayPropertyName:"custom role id",cliDisplayName:"Custom Role Ping",cliDisplayDescription:"Choose your own roles to ping in this message."},{cliDisplayName:"Custom Role",cliDisplayDescription:"The discord role ID of a custom mention/ping."})}, + ],cliInitDefaultValue:{"@here":true,"@everyone":false,custom:[],cliDisplayName:"Message Pings",cliDisplayDescription:"Configure the pings/mentions of this message."}}) } const createPanelEmbedStructure = (id:api.ODValidId) => { return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ - {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-text",{maxLength:256})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-description",{maxLength:4096})}, - {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:panel-embed-color",true,true)}, - {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-url",true,{allowHttp:false})}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the embed of this panel."})}, + {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-text",{maxLength:256,cliDisplayName:"Title",cliDisplayDescription:"The title of this embed."})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-description",{maxLength:4096,cliDisplayName:"Description",cliDisplayDescription:"The description of this embed."})}, + {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:panel-embed-color",true,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Set a custom color for this embed. When empty, the default bot color will be used."})}, + {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-url",true,{allowHttp:false},{cliDisplayName:"URL",cliDisplayDescription:"Set a URL which will be displayed in the title of the embed."})}, - {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, - {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, + {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Image",cliDisplayDescription:"Add an image to the embed using an image URL."})}, + {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Thumbnail",cliDisplayDescription:"Add a thumbnail to the embed using an image URL."})}, - {key:"footer",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-footer",{maxLength:2048})}, + {key:"footer",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-footer",{maxLength:2048,cliDisplayName:"Footer",cliDisplayDescription:"The footer of this embed."})}, {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:panel-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panel-embed-fields",{children:[ - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-name",{minLength:1,maxLength:256})}, - {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-value",{minLength:1,maxLength:1024})}, - {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-field-inline",{})} - ]})})}, - {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-timestamp",{})} - ]}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",url:"",image:"",thumbnail:"",footer:"",fields:[],timestamp:false}}) + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-name",{minLength:1,maxLength:256,cliDisplayName:"Field Name",cliDisplayDescription:"The name/title of this embed field."})}, + {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-value",{minLength:1,maxLength:1024,cliDisplayName:"Field Value",cliDisplayDescription:"The value/description of this embed field."})}, + {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-field-inline",{cliDisplayName:"Field Inline",cliDisplayDescription:"Should this field be displayed inline with other fields?"})} + ],cliDisplayName:"Field",cliDisplayDescription:"Customise and configure an embed field."}),cliDisplayName:"Fields",cliDisplayDescription:"Customise and configure embed fields."})}, + {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-timestamp",{cliDisplayName:"Timestamp",cliDisplayDescription:"Add a timestamp to the embed."})} + ],cliDisplayName:"Panel Embed",cliDisplayDescription:"Configure the embed of this panel."}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",url:"",image:"",thumbnail:"",footer:"",fields:[],timestamp:false},cliDisplayName:"Panel Embed",cliDisplayDescription:"Configure the embed of this panel."}) } function loadFromEnv(){ @@ -177,7 +179,7 @@ function loadFromEnv(){ //STRUCTURES export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendiscord:general",{children:[ - //STATUS + //INFO {key:"_INFO",optional:false,priority:0,cliHideInEditMode:true,checker:new api.ODCheckerObjectStructure("opendiscord:info",{children:[ {key:"support",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-support",{choices:["https://otdocs.dj-dj.be"]})}, {key:"discord",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-discord",{choices:["https://discord.dj-dj.be"]})}, @@ -212,7 +214,7 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis })}, {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:prefix",{minLength:1,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix used for the text-commands from the bot."})}, {key:"serverId",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:server-id","server",false,[],{cliDisplayName:"Server Id",cliDisplayDescription:"The ID of the discord server you will be using this bot in."})}, - {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role",cliDisplayName:"Global Admin Roles",cliDisplayDescription:"A list of role IDs that are able to interact with all commands and tickets."})}, + {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role",cliDisplayName:"Global Admin Roles",cliDisplayDescription:"A list of role IDs that are able to interact with all commands and tickets."},{cliDisplayName:"Global Admin Role",cliDisplayDescription:"The discord role ID of a global admin."})}, {key:"slashCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:slash-commands",{cliDisplayName:"Enable Slash Commands",cliDisplayDescription:"Enable/disable slash commands in the bot. When disabled, the commands will not be displayed."})}, {key:"textCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:text-commands",{cliDisplayName:"Enable Text Commands",cliDisplayDescription:"Enable/disable text commands in the bot. (Disabling is recommended in large servers)"})}, @@ -225,7 +227,7 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-type",{choices:["listening","watching","playing","custom"],cliDisplayName:"Type",cliDisplayDescription:"The type of status: Listening, Watching, Playing or Custom."})}, {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-text",{minLength:1,maxLength:128,cliDisplayName:"Text",cliDisplayDescription:"The text displayed in the status."})}, {key:"status",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-status",{choices:["online","invisible","idle","dnd"],cliDisplayName:"Status",cliDisplayDescription:"The profile status of the bot: Online, Invisible, Idle or Do Not Disturb."})}, - ]}), + ],cliDisplayName:"Bot Status",cliDisplayDescription:"Manage the status of the bot."}), cliDisplayName:"Bot Status", cliDisplayDescription:"Manage the status of the bot." })}, @@ -251,10 +253,12 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"enableDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-delete-without-transcript",{cliDisplayName:"Enable Delete Without Transcript",cliDisplayDescription:"Enable/disable the ability to delete tickets without a transcript."})}, {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:log-channel","channel",false,[],{cliDisplayName:"Log Channel",cliDisplayDescription:"The ID of the discord channel to log messages to. You can configure the messages somewhere else."})}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:logs-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable discord logs in a discord channel."})}, + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:logs-channel","channel",false,[],{cliDisplayName:"Log Channel",cliDisplayDescription:"The ID of the discord channel to log messages to. You can configure the messages somewhere else."})}, ]}),cliDisplayName:"Discord Logs",cliDisplayDescription:"Manage everything related to logs in a discord channel."})}, {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:limits",{children:[ + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable global limits."})}, {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets that are able to exist in the server at the same time."})}, {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets from a specific user that are able to exist in the server at the same time."})} ]}),cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."})}, @@ -301,16 +305,16 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],cliDisplayPropertyName:"option",propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ //TICKET - {name:"ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-name",{minLength:2,maxLength:45})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-description",{maxLength:256})}, + {name:"Ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this ticket option. Used in panels."})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this ticket option."})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this ticket option."})}, //TICKET BUTTON {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ - {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true)}, - {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80})}, - {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"]})}, + {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"],cliDisplayName:"Color",cliDisplayDescription:"The color of the button. Does not apply when using a dropdown panel."})}, ],custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) //check if emoji & label exists @@ -320,28 +324,28 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc checker.createMessage("opendiscord:invalid-button","error",`This button needs to have at least an "emoji" or "label"!`,lt,null,[`"emoji"`,`"label"`],locationId,locationDocs) return false }else return true - }})}, + },cliDisplayName:"Button",cliDisplayDescription:"Customise the layout of the button/dropdown of this ticket option."})}, //TICKET ADMINS - {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role"})}, - {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"read-only ticket admin role"})}, - {key:"allowCreationByBlacklistedUsers",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-allow-blacklisted-users",{})}, - {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question"})}, + {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role",cliDisplayName:"Ticket Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to interact with this ticket option."},{cliDisplayName:"Ticket Admin Role",cliDisplayDescription:"The discord role ID of a ticket admin."})}, + {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"read-only ticket admin role",cliDisplayName:"Readonly Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to read this ticket option."},{cliDisplayName:"Readonly Admin Role",cliDisplayDescription:"The discord role ID of a readonly admin."})}, + {key:"allowCreationByBlacklistedUsers",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-allow-blacklisted-users",{cliDisplayName:"Allow Creation By Blacklisted Users",cliDisplayDescription:"When enabled, the blacklist doesn't apply to this ticket option/type and users are still able to create a ticket."})}, + {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question",cliDisplayName:"Questions",cliDisplayDescription:"A list of valid question IDs to ask before creating this ticket."},{cliDisplayName:"Question ID",cliDisplayDescription:"A valid question ID from the questions.json config."})}, //TICKET CHANNEL {key:"channel",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{children:[ - {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-prefix",{maxLength:25,regex:/^[^\s]*$/})}, - {key:"suffix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-suffix",{choices:["user-name","user-id","random-number","random-hex","counter-dynamic","counter-fixed"]})}, + {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-prefix",{maxLength:25,regex:/^[^\s]*$/,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix of the name of the ticket channel. (e.g. 'question-')"})}, + {key:"suffix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-suffix",{choices:["user-name","user-id","random-number","random-hex","counter-dynamic","counter-fixed"],cliDisplayName:"Suffix",cliDisplayDescription:"The suffix mode to use. The number/text will be appended after the prefix."})}, - {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-category","category",true,[])}, - {key:"closedCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-closed-category","category",true,[])}, - {key:"backupCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-backup-category","category",true,[])}, + {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-category","category",true,[],{cliDisplayName:"Category",cliDisplayDescription:"The category the ticket will be created in. Leave empty for no category."})}, + {key:"closedCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-closed-category","category",true,[],{cliDisplayName:"Closed Category",cliDisplayDescription:"An additional category where the ticket will be moved to when closed."})}, + {key:"backupCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-backup-category","category",true,[],{cliDisplayName:"Backup Category",cliDisplayDescription:"An additional category where the ticket will be created in when the primary category is full (50 channels)."})}, {key:"claimedCategory",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],cliDisplayPropertyName:"claim category",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel-claimed-category",{children:[ - {key:"user",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-user","user",false,[])}, - {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-category","category",false,[])} - ]})})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-description",{})}, - ]})}, + {key:"user",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-user","user",false,[],{cliDisplayName:"User",cliDisplayDescription:"A discord user ID of the ticket claimer."})}, + {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-category","category",false,[],{cliDisplayName:"Category",cliDisplayDescription:"A discord category ID to move the ticket to."})} + ],cliDisplayName:"Claimed Category",cliDisplayDescription:"A collection of a user ID and a category ID. The ticket will be moved to the category when this user claims the ticket."}),cliDisplayName:"Claimed Categories",cliDisplayDescription:"Add categories to move the ticket to when a user claims a ticket."})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-description",{cliDisplayName:"Channel Description",cliDisplayDescription:"The description of the ticket channel. Visible in the discord client."})}, + ],cliDisplayName:"Channel",cliDisplayDescription:"Manage all settings related to the ticket channel and categories."})}, //DM MESSAGE {key:"dmMessage",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-dm-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-dm-message",{children:[ From 515b7e1ffc6c5653ecbf76b309860236f4b90f20 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 16 Apr 2025 09:18:13 +0200 Subject: [PATCH 14/78] Added more display names + descriptions (Part 3) --- src/data/framework/checkerLoader.ts | 126 ++++++++++++++-------------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 15ca67a..0d01cf0 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -255,13 +255,13 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:logs-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable discord logs in a discord channel."})}, {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:logs-channel","channel",false,[],{cliDisplayName:"Log Channel",cliDisplayDescription:"The ID of the discord channel to log messages to. You can configure the messages somewhere else."})}, - ]}),cliDisplayName:"Discord Logs",cliDisplayDescription:"Manage everything related to logs in a discord channel."})}, + ],cliDisplayName:"Discord Logs",cliDisplayDescription:"Manage everything related to logs in a discord channel."}),cliDisplayName:"Discord Logs",cliDisplayDescription:"Manage everything related to logs in a discord channel."})}, {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:limits",{children:[ {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable global limits."})}, {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets that are able to exist in the server at the same time."})}, {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets from a specific user that are able to exist in the server at the same time."})} - ]}),cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."})}, + ],cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."}),cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."})}, {key:"permissions",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ {key:"help",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-help","role",false,["admin","everyone","none"],{cliDisplayName:"Help",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, @@ -324,7 +324,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc checker.createMessage("opendiscord:invalid-button","error",`This button needs to have at least an "emoji" or "label"!`,lt,null,[`"emoji"`,`"label"`],locationId,locationDocs) return false }else return true - },cliDisplayName:"Button",cliDisplayDescription:"Customise the layout of the button/dropdown of this ticket option."})}, + },cliDisplayName:"Button",cliDisplayDescription:"Customise the button/dropdown layout of this ticket option."})}, //TICKET ADMINS {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role",cliDisplayName:"Ticket Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to interact with this ticket option."},{cliDisplayName:"Ticket Admin Role",cliDisplayDescription:"The discord role ID of a ticket admin."})}, @@ -349,55 +349,59 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc //DM MESSAGE {key:"dmMessage",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-dm-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-dm-message",{children:[ - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096})}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-message-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the DM message on ticket creation."})}, + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096,cliDisplayName:"Message Text",cliDisplayDescription:"The raw text of the DM message. Leave empty to only use the embed."})}, {key:"embed",optional:false,priority:0,checker:createTicketEmbedStructure("opendiscord:ticket-message-embed")} - ]})})}, + ],cliDisplayName:"DM Message",cliDisplayDescription:"The DM message is the message that will be sent to the creator of the ticket when he/she creates a ticket."}),cliDisplayName:"DM Message",cliDisplayDescription:"The DM message is the message that will be sent to the creator of the ticket when he/she creates a ticket."})}, //TICKET MESSAGE {key:"ticketMessage",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-message",{children:[ - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096})}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-message-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the ticket message on ticket creation. (Recommended)"})}, + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096,cliDisplayName:"Message Text",cliDisplayDescription:"The raw text of the ticket message. Leave empty to only use the embed."})}, {key:"embed",optional:false,priority:0,checker:createTicketEmbedStructure("opendiscord:ticket-message-embed")}, {key:"ping",optional:false,priority:0,checker:createTicketPingStructure("opendiscord:ticket-message-ping")} - ]})})}, + ],cliDisplayName:"Ticket Message",cliDisplayDescription:"The Ticket Message is the message that will be sent in the ticket itself. It contains a few buttons for quick access to actions."}),cliDisplayName:"Ticket Message",cliDisplayDescription:"The Ticket Message is the message that will be sent in the ticket itself. It contains a few buttons for quick access to actions."})}, //AUTOCLOSE {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-autoclose",{children:[ - {key:"enableInactiveHours",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-hours",{})}, - {key:"inactiveHours",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autoclose-hours",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:8544})}, - {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-leave",{})}, - {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-disable-claim",{})}, - ]})}, + {key:"enableInactiveHours",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-hours",{cliDisplayName:"Enable Inactive Hours",cliDisplayDescription:"Enable/disable closing the ticket when it has been inactive for the configured amount of time."})}, + {key:"inactiveHours",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autoclose-hours",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:8544,cliDisplayName:"Inactive Hours",cliDisplayDescription:"The amount of hours the ticket must be inactive."})}, + {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-leave",{cliDisplayName:"Enable User Leave",cliDisplayDescription:"Instantly close the ticket when the creator of the ticket leaves the server."})}, + {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-disable-claim",{cliDisplayName:"Disable On Claim",cliDisplayDescription:"Disable the autoclose system when the ticket is claimed by any admin."})}, + ],cliDisplayName:"Autoclose",cliDisplayDescription:"Manage the autoclose system for this ticket type/option."})}, //AUTODELETE {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-autodelete",{children:[ - {key:"enableInactiveDays",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-days",{})}, - {key:"inactiveDays",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autodelete-days",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:356})}, - {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-leave",{})}, - {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-disable-claim",{})}, - ]})}, + {key:"enableInactiveDays",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-days",{cliDisplayName:"Enable Inactive Days",cliDisplayDescription:"Enable/disable deleting the ticket when it has been inactive for the configured amount of time."})}, + {key:"inactiveDays",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autodelete-days",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:356,cliDisplayName:"Inactive Days",cliDisplayDescription:"The amount of days the ticket must be inactive."})}, + {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-leave",{cliDisplayName:"Enable User Leave",cliDisplayDescription:"Instantly delete the ticket when the creator of the ticket leaves the server."})}, + {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-disable-claim",{cliDisplayName:"Disable On Claim",cliDisplayDescription:"Disable the autodelete system when the ticket is claimed by any admin."})}, + ],cliDisplayName:"Autodelete",cliDisplayDescription:"Manage the autodelete system for this ticket type/option."})}, //COOLDOWN {key:"cooldown",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-cooldown",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-cooldown",{children:[ - {key:"cooldownMinutes",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-cooldown-minutes",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,max:512640})}, - ]})})}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-cooldown-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the cooldown of this ticket option."})}, + {key:"cooldownMinutes",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-cooldown-minutes",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,max:512640,cliDisplayName:"Cooldown Minutes",cliDisplayDescription:"The amount of minutes a user needs to wait before creating another ticket of this type/option."})}, + ],cliDisplayName:"Cooldown",cliDisplayDescription:"Manage cooldowns for this ticket type/option."}),cliDisplayName:"Cooldown",cliDisplayDescription:"Manage cooldowns for this ticket type/option."})}, //LIMITS {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-limits",{children:[ - {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})}, - {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})} - ]})})}, - ]})}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the limits of this ticket option. This is not related to the global ticket limits."})}, + {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option that are able to exist in the server at the same time."})}, + {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option from a specific user that are able to exist in the server at the same time."})} + ],cliDisplayName:"Option Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."}),cliDisplayName:"Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."})}, + ],cliDisplayName:"Ticket Option",cliDisplayDescription:"Manage all ticket-specific settings of this option/type."})}, //WEBSITE - {name:"website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-website",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:website-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-name",{minLength:2,maxLength:45})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-description",{maxLength:256})}, + {name:"Website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("opendiscord:website",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:website-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this website option. Used in panels."})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this website option."})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this website option."})}, //WEBSITE BUTTON {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ - {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true)}, - {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80})}, + {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, ],custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) //check if emoji & label exists @@ -407,23 +411,23 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc checker.createMessage("opendiscord:invalid-button","error",`This button needs to have at least an "emoji" or "label"!`,lt,null,[`"emoji"`,`"label"`],locationId,locationDocs) return false }else return true - }})}, + },cliDisplayName:"Button",cliDisplayDescription:"Customise the button layout of this website option."})}, //WEBSITE URL - {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:website-url",false,{allowHttp:false})}, - ]})}, + {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:website-url",false,{allowHttp:false},{cliDisplayName:"URL",cliDisplayDescription:"The URL this button will link to."})}, + ],cliDisplayName:"Website Option",cliDisplayDescription:"Manage all settings of this website/url option."})}, //REACTION ROLES - {name:"role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-role",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:role-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-name",{minLength:2,maxLength:45})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-description",{maxLength:256})}, + {name:"Reaction Role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("opendiscord:role",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:role-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this role option. Used in panels."})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this role option."})}, + {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this role option."})}, //ROLE BUTTON {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ - {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true)}, - {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80})}, - {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"]})}, + {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"],cliDisplayName:"Color",cliDisplayDescription:"The color of the button. Does not apply when using a dropdown panel."})}, ],custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) //check if emoji & label exists @@ -433,39 +437,39 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc checker.createMessage("opendiscord:invalid-button","error",`This button needs to have at least an "emoji" or "label"!`,lt,null,[`"emoji"`,`"label"`],locationId,locationDocs) return false }else return true - }})}, + },cliDisplayName:"Button",cliDisplayDescription:"Customise the button layout of this reaction role option."})}, //ROLE SETTINGS - {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1,cliDisplayPropertyName:"role"})}, - {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-mode",{choices:["add","remove","add&remove"]})}, - {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false,cliDisplayPropertyName:"role"})}, - {key:"addOnMemberJoin",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:role-add-on-join",{})}, - ]})}, -]})}) + {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1,cliDisplayPropertyName:"role",cliDisplayName:"Roles",cliDisplayDescription:"A list of roles to add/remove when clicking on the button."},{cliDisplayName:"Role",cliDisplayDescription:"The discord role ID you want to add/remove."})}, + {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-mode",{choices:["add","remove","add&remove"],cliDisplayName:"Mode",cliDisplayDescription:"Decide how the button will work: add-only, remove-only or add & remove."})}, + {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false,cliDisplayPropertyName:"role",cliDisplayName:"Remove Roles On Add",cliDisplayDescription:"An additional list of roles to remove when the roles of this option are added. (Can be used to select between roles)"},{cliDisplayName:"Remove Role",cliDisplayDescription:"The discord role ID you want to remove when other roles are added."})}, + {key:"addOnMemberJoin",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:role-add-on-join",{cliDisplayName:"Add On Member Join",cliDisplayDescription:"Automatically add these roles to a user when joining the server."})}, + ],cliDisplayName:"Reaction Role Option",cliDisplayDescription:"Manage all settings of this reaction role option."})}, +],cliDisplayName:"Option",cliDisplayDescription:"Manage an option of one of the 3 types: ticket, website, role."}),cliDisplayName:"Options",cliDisplayDescription:"A list of all options in the bot. Here you can add, modify & remove ticket types, website buttons & reaction roles!"}) export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendiscord:panels",{allowedTypes:["object"],cliDisplayPropertyName:"panel",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panels",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","dropdown"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-name",{minLength:3,maxLength:50})}, - {key:"dropdown",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-dropdown",{})}, - {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25,cliDisplayPropertyName:"option"})}, + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this panel. Used in the /panel command."})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-name",{minLength:3,maxLength:50,cliDisplayName:"Name",cliDisplayDescription:"The name of this panel."})}, + {key:"dropdown",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-dropdown",{cliDisplayName:"Dropdown",cliDisplayDescription:"Decide whether to use buttons or a dropdown in the panel. Dropdowns only support options of the 'ticket' type!"})}, + {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25,cliDisplayPropertyName:"option",cliDisplayName:"Options",cliDisplayDescription:"A list of valid option IDs to show in this panel."},{cliDisplayName:"Option ID",cliDisplayDescription:"A valid option ID from the options.json config."})}, //EMBED & TEXT - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-text",{maxLength:4096})}, + {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-text",{maxLength:4096,cliDisplayName:"Panel Text",cliDisplayDescription:"The raw text of the panel message. Leave empty to use the embed."})}, {key:"embed",optional:false,priority:0,checker:createPanelEmbedStructure("opendiscord:panel-embed")}, //SETTINGS {key:"settings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:panel-settings",{children:[ - {key:"dropdownPlaceholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-placeholder",{maxLength:100})}, - {key:"enableMaxTicketsWarningInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-text",{})}, - {key:"enableMaxTicketsWarningInEmbed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-embed",{})}, + {key:"dropdownPlaceholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-placeholder",{maxLength:100,cliDisplayName:"Dropdown Placeholder",cliDisplayDescription:"Configure the text displayed in the dropdown when nothing is selected."})}, + {key:"enableMaxTicketsWarningInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-text",{cliDisplayName:"Enable Max Tickets Warning (Text)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the text contents of the panel."})}, + {key:"enableMaxTicketsWarningInEmbed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-embed",{cliDisplayName:"Enable Max Tickets Warning (Embed)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the embed of the panel."})}, - {key:"describeOptionsLayout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-layout",{choices:["simple","normal","detailed"]})}, - {key:"describeOptionsCustomTitle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-title",{maxLength:512})}, - {key:"describeOptionsInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-text",{})}, - {key:"describeOptionsInEmbedFields",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-fields",{})}, - {key:"describeOptionsInEmbedDescription",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-embed",{})}, - ]})}, -]})}) + {key:"describeOptionsLayout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-layout",{choices:["simple","normal","detailed"],cliDisplayName:"Describe Options Layout",cliDisplayDescription:"The layout to use in the auto-generated option descriptions (simple, normal, detailed)."})}, + {key:"describeOptionsCustomTitle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-title",{maxLength:512,cliDisplayName:"Describe Options Title",cliDisplayDescription:"Customise the title to use in the auto-generated option descriptions."})}, + {key:"describeOptionsInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-text",{cliDisplayName:"Describe Options In Text",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the raw text contents of the panel."})}, + {key:"describeOptionsInEmbedFields",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-fields",{cliDisplayName:"Describe Options In Embed Fields",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the embed fields of the panel."})}, + {key:"describeOptionsInEmbedDescription",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-embed",{cliDisplayName:"Describe Options In Embed Description",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the embed description of the panel."})}, + ],cliDisplayName:"Settings",cliDisplayDescription:"Manage additional settings & customisability for this panel."})}, +],cliDisplayName:"Panel",cliDisplayDescription:"Manage, customise and configure a panel to your preference."}),cliDisplayName:"Panels",cliDisplayDescription:"A list of all panels in the bot. Here you can add, modify & remove existing panels or customise them to your preference."}) export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("opendiscord:questions",{allowedTypes:["object"],cliDisplayPropertyName:"question",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:questions",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, From ad623f9650cf2d8823a6c1110e7cb77971b2a0fa Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:20:07 +0200 Subject: [PATCH 15/78] Finished display names + descriptions (Part 4) --- package.json | 2 +- src/core/api/modules/checker.ts | 4 +- src/core/startup/cli.ts | 6 +- src/data/framework/checkerLoader.ts | 131 ++++++++++++++++------------ 4 files changed, 82 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 812f2d8..2844151 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@types/terminal-kit": "^2.5.7", "ansis": "^2.3.0", "discord.js": "^14.17.2", - "formatted-json-stringify": "^1.2.0", + "formatted-json-stringify": "^1.2.1", "terminal-kit": "^3.1.2", "typescript": "^5.5.4" }, diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index e220fe5..26af713 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -529,7 +529,9 @@ export interface ODCheckerStringStructureOptions extends ODCheckerStructureOptio /**The string needs to match this regex */ regex?:RegExp, /**Provide an optional list for autocomplete when using the Interactive Setup CLI. Defaults to the `choices` option. */ - cliAutocompleteList?:string[] + cliAutocompleteList?:string[], + /**Dynamically provide a list for autocomplete items when using the Interactive Setup CLI. */ + cliAutocompleteFunc?:() => Promise } /**## ODCheckerStringStructure `class` diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts index 62b5c4f..6cc6331 100644 --- a/src/core/startup/cli.ts +++ b/src/core/startup/cli.ts @@ -388,7 +388,8 @@ async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(( terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const customExtraOptions = (structure instanceof api.ODCheckerCustomStructure_DiscordId) ? structure.extraOptions : undefined - const autocompleteList = (structure.options.cliAutocompleteList ?? customExtraOptions) ?? structure.options.choices + const customAutocompleteFunc = structure.options.cliAutocompleteFunc ? await structure.options.cliAutocompleteFunc() : null + const autocompleteList = ((customAutocompleteFunc ?? structure.options.cliAutocompleteList) ?? customExtraOptions) ?? structure.options.choices const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, selectedStyle:terminal.bgBlue.white @@ -810,7 +811,8 @@ async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn: terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n") const customExtraOptions = (structure instanceof api.ODCheckerCustomStructure_DiscordId) ? structure.extraOptions : undefined - const autocompleteList = (structure.options.cliAutocompleteList ?? customExtraOptions) ?? structure.options.choices + const customAutocompleteFunc = structure.options.cliAutocompleteFunc ? await structure.options.cliAutocompleteFunc() : null + const autocompleteList = ((customAutocompleteFunc ?? structure.options.cliAutocompleteList) ?? customExtraOptions) ?? structure.options.choices const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, selectedStyle:terminal.bgBlue.white diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 0d01cf0..c4076fe 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -301,11 +301,11 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"roleRemoving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-role-removing","Role Removed")} ],cliDisplayName:"Messages",cliDisplayDescription:"Manage all messages & DM's for each action of the bot. (Visit docs for more info)"})}, ],cliDisplayName:"System",cliDisplayDescription:"Configure everything related to the ticket system."})} -]}) +],cliDisplayName:"General",cliDisplayDescription:"General settings for the bot."}) export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],cliDisplayPropertyName:"option",propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ //TICKET - {name:"Ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ + {name:"Ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],cliInitSkipKeys:["readonlyAdmins"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this ticket option. Used in panels."})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this ticket option."})}, {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this ticket option."})}, @@ -328,12 +328,17 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc //TICKET ADMINS {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role",cliDisplayName:"Ticket Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to interact with this ticket option."},{cliDisplayName:"Ticket Admin Role",cliDisplayDescription:"The discord role ID of a ticket admin."})}, - {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"read-only ticket admin role",cliDisplayName:"Readonly Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to read this ticket option."},{cliDisplayName:"Readonly Admin Role",cliDisplayDescription:"The discord role ID of a readonly admin."})}, + {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliInitDefaultValue:[],cliDisplayPropertyName:"read-only ticket admin role",cliDisplayName:"Readonly Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to read this ticket option."},{cliDisplayName:"Readonly Admin Role",cliDisplayDescription:"The discord role ID of a readonly admin."})}, {key:"allowCreationByBlacklistedUsers",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-allow-blacklisted-users",{cliDisplayName:"Allow Creation By Blacklisted Users",cliDisplayDescription:"When enabled, the blacklist doesn't apply to this ticket option/type and users are still able to create a ticket."})}, - {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question",cliDisplayName:"Questions",cliDisplayDescription:"A list of valid question IDs to ask before creating this ticket."},{cliDisplayName:"Question ID",cliDisplayDescription:"A valid question ID from the questions.json config."})}, + {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question",cliDisplayName:"Questions",cliDisplayDescription:"A list of valid question IDs to ask before creating this ticket."},{cliDisplayName:"Question ID",cliDisplayDescription:"A valid question ID from the questions.json config.",cliAutocompleteFunc:async () => { + const uncheckedRawData = opendiscord.configs.get("opendiscord:questions").data + if (!Array.isArray(uncheckedRawData)) return null + const idList = uncheckedRawData.filter((option) => typeof option == "object" && typeof option["id"] == "string").map((option) => option.id) + return (idList.length > 0) ? idList : null + }})}, //TICKET CHANNEL - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{children:[ + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{cliInitSkipKeys:["backupCategory","claimedCategory"],children:[ {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-prefix",{maxLength:25,regex:/^[^\s]*$/,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix of the name of the ticket channel. (e.g. 'question-')"})}, {key:"suffix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-suffix",{choices:["user-name","user-id","random-number","random-hex","counter-dynamic","counter-fixed"],cliDisplayName:"Suffix",cliDisplayDescription:"The suffix mode to use. The number/text will be appended after the prefix."})}, @@ -387,8 +392,8 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc //LIMITS {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-limits",{children:[ {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the limits of this ticket option. This is not related to the global ticket limits."})}, - {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option that are able to exist in the server at the same time."})}, - {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option from a specific user that are able to exist in the server at the same time."})} + {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:10,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option that are able to exist in the server at the same time."})}, + {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:3,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option from a specific user that are able to exist in the server at the same time."})} ],cliDisplayName:"Option Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."}),cliDisplayName:"Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."})}, ],cliDisplayName:"Ticket Option",cliDisplayDescription:"Manage all ticket-specific settings of this option/type."})}, @@ -451,15 +456,20 @@ export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendisco {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this panel. Used in the /panel command."})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-name",{minLength:3,maxLength:50,cliDisplayName:"Name",cliDisplayDescription:"The name of this panel."})}, {key:"dropdown",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-dropdown",{cliDisplayName:"Dropdown",cliDisplayDescription:"Decide whether to use buttons or a dropdown in the panel. Dropdowns only support options of the 'ticket' type!"})}, - {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25,cliDisplayPropertyName:"option",cliDisplayName:"Options",cliDisplayDescription:"A list of valid option IDs to show in this panel."},{cliDisplayName:"Option ID",cliDisplayDescription:"A valid option ID from the options.json config."})}, + {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25,cliDisplayPropertyName:"option",cliDisplayName:"Options",cliDisplayDescription:"A list of valid option IDs to show in this panel."},{cliDisplayName:"Option ID",cliDisplayDescription:"A valid option ID from the options.json config.",cliAutocompleteFunc:async () => { + const uncheckedRawData = opendiscord.configs.get("opendiscord:options").data + if (!Array.isArray(uncheckedRawData)) return null + const idList = uncheckedRawData.filter((option) => typeof option == "object" && typeof option["id"] == "string").map((option) => option.id) + return (idList.length > 0) ? idList : null + }})}, //EMBED & TEXT {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-text",{maxLength:4096,cliDisplayName:"Panel Text",cliDisplayDescription:"The raw text of the panel message. Leave empty to use the embed."})}, {key:"embed",optional:false,priority:0,checker:createPanelEmbedStructure("opendiscord:panel-embed")}, //SETTINGS - {key:"settings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:panel-settings",{children:[ - {key:"dropdownPlaceholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-placeholder",{maxLength:100,cliDisplayName:"Dropdown Placeholder",cliDisplayDescription:"Configure the text displayed in the dropdown when nothing is selected."})}, + {key:"settings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:panel-settings",{cliInitSkipKeys:["dropdownPlaceholder","describeOptionsCustomTitle"],children:[ + {key:"dropdownPlaceholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-placeholder",{maxLength:100,cliInitDefaultValue:"Create a ticket!",cliDisplayName:"Dropdown Placeholder",cliDisplayDescription:"Configure the text displayed in the dropdown when nothing is selected."})}, {key:"enableMaxTicketsWarningInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-text",{cliDisplayName:"Enable Max Tickets Warning (Text)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the text contents of the panel."})}, {key:"enableMaxTicketsWarningInEmbed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-embed",{cliDisplayName:"Enable Max Tickets Warning (Embed)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the embed of the panel."})}, @@ -472,82 +482,89 @@ export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendisco ],cliDisplayName:"Panel",cliDisplayDescription:"Manage, customise and configure a panel to your preference."}),cliDisplayName:"Panels",cliDisplayDescription:"A list of all panels in the bot. Here you can add, modify & remove existing panels or customise them to your preference."}) export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("opendiscord:questions",{allowedTypes:["object"],cliDisplayPropertyName:"question",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:questions",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-name",{minLength:3,maxLength:45})}, - {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-type",{choices:["short","paragraph"]})}, + {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this question. Used in ticket options."})}, + {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-name",{minLength:3,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this question."})}, + {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-type",{choices:["short","paragraph"],cliDisplayName:"Type",cliDisplayDescription:"The type of this question (short/paragraph)."})}, - {key:"required",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-required",{})}, - {key:"placeholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-placeholder",{maxLength:100})}, + {key:"required",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-required",{cliDisplayName:"Required",cliDisplayDescription:"Is this question required? If not, it can be left empty."})}, + {key:"placeholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-placeholder",{maxLength:100,cliDisplayName:"Placeholder",cliDisplayDescription:"The placeholder to show in the field when nothing has been written yet."})}, {key:"length",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:question-length",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:question-length",{children:[ - {key:"min",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:question-length-min",{min:0,max:1024,negativeAllowed:false,floatAllowed:false})}, - {key:"max",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:question-length-max",{min:1,max:1024,negativeAllowed:false,floatAllowed:false})}, - ]})})}, -]})}) + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-length-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable length validation for this question."})}, + {key:"min",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:question-length-min",{min:0,max:1024,negativeAllowed:false,floatAllowed:false,cliDisplayName:"Min Length",cliDisplayDescription:"The minimum amount of characters required."})}, + {key:"max",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:question-length-max",{min:1,max:1024,negativeAllowed:false,floatAllowed:false,cliInitDefaultValue:100,cliDisplayName:"Max Length",cliDisplayDescription:"The maximum amount of characters allowed."})}, + ],cliDisplayName:"Length Validation",cliDisplayDescription:"Add length validation to the question. This way, the contents must be at least/most ... characters."}),cliDisplayName:"Length Validation",cliDisplayDescription:"Add length validation to the question. This way, the contents must be at least/most ... characters."})}, +],cliDisplayName:"Question",cliDisplayDescription:"Manage, customise and configure a question to your preference."}),cliDisplayName:"Questions",cliDisplayDescription:"A list of all questions in the bot. Here you can add, modify & remove existing questions or customise them to your preference."}) export const defaultTranscriptsStructure = new api.ODCheckerObjectStructure("opendiscord:transcripts",{children:[ //GENERAL {key:"general",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-general",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-general",{children:[ - {key:"enableChannel",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-channel",{})}, - {key:"enableCreatorDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-creator-dm",{})}, - {key:"enableParticipantDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-participant-dm",{})}, - {key:"enableActiveAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-active-admin-dm",{})}, - {key:"enableEveryAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-every-admin-dm",{})}, + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the transcript system."})}, + + {key:"enableChannel",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-channel",{cliDisplayName:"Enable Channel",cliDisplayDescription:"Send the transcript to a specific channel in your server (configurable in 'channel' property)."})}, + {key:"enableCreatorDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-creator-dm",{cliDisplayName:"Enable Creator DM",cliDisplayDescription:"Send the transcript in DM to the creator of the ticket."})}, + {key:"enableParticipantDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-participant-dm",{cliDisplayName:"Enable Participant DM",cliDisplayDescription:"Send the transcript in DM to all non-admin participants of the ticket."})}, + {key:"enableActiveAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-active-admin-dm",{cliDisplayName:"Enable Active Admin DM",cliDisplayDescription:"Send the transcript in DM to all admins that actively wrote in the ticket."})}, + {key:"enableEveryAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-every-admin-dm",{cliDisplayName:"Enable Every Admin DM",cliDisplayDescription:"Send the transcript in DM to all admins assigned to the ticket."})}, - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:transcripts-channel","channel",true,[])}, - {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-mode",{choices:["html","text"]})}, - ]})})}, + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:transcripts-channel","channel",true,[],{cliDisplayName:"Channel",cliDisplayDescription:"The discord channel ID to send the transcript to."})}, + {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-mode",{choices:["html","text"],cliDisplayName:"Transcript Mode",cliDisplayDescription:"The transcript type to use: 'text' or 'html'."})}, + ],cliDisplayName:"General",cliDisplayDescription:"General settings for the transcripts."}),cliDisplayName:"General",cliDisplayDescription:"General settings for the transcripts."})}, //EMBED SETTINGS {key:"embedSettings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-embed-settings",{children:[ - {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-embed-color",false,true)}, - {key:"listAllParticipants",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-list-participants",{})}, - {key:"includeTicketStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-include-ticket-stats",{})}, - ]})}, + {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-embed-color",false,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Use a custom color in the embed. When empty, the default bot color will be used."})}, + {key:"listAllParticipants",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-list-participants",{cliDisplayName:"List Participants",cliDisplayDescription:"List all participants of the ticket in the embed."})}, + {key:"includeTicketStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-include-ticket-stats",{cliDisplayName:"Include Ticket Stats",cliDisplayDescription:"Include some stats from the ticket in the embed."})}, + ],cliDisplayName:"Embed Settings",cliDisplayDescription:"Settings and customisability related to the embed which contains the transcript."})}, //TEXT STYLE {key:"textTranscriptStyle",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-text",{children:[ - {key:"layout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-layout",{choices:["simple","normal","detailed"]})}, - {key:"includeStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-stats",{})}, - {key:"includeIds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-ids",{})}, - {key:"includeEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-embeds",{})}, - {key:"includeFiles",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-files",{})}, - {key:"includeBotMessages",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-bots",{})}, + {key:"layout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-layout",{choices:["simple","normal","detailed"],cliDisplayName:"Layout",cliDisplayDescription:"The layout to use in the text-transcripts (simple, normal, detailed)."})}, + {key:"includeStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-stats",{cliDisplayName:"Include Stats",cliDisplayDescription:"Include statistics in the transcript?"})}, + {key:"includeIds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-ids",{cliDisplayName:"Include Ids",cliDisplayDescription:"Include role, channel & user ID's in the transcript?"})}, + {key:"includeEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-embeds",{cliDisplayName:"Include Embeds",cliDisplayDescription:"Include message embeds in the transcript?"})}, + {key:"includeFiles",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-files",{cliDisplayName:"Include Files",cliDisplayDescription:"Include files & attachments in the transcript?"})}, + {key:"includeBotMessages",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-bots",{cliDisplayName:"Include Bots",cliDisplayDescription:"Include messages sent by bots/apps?"})}, - {key:"fileMode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-file-mode",{choices:["custom","channel-name","channel-id","user-name","user-id"]})}, - {key:"customFileName",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-file-name",{maxLength:512,regex:/^[^\.#%&{}\\<>*?/!'":@`|=]*$/})}, - ]})}, + {key:"fileMode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-file-mode",{choices:["custom","channel-name","channel-id","user-name","user-id"],cliDisplayName:"File Mode",cliDisplayDescription:"Select the mode the transcript will be named: custom, channel-name, user-name, user-id."})}, + {key:"customFileName",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-file-name",{maxLength:512,regex:/^[^\.#%&{}\\<>*?/!'":@`|=]*$/,cliDisplayName:"Custom File Name",cliDisplayDescription:"Use this as transcript name when the mode is set to 'custom'."})}, + ],cliDisplayName:"Text Transcript Style",cliDisplayDescription:"Configure the 'Text Transcripts' from Open Ticket."})}, //HTML STYLE {key:"htmlTranscriptStyle",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html",{children:[ //HTML BACKGROUND {key:"background",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-background",{property:"enableCustomBackground",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-background",{children:[ - {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-background-color",false,true)}, - {key:"backgroundImage",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-background-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, - ]})})}, + {key:"enableCustomBackground",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-background-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable background customisation in the HTML Transcripts."})}, + {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-background-color",false,true,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the background."})}, + {key:"backgroundImage",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-background-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Background Image",cliDisplayDescription:"A URL to an image to use in the background. This will overwrite the background color."})}, + ],cliDisplayName:"Background Style",cliDisplayDescription:"Customise the background of the HTML Transcripts."}),cliDisplayName:"Background Style",cliDisplayDescription:"Customise the background of the HTML Transcripts."})}, //HTML HEADER {key:"header",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-header",{property:"enableCustomHeader",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-header",{children:[ - {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-bgcolor",false,false)}, - {key:"decoColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-decocolor",false,false)}, - {key:"textColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-textcolor",false,false)}, - ]})})}, + {key:"enableCustomHeader",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-header-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable header customisation in the HTML Transcripts."})}, + {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-bgcolor",false,false,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the header background."})}, + {key:"decoColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-decocolor",false,false,{cliDisplayName:"Decoration Color",cliDisplayDescription:"The hex-color of the header decoration (e.g. horizontal line)."})}, + {key:"textColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-textcolor",false,false,{cliDisplayName:"Text Color",cliDisplayDescription:"The hex-color of the header text."})}, + ],cliDisplayName:"Header Style",cliDisplayDescription:"Customise the header of the HTML Transcripts."}),cliDisplayName:"Header Style",cliDisplayDescription:"Customise the header of the HTML Transcripts."})}, //HTML STATS {key:"stats",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-stats",{property:"enableCustomStats",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-stats",{children:[ - {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-bgcolor",false,false)}, - {key:"keyTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-keycolor",false,false)}, - {key:"valueTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-valuecolor",false,false)}, - {key:"hideBackgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidebgcolor",false,false)}, - {key:"hideTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidecolor",false,false)}, - ]})})}, + {key:"enableCustomStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-stats-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable stats customisation in the HTML Transcripts."})}, + {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-bgcolor",false,false,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the stats background."})}, + {key:"keyTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-keycolor",false,false,{cliDisplayName:"Key Text Color",cliDisplayDescription:"The hex-color of the stats key text."})}, + {key:"valueTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-valuecolor",false,false,{cliDisplayName:"Value Text Color",cliDisplayDescription:"The hex-color of the stats value text."})}, + {key:"hideBackgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidebgcolor",false,false,{cliDisplayName:"Hide Background Color",cliDisplayDescription:"The hex-color of the stats hide button background."})}, + {key:"hideTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidecolor",false,false,{cliDisplayName:"Hide Text Color",cliDisplayDescription:"The hex-color of the stats hide button text."})}, + ],cliDisplayName:"Stats Style",cliDisplayDescription:"Customise the stats of the HTML Transcripts."}),cliDisplayName:"Stats Style",cliDisplayDescription:"Customise the stats of the HTML Transcripts."})}, //HTML FAVICON {key:"favicon",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-favicon",{property:"enableCustomFavicon",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-favicon",{children:[ - {key:"imageUrl",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-favicon-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp"]})}, - ]})})}, - ]})}, -]}) + {key:"enableCustomFavicon",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-favicon-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable favicon customisation in the HTML Transcripts."})}, + {key:"imageUrl",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-favicon-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp"]},{cliDisplayName:"LOREMIPSUM",cliDisplayDescription:"IPSUMLOREM"})}, + ],cliDisplayName:"Favicon Style",cliDisplayDescription:"Customise the favicon of the HTML Transcripts."}),cliDisplayName:"Favicon Style",cliDisplayDescription:"Customise the favicon of the HTML Transcripts."})}, + ],cliDisplayName:"Html Transcript Style",cliDisplayDescription:"Configure the 'Html Transcripts' from Open Ticket."})}, +],cliDisplayName:"Transcripts",cliDisplayDescription:"All settings related to transcripts."}) export const defaultUnusedOptionsFunction = (manager:api.ODCheckerManager, functions:api.ODCheckerFunctionManager): api.ODCheckerResult => { const optionList: string[] = manager.storage.get("openticket","option-ids") From e73c8e9b3be0164ca1305cc135d5458ddc818e8f Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 16 Apr 2025 21:44:23 +0200 Subject: [PATCH 16/78] Started with CLI "Quick Setup" feature. --- src/core/cli/cli.ts | 75 +++++++++++ .../{startup/cli.ts => cli/editConfig.ts} | 56 +------- src/core/cli/quickSetup.ts | 121 ++++++++++++++++++ src/index.ts | 2 +- 4 files changed, 200 insertions(+), 54 deletions(-) create mode 100644 src/core/cli/cli.ts rename src/core/{startup/cli.ts => cli/editConfig.ts} (94%) create mode 100644 src/core/cli/quickSetup.ts diff --git a/src/core/cli/cli.ts b/src/core/cli/cli.ts new file mode 100644 index 0000000..6fcb2ce --- /dev/null +++ b/src/core/cli/cli.ts @@ -0,0 +1,75 @@ +import {opendiscord, api, utilities} from "../../index" +import {Terminal, terminal} from "terminal-kit" +import ansis from "ansis" + +const logo = [ + " ██████╗ ██████╗ ███████╗███╗ ██╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ ", + " ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ ", + " ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║██║ █████╔╝ █████╗ ██║ ", + " ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ", + " ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ", + " ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ " +] + +/**A utility function to center text to a certain width. */ +export function centerText(text:string,width:number){ + if (width < text.length) return text + let newWidth = width-ansis.strip(text).length+1 + let final = " ".repeat(newWidth/2)+text + return final +} + +/**A utility function to terminate the interactive CLI. */ +export async function terminate(){ + terminal.grabInput(false) + terminal.clear() + terminal.green("👋 Exited the Open Ticket Interactive Setup CLI.\n") + process.exit(0) +} +terminal.on("key",(name,matches,data) => { + if (name == "CTRL_C") terminate() +}) + +/**Render the header of the interactive CLI. */ +export function renderHeader(path:(string|number)[]|string){ + terminal.grabInput(true) + terminal.clear().moveTo(1,1) + terminal(ansis.hex("#f8ba00")(logo.join("\n")+"\n")) + terminal.bold(centerText("Interactive Setup CLI - Version: "+opendiscord.versions.get("opendiscord:version").toString()+" - Support: https://discord.dj-dj.be\n",88)) + if (typeof path == "string") terminal.cyan(centerText(path+"\n\n",88)) + else if (path.length < 1) terminal.cyan(centerText("👋 Hi! Welcome to the Open Ticket Interactive Setup CLI! 👋\n\n",88)) + else terminal.cyan(centerText("🌐 Current Location: "+path.map((v,i) => { + if (i == 0) return v.toString() + else if (typeof v == "string") return ".\""+v+"\"" + else if (typeof v == "number") return "."+v + }).join("")+"\n\n",88)) +} + +async function renderCliModeSelector(backFn:(() => api.ODPromiseVoid)){ + renderHeader([]) + terminal(ansis.bold.green("Please select what CLI module you want to use.\n")+ansis.italic.gray("(use arrow keys to navigate, exit using escape)\n")) + + const answer = await terminal.singleColumnMenu([ + "✏️ Edit Config "+ansis.gray("=> Edit the current config, add/remove new tickets/questions/panels & more!"), + "⏱️ Quick Setup "+ansis.gray("=> A quick and easy way of setting up the bot in your Discord server."), + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else if (answer.selectedIndex == 0) await (await import("./editConfig.js")).renderEditConfig(async () => {await renderCliModeSelector(backFn)}) + else if (answer.selectedIndex == 1) await (await import("./quickSetup.js")).renderQuickSetup(async () => {await renderCliModeSelector(backFn)}) +} + +export async function execute(){ + if (terminal.width < 100 || terminal.height < 35){ + terminal(ansis.red.bold("\n\nMake sure your console or cmd window has a "+ansis.cyan("minimum width & height")+" of "+ansis.cyan("100x35")+" characters.")) + terminal(ansis.red.bold("\nOtherwise the Open Ticket Interactive Setup CLI will be rendered incorrectly.")) + terminal(ansis.red.bold("\nThe current terminal dimensions are: "+ansis.cyan(terminal.width+"x"+terminal.height)+".")) + }else await renderCliModeSelector(terminate) +} \ No newline at end of file diff --git a/src/core/startup/cli.ts b/src/core/cli/editConfig.ts similarity index 94% rename from src/core/startup/cli.ts rename to src/core/cli/editConfig.ts index 6cc6331..9eebf29 100644 --- a/src/core/startup/cli.ts +++ b/src/core/cli/editConfig.ts @@ -1,50 +1,9 @@ import {opendiscord, api, utilities} from "../../index" import {Terminal, terminal} from "terminal-kit" import ansis from "ansis" +import {renderHeader} from "./cli" -const logo = [ - " ██████╗ ██████╗ ███████╗███╗ ██╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ ", - " ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ ", - " ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║██║ █████╔╝ █████╗ ██║ ", - " ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ", - " ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ", - " ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ " -] - -/**A utility function to center text to a certain width. */ -function centerText(text:string,width:number){ - if (width < text.length) return text - let newWidth = width-ansis.strip(text).length+1 - let final = " ".repeat(newWidth/2)+text - return final -} - -/**A utility function to terminate the interactive CLI. */ -async function terminate(){ - terminal.grabInput(false) - terminal.clear() - terminal.green("👋 Exited the Open Ticket Interactive Setup CLI.\n") - process.exit(0) -} -terminal.on("key",(name,matches,data) => { - if (name == "CTRL_C") terminate() -}) - -/**Render the header of the interactive CLI. */ -function renderHeader(path:(string|number)[]){ - terminal.grabInput(true) - terminal.clear().moveTo(1,1) - terminal(ansis.hex("#f8ba00")(logo.join("\n")+"\n")) - terminal.bold(centerText("Interactive Setup CLI - Version: "+opendiscord.versions.get("opendiscord:version").toString()+" - Support: https://discord.dj-dj.be\n",88)) - if (path.length < 1) terminal.cyan(centerText("👋 Hi! Welcome to the Open Ticket Interactive Setup CLI! 👋\n\n",88)) - else terminal.cyan(centerText("🌐 Current Location: "+path.map((v,i) => { - if (i == 0) return v.toString() - else if (typeof v == "string") return ".\""+v+"\"" - else if (typeof v == "number") return "."+v - }).join("")+"\n\n",88)) -} - -async function renderConfigSelector(backFn:(() => api.ODPromiseVoid)){ +export async function renderEditConfig(backFn:(() => api.ODPromiseVoid)){ renderHeader([]) terminal(ansis.bold.green("Please select which config you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) @@ -65,7 +24,7 @@ async function renderConfigSelector(backFn:(() => api.ODPromiseVoid)){ if (answer.canceled) return await backFn() const checker = checkerList[answer.selectedIndex] const configData = checker.config.data as api.ODValidJsonType - await chooseConfigStructure(checker,async () => {await renderConfigSelector(backFn)},checker.structure,configData,{},NaN,["("+checker.config.path+")"]) + await chooseConfigStructure(checker,async () => {await renderEditConfig(backFn)},checker.structure,configData,{},NaN,["("+checker.config.path+")"]) } async function chooseConfigStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStructure,data:api.ODValidJsonType,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ @@ -938,7 +897,6 @@ async function renderAdditionConfigArrayStructureSelector(checker:api.ODChecker, else if (answer.selectedIndex == 5) await renderConfigArrayStructureDuplicateSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path) } - async function renderAdditionConfigTypeSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerTypeSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){ renderHeader(path) terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) @@ -972,12 +930,4 @@ async function renderAdditionConfigTypeSwitchStructure(checker:api.ODChecker,bac else if (answer.selectedText.startsWith("Create as object") && structure.options.object) await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.object,parent,parentIndex,path,localPath) else if (answer.selectedText.startsWith("Create as array/list") && structure.options.array) await renderAdditionConfigArrayStructureSelector(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.array,parent,parentIndex,path,localPath) else if (answer.selectedText.startsWith("Create as null") && structure.options.null) await renderAdditionConfigNullStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.null,parent,parentIndex,path,localPath) -} - -export async function execute(){ - if (terminal.width < 100 || terminal.height < 35){ - terminal(ansis.red.bold("\n\nMake sure your console or cmd window has a "+ansis.cyan("minimum width & height")+" of "+ansis.cyan("100x35")+" characters.")) - terminal(ansis.red.bold("\nOtherwise the Open Ticket Interactive Setup CLI will be rendered incorrectly.")) - terminal(ansis.red.bold("\nThe current terminal dimensions are: "+ansis.cyan(terminal.width+"x"+terminal.height)+".")) - }else await renderConfigSelector(terminate) } \ No newline at end of file diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts new file mode 100644 index 0000000..c27e17f --- /dev/null +++ b/src/core/cli/quickSetup.ts @@ -0,0 +1,121 @@ +import {opendiscord, api, utilities} from "../../index" +import {Terminal, terminal} from "terminal-kit" +import ansis from "ansis" +import {renderHeader} from "./cli" + +export async function renderQuickSetup(backFn:() => api.ODPromiseVoid){ + if (quickSetupRequiresReset()) await renderQuickSetupWarning(backFn) + else await renderQuickSetupWelcome(backFn) +} + +function quickSetupRequiresReset(): boolean { + const generalConfig = opendiscord.configs.get("opendiscord:general") + if (generalConfig.data.token != "your bot token here! (or leave empty when using 'tokenFromENV')") return true + if (generalConfig.data.mainColor != "#f8ba00") return true + if (generalConfig.data.language != "english") return true + if (generalConfig.data.prefix != "!ticket ") return true + if (generalConfig.data.serverId != "discord server id") return true + + return false +} + +async function renderQuickSetupWarning(backFn:() => api.ODPromiseVoid) { + renderHeader("⏱️ Open Ticket Quick Setup: Warning") + + terminal.bold(ansis.yellow("WARNING! ")+ansis.red("By using the 'Quick Setup' feature, your current config will be completely resetted!")) + terminal.gray("\n\nAre you sure you want to continue?") + + const answer = await terminal.singleColumnMenu([ + ansis.green("✅ No, take me back."), + ansis.red("🚨 Yes, continue and reset the config.") + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled || answer.selectedIndex == 0) return await backFn() + if (answer.selectedIndex == 1) await renderQuickSetupWelcome(async () => {await renderQuickSetupWarning(backFn)}) +} + +async function renderQuickSetupWelcome(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Introduction") + + terminal.bold.underline.blue("Open Ticket: Quick Setup\n") + terminal.gray([ + "Hi there! Thank you for downloading and installing Open Ticket.", + "You have chosen to configure the bot using the 'Quick Setup CLI'.", + "", + "This program will help you with configuring Open Ticket using a step-by-step method.", + "If you've ever used Google Forms, then this will probably be very easy for you 😉.", + "", + ansis.magenta("The configuration should normally only take around 5 minutes."), + ansis.magenta("Once you've completed the form, the bot is technically ready for usage!") + ].join("\n")+"\n\n") + + const answer = await terminal.singleColumnMenu([ + ansis.green("Press 'Enter' to start!") + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else if (answer.selectedIndex == 0) await renderQuickSetupDevPortalInfo(async () => {await renderQuickSetupWelcome(backFn)}) +} + +async function renderQuickSetupDevPortalInfo(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Dev Portal & Experience") + + terminal.bold.blue("(Step 1) Have you already created a Discord bot to use for Open Ticket?\n") + + const answer = await terminal.singleColumnMenu([ + "✅ Yes I have, and it has been invited to the server.", + "❓ No not yet, I don't know how to create one.", + "👶 I've never seen a Discord bot before.", + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else if (answer.selectedIndex == 0) await backFn() //TODO NEXT QUESTION + else if (answer.selectedIndex == 1) await renderQuickSetupDevPortalGuide(0,async () => {await renderQuickSetupDevPortalInfo(backFn)}) + else if (answer.selectedIndex == 2) await renderQuickSetupDevPortalGuide(1,async () => {await renderQuickSetupDevPortalInfo(backFn)}) +} + +async function renderQuickSetupDevPortalGuide(variation:0|1,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Dev Portal & Experience") + + if (variation == 0){ + terminal.bold.blue("(Step 1.1) You've mentioned that you don't know how to create a Discord bot.\n\n") + terminal.gray("Please visit the following URL for a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n")) + }else{ + terminal.bold.blue("(Step 1.2) You've mentioned that you've never seen Discord bot before.\n\n") + terminal.gray("How did you even download Open Ticket 🤪? But still, we have a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n")) + } + + const answer = await terminal.singleColumnMenu([ + ansis.green("✅ Alright, I've made the bot. Take me back please!") + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + return await backFn() +} diff --git a/src/index.ts b/src/index.ts index 5bbb50c..c83ae71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -301,7 +301,7 @@ const main = async () => { //switch to CLI context instead of running the bot if (useCliFlag && useCliFlag.value){ - await (await (import("./core/startup/cli.js"))).execute() + await (await (import("./core/cli/cli.js"))).execute() await utilities.timer(1000) console.log("\n\n"+ansis.red("❌ Something went wrong in the Interactive Setup CLI. Please try again or report a bug in our discord server.")) process.exit(0) From e36c1992deacbe354b05523769e4083a61e94cb0 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:38:54 +0200 Subject: [PATCH 17/78] Added 2nd question in the CLI quick setup --- src/core/api/modules/client.ts | 7 ++-- src/core/cli/quickSetup.ts | 73 ++++++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index d2e2868..45dea53 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -135,8 +135,8 @@ export class ODClientManager { }) } } - /**Log-in with a discord auth token. */ - login(): Promise { + /**Log-in with a discord auth token. Rejects returns `false` using 'softErrors' on failure. */ + login(softErrors?:boolean): Promise { return new Promise(async (resolve,reject) => { if (!this.initiated) reject("Client isn't initiated yet!") if (!this.token) reject("Client doesn't have a token!") @@ -158,7 +158,8 @@ export class ODClientManager { this.#debug.debug("Finished discord.js client.login()") this.loggedIn = true }catch(err){ - if (err.message.toLowerCase().includes("used disallowed intents")){ + if (softErrors) return resolve(false) + else if (err.message.toLowerCase().includes("used disallowed intents")){ process.emit("uncaughtException",new ODSystemError("Used disallowed intents")) }else if (err.message.toLowerCase().includes("tokeninvalid") || err.message.toLowerCase().includes("an invalid token was provided")){ process.emit("uncaughtException",new ODSystemError("Invalid discord bot token provided")) diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index c27e17f..2e7d283 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -68,11 +68,11 @@ async function renderQuickSetupWelcome(backFn:() => api.ODPromiseVoid){ }).promise if (answer.canceled) return await backFn() - else if (answer.selectedIndex == 0) await renderQuickSetupDevPortalInfo(async () => {await renderQuickSetupWelcome(backFn)}) + else if (answer.selectedIndex == 0) await renderQuickSetupDevPortal(async () => {await renderQuickSetupWelcome(backFn)}) } -async function renderQuickSetupDevPortalInfo(backFn:() => api.ODPromiseVoid){ - renderHeader("⏱️ Open Ticket Quick Setup: Dev Portal & Experience") +async function renderQuickSetupDevPortal(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Discord Bot & Developer Portal") terminal.bold.blue("(Step 1) Have you already created a Discord bot to use for Open Ticket?\n") @@ -90,20 +90,20 @@ async function renderQuickSetupDevPortalInfo(backFn:() => api.ODPromiseVoid){ }).promise if (answer.canceled) return await backFn() - else if (answer.selectedIndex == 0) await backFn() //TODO NEXT QUESTION - else if (answer.selectedIndex == 1) await renderQuickSetupDevPortalGuide(0,async () => {await renderQuickSetupDevPortalInfo(backFn)}) - else if (answer.selectedIndex == 2) await renderQuickSetupDevPortalGuide(1,async () => {await renderQuickSetupDevPortalInfo(backFn)}) + else if (answer.selectedIndex == 0) await renderQuickSetupBotToken(async () => {await renderQuickSetupDevPortal(backFn)}) + else if (answer.selectedIndex == 1) await renderQuickSetupDevPortalGuide(0,async () => {await renderQuickSetupDevPortal(backFn)}) + else if (answer.selectedIndex == 2) await renderQuickSetupDevPortalGuide(1,async () => {await renderQuickSetupDevPortal(backFn)}) } async function renderQuickSetupDevPortalGuide(variation:0|1,backFn:() => api.ODPromiseVoid){ - renderHeader("⏱️ Open Ticket Quick Setup: Dev Portal & Experience") + renderHeader("⏱️ Open Ticket Quick Setup: Discord Bot & Developer Portal") if (variation == 0){ terminal.bold.blue("(Step 1.1) You've mentioned that you don't know how to create a Discord bot.\n\n") terminal.gray("Please visit the following URL for a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n")) }else{ terminal.bold.blue("(Step 1.2) You've mentioned that you've never seen Discord bot before.\n\n") - terminal.gray("How did you even download Open Ticket 🤪? But still, we have a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n")) + terminal.gray("How did you even download Open Ticket 🤪? But all jokes aside, we have a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n")) } const answer = await terminal.singleColumnMenu([ @@ -119,3 +119,60 @@ async function renderQuickSetupDevPortalGuide(variation:0|1,backFn:() => api.ODP return await backFn() } + +async function quickSetupLogin(token:string): Promise { + const client = new api.ODClientManager(opendiscord.debug) + client.token = token + client.intents.push("Guilds","GuildMessages","DirectMessages","GuildEmojisAndStickers","GuildMembers","MessageContent","GuildWebhooks","GuildInvites") + client.privileges.push("MessageContent","GuildMembers") + client.partials.push("Channel","Message") + client.permissions.push("AddReactions","AttachFiles","CreatePrivateThreads","CreatePublicThreads","EmbedLinks","ManageChannels","ManageGuild","ManageMessages","ChangeNickname","ManageRoles","ManageThreads","ManageWebhooks","MentionEveryone","ReadMessageHistory","SendMessages","SendMessagesInThreads","UseApplicationCommands","UseExternalEmojis","ViewAuditLog","ViewChannel") + client.initClient() + + //client login + return new Promise(async (resolve) => { + try{ + client.readyListener = async () => { + client.activity.setStatus("custom","Configuring Open Ticket...","idle",true) + resolve(client) + } + const success = await client.login(true) + if (!success) resolve(null) + }catch(err){ + resolve(null) + } + }) +} + +async function renderQuickSetupBotToken(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Bot Token") + + terminal.bold.blue("(Step 2) Please insert the token of your discord bot.\n") + terminal.gray("It will be safely stored in the 'config/general.json' file.\n\n> ") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true + }).promise + + if (typeof answer != "string") return await backFn() + else{ + const result = await quickSetupLogin(answer) + if (!result){ + //login failure + terminal.red.bold("\n\n❌ Something went wrong with logging into the bot.\n") + terminal.gray("Please try again and check your token for any mistakes.\nAlso make sure the permissions and priviliged gateaway intents are configured correctly in the developer panel.") + await utilities.timer(3000) + //retry + await renderQuickSetupBotToken(backFn) + }else{ + //login success + terminal.green.bold("\n\n✅ Succesfully logged into the bot.\n") + terminal.gray("Your bot should be online with the status 'Configuring Open Ticket...'.") + await utilities.timer(3000) + //continue + //TODO + } + } +} From 7696202dec6f8aa9f91a1f262c6c28010ba9075f Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sat, 19 Apr 2025 21:05:08 +0200 Subject: [PATCH 18/78] Updated Dutch Translations --- languages/dutch.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/languages/dutch.json b/languages/dutch.json index 9a1de8c..d2dd253 100644 --- a/languages/dutch.json +++ b/languages/dutch.json @@ -169,12 +169,12 @@ "rolesEmpty":"Geen enkele rollen zijn bijgewerkt!", "autocloseLeave":"Dit ticket is automatisch gesloten omdat de maker de server verlaten is!", - "autocloseTimeout":"Dit ticket is automatisch gesloten omdat het inactief was voor meer dan `{0} uur`!", + "autocloseTimeout":"Dit ticket is automatisch gesloten omdat het meer dan `{0} uur` inactief was!", "autodeleteLeave":"Dit ticket is automatisch verwijderd omdat de maker de server verlaten is!", - "autodeleteTimeout":"Dit ticket is automatisch verwijderd omdat het inactief was voor meer dan `{0} dagen`!", - "autocloseEnabled":"Autoclose is ingeschakeld in dit ticket!\nHet wordt gesloten wanneer het inactief is voor meer dan `{0} uur`!", + "autodeleteTimeout":"Dit ticket is automatisch verwijderd omdat het meer dan `{0} dagen` inactief was!", + "autocloseEnabled":"Autoclose is ingeschakeld in dit ticket!\nHet wordt gesloten na `{0} uur` inactiviteit!", "autocloseDisabled":"Autoclose is uitgeschakeld in dit ticket!\nHet wordt niet meer automatisch gesloten!", - "autodeleteEnabled":"Autodelete is ingeschakeld in dit ticket!\nHet wordt verwijderd wanneer het inactief is voor meer dan `{0} dagen`!", + "autodeleteEnabled":"Autodelete is ingeschakeld in dit ticket!\nHet wordt verwijderd na `{0} dagen` inactiviteit!", "autodeleteDisabled":"Autodelete is uitgeschakeld in dit ticket!\nHet wordt niet meer automatisch verwijderd!", "ticketMessageLimit":"Je kan maar {0} ticket(s) op het zelfde moment aanmaken!", From 2d1724893efeb889ed29a0a95098d12ad9abaa46 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 20 Apr 2025 11:14:19 +0200 Subject: [PATCH 19/78] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4b624f8..e3eec83 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@

-Open Ticket is the most advanced & customisable discord ticket bot that you will ever find! You can customise up to 300+ variables! This includes Html Transcripts, Advanced Plugins, Custom Embeds, Questions/Modals, Stats & more! -You're also able to customise every little aspect of the bot! From embeds to transcripts. Open Ticket is also translated in more than 27 Languages! If you need any help, feel free to join our discord server! +Open Ticket is the most advanced & customisable discord ticket bot available! You are able to customise up to 300+ settings and aspects! This includes Html Transcripts, Advanced Plugins, Custom Embeds, Questions/Modals, Stats & more! +The bot is translated in more than 27 Languages and has been battle tested in large Discord servers! If you need any help, feel free to join our discord server!

-

⭐️ Help us grow by giving a star! ⭐️

+

⭐️ Help us grow by giving a star! ⭐️

### 📌 Features - **🦇 pterodactyl support** - Open Ticket works perfect on Pterodactyl based panels! [(Download official eggs)](.eggs/README.md) @@ -68,10 +68,10 @@ A big thanks to all our sponsors! Without them, it wouldn't be possible to creat - + - +
Profile PictureProfile Picture
roppl3rguillee3
From fd739054fdd0a9bd74c1297786f260e2581a6155 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:08:52 +0200 Subject: [PATCH 20/78] Added 3th & 4th question to Quick Setup CLI --- src/core/api/modules/client.ts | 2 +- src/core/cli/quickSetup.ts | 37 +++++++++++++++++++++++++++++++++- src/index.ts | 2 +- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index 45dea53..7175de1 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -114,7 +114,7 @@ export class ODClientManager { this.#debug.debug("Created client with permissions: "+this.permissions.join(", ")) } /**Get all servers the bot is part of. */ - getGuilds(){ + async getGuilds(): Promise { if (!this.initiated) throw new ODSystemError("Client isn't initiated yet!") if (!this.ready) throw new ODSystemError("Client isn't ready yet!") diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 2e7d283..8c7c636 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -1,6 +1,7 @@ import {opendiscord, api, utilities} from "../../index" import {Terminal, terminal} from "terminal-kit" import ansis from "ansis" +import * as discord from "discord.js" import {renderHeader} from "./cli" export async function renderQuickSetup(backFn:() => api.ODPromiseVoid){ @@ -172,7 +173,41 @@ async function renderQuickSetupBotToken(backFn:() => api.ODPromiseVoid){ terminal.gray("Your bot should be online with the status 'Configuring Open Ticket...'.") await utilities.timer(3000) //continue - //TODO + await renderQuickSetupServer(result,async () => {await renderQuickSetupBotToken(backFn)}) } } } + +async function renderQuickSetupServer(client:api.ODClientManager,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Discord Server") + + terminal.bold.blue("(Step 3) Please select a Discord Server to use.\n") + terminal.gray("The bot will only work in this server.\n\n") + + const guilds = await client.getGuilds() + const nameList = guilds.map((g) => g.name) + const longestName = utilities.getLongestLength(nameList) + const guildList = guilds.map((g) => g.name.padEnd(longestName+5," ")+ansis.gray(" ("+g.id+")")) + + const answer = await terminal.singleColumnMenu([ansis.green("🔄 "),...guildList],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return backFn() + if (answer.selectedIndex == 0) return await renderQuickSetupServer(client,backFn) + const server = guilds[answer.selectedIndex-1] + await renderQuickSetupAdminRoles(client,server,[],async () => {await renderQuickSetupServer(client,backFn)}) +} + +async function renderQuickSetupAdminRoles(client:api.ODClientManager,guild:discord.Guild,selectedAdmins:string[],backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Admin Roles") + + terminal.bold.blue("(Step 4) Please select all 'Global Admins' roles to use.\n") + terminal.gray("Users with one of these roles will be able to access & interact with all tickets.\n\n") + +} diff --git a/src/index.ts b/src/index.ts index c83ae71..9c413d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -373,7 +373,7 @@ const main = async () => { const client = opendiscord.client //check if all servers are valid - const botServers = client.getGuilds() + const botServers = await client.getGuilds() const generalConfig = opendiscord.configs.get("opendiscord:general") const serverId = generalConfig.data.serverId ? generalConfig.data.serverId : "" if (!serverId) throw new api.ODSystemError("Server Id Missing!") From d404f343e0d082e52d34be39b02869b616384a3c Mon Sep 17 00:00:00 2001 From: Jasper Date: Mon, 28 Apr 2025 18:30:43 +0200 Subject: [PATCH 21/78] Sync Dev Branches * Fixed bug due to discord.js new components * Updated all versions to v4.0.3 * #155 : Fixed reply on creation when disabled. Fixed a bug that caused the bot to send a partial ephemeral message while "replyOnTicketCreation" was disabled. --- .eggs/README.md | 5 ++- .eggs/openticket-egg-v4.0.3.json | 62 +++++++++++++++++++++++++++ .github/CONTRIBUTING.md | 2 +- .github/SECURITY.md | 7 +-- README.md | 2 +- config/general.json | 2 +- config/panels.json | 2 +- index.js | 2 +- languages/arabic.json | 2 +- languages/catalan.json | 2 +- languages/custom.json | 2 +- languages/czech.json | 2 +- languages/danish.json | 2 +- languages/dutch.json | 2 +- languages/english.json | 2 +- languages/estonian.json | 2 +- languages/finnish.json | 2 +- languages/french.json | 2 +- languages/german.json | 2 +- languages/hindi.json | 2 +- languages/hungarian.json | 2 +- languages/indonesian.json | 2 +- languages/italian.json | 2 +- languages/latvian.json | 2 +- languages/lithuanian.json | 2 +- languages/norwegian.json | 2 +- languages/polish.json | 2 +- languages/portuguese.json | 2 +- languages/romanian.json | 2 +- languages/russian.json | 2 +- languages/spanish.json | 2 +- languages/swedish.json | 2 +- languages/thai.json | 2 +- languages/turkish.json | 2 +- languages/ukrainian.json | 2 +- languages/vietnamese.json | 2 +- package.json | 4 +- src/commands/ticket.ts | 9 ++-- src/core/api/main.ts | 2 +- src/core/api/modules/base.ts | 8 ++-- src/core/api/modules/responder.ts | 2 + src/core/api/openticket/transcript.ts | 1 + src/core/startup/migration.ts | 8 +++- src/index.ts | 2 +- 44 files changed, 127 insertions(+), 49 deletions(-) create mode 100644 .eggs/openticket-egg-v4.0.3.json diff --git a/.eggs/README.md b/.eggs/README.md index 5a8a58b..be20612 100644 --- a/.eggs/README.md +++ b/.eggs/README.md @@ -2,7 +2,7 @@ Open Ticket Logo [![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) -[![version](https://img.shields.io/badge/version-4.0.2-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.2) +[![version](https://img.shields.io/badge/version-4.0.3-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.3) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) [![Open Ticket supports Pterodactyl Eggs!](https://img.shields.io/badge/pterodactyl-supported-10539F?style=flat-square&logo=pterodactyl)](.eggs/README.md) @@ -20,6 +20,9 @@ It's recommended to provide at least `1GB` of **Memory/RAM** and `5GB` of **disk [**`openticket-egg-main.json` (Recommended)**](openticket-egg-main.json) - This egg will use the `main` branch of Open Ticket. +[**`openticket-egg-v4.0.3.json`**](openticket-egg-v4.0.3.json) +- This egg will always use Open Ticket `v4.0.3`. Open Ticket updates will not have an effect on this egg. + [**`openticket-egg-v4.0.2.json`**](openticket-egg-v4.0.2.json) - This egg will always use Open Ticket `v4.0.2`. Open Ticket updates will not have an effect on this egg. diff --git a/.eggs/openticket-egg-v4.0.3.json b/.eggs/openticket-egg-v4.0.3.json new file mode 100644 index 0000000..2535a49 --- /dev/null +++ b/.eggs/openticket-egg-v4.0.3.json @@ -0,0 +1,62 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2025-03-16T18:10:18+01:00", + "name": "Open Ticket (v4.0.3)", + "author": "support@dj-dj.be", + "description": "This is the official Pterodactyl egg for Open Ticket v4.0.3, the most advanced & customisable discord ticket bot that you will ever find! You can customise up to 300+ variables! This includes Html Transcripts, Advanced Plugins, Custom Embeds, Questions\/Modals, Stats & more!", + "features": null, + "docker_images": { + "ghcr.io\/parkervcp\/yolks:nodejs_20": "ghcr.io\/parkervcp\/yolks:nodejs_20" + }, + "file_denylist": [], + "startup": "if [[ ! -z ${NODE_PACKAGES} ]]; then npm install ${NODE_PACKAGES}; fi; if [[ ! -z ${UNNODE_PACKAGES} ]]; then npm uninstall ${UNNODE_PACKAGES}; fi; if [ -f \/home\/container\/package.json ]; then npm install; fi; node \"\/home\/container\/index.js\" ${NODE_FLAGS};", + "config": { + "files": "{}", + "startup": "{\r\n \"done\": \"STARTUP INFO:\"\r\n}", + "logs": "{}", + "stop": "^C" + }, + "scripts": { + "installation": { + "script": "#!\/bin\/bash\r\n# Open Ticket Installation Script (v4.0.3)\r\n# Inspired by: Node.js Installation Script\r\n# \u00a9 DJdj Development\r\n\r\necho -e \"[OT INSTALLER] installing dependencies. please wait...\"\r\napt update\r\napt install -y git curl\r\n\r\necho -e \"[OT INSTALLER] updating npm. please wait...\"\r\nnpm install npm@latest --location=global\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nBRANCH=\"v4.0.3\"\r\n\r\nif [ \"$(ls -A \/mnt\/server)\" ]; then\r\n echo -e \"[OT INSTALLER] \/mnt\/server directory is not empty.\"\r\n if [ -d .git ]; then\r\n echo -e \"[OT INSTALLER] .git directory exists\"\r\n if [ -f .git\/config ]; then\r\n echo -e \"[OT INSTALLER] loading info from git config\"\r\n ORIGIN=$(git config --get remote.origin.url)\r\n else\r\n echo -e \"[OT INSTALLER] files found with no git config\"\r\n echo -e \"[OT INSTALLER] closing out without touching things to not break anything\"\r\n exit 10\r\n fi\r\n fi\r\n\r\n if [ \"${ORIGIN}\" == \"https:\/\/github.com\/open-discord-bots\/open-ticket.git\" ]; then\r\n echo \"pulling latest from github\"\r\n git pull\r\n fi\r\nelse\r\n echo -e \"[OT INSTALLER] \/mnt\/server is empty.\"\r\n echo -e \"[OT INSTALLER] cloning files into repo.\"\r\n if [ -z ${BRANCH} ]; then\r\n echo -e \"[OT INSTALLER] cloning default branch\"\r\n git clone https:\/\/github.com\/open-discord-bots\/open-ticket.git .\r\n else\r\n echo -e \"[OT INSTALLER] cloning ${BRANCH}'\"\r\n git clone --single-branch --branch ${BRANCH} https:\/\/github.com\/open-discord-bots\/open-ticket.git .\r\n fi\r\nfi\r\n\r\necho -e \"[OT INSTALLER] installing Open Ticket node.js packages.\"\r\nif [ -f \/mnt\/server\/package.json ]; then\r\n \/usr\/local\/bin\/npm install --omit=dev\r\nfi\r\n\r\necho -e \"[OT INSTALLER] installing custom node.js packages.\"\r\nif [[ ! -z ${NODE_PACKAGES} ]]; then\r\n \/usr\/local\/bin\/npm install ${NODE_PACKAGES}\r\nfi\r\n\r\necho -e \"[OT INSTALLER] install complete!\"\r\nexit 0", + "container": "node:latest", + "entrypoint": "bash" + } + }, + "variables": [ + { + "name": "Additional Npm Packages", + "description": "Specify additional npm packages used by Open Ticket plugins. Use spaces to separate.", + "env_variable": "NODE_PACKAGES", + "default_value": "", + "user_viewable": false, + "user_editable": true, + "rules": "string|nullable", + "field_type": "text" + }, + { + "name": "Startup Flags", + "description": "Start the bot with additional Open Ticket flags. Separate using spaces.\r\nA full reference list can be found in the documentation.", + "env_variable": "NODE_FLAGS", + "default_value": "", + "user_viewable": false, + "user_editable": true, + "rules": "string|nullable", + "field_type": "text" + }, + { + "name": "Uninstall Npm Packages", + "description": "A list of npm packages to uninstall. Separate by spaces.", + "env_variable": "UNNODE_PACKAGES", + "default_value": "", + "user_viewable": false, + "user_editable": true, + "rules": "string|nullable", + "field_type": "text" + } + ] +} \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d0fc458..cece475 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing Guidelines Open Ticket Logo -[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.0.2-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.2) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) +[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.0.3-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.3) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) These are the Contributing Guidelines of Open Ticket!
Here you can find everything you need to know about contributing to Open Ticket.
diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 1392142..799664c 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,7 +1,7 @@ # Security Policy Open Ticket Logo -[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.0.1-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.1) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) +[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.0.3-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.3) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) This is the Security Policy of Open Ticket!
Here you can find a list of all supported & deprecated versions of the bot.
@@ -20,9 +20,10 @@ This list will be updated on every release. | Version | Supported | Notes | |------------|-----------|--------------------------------| | 4.1.0 | 🟦 | | -| 4.0.2 | ✅ | | +| 4.0.3 | ✅ | | +| 4.0.2 | ✅ | Major discord.js bug | | 4.0.1 | ✅ | | -| 4.0.0 | ✅ | | +| 4.0.0 | 🚧 | Supported until May 2025 | | 3.5.9 | 🚧 | Supported until May 2025 (LTS) | | 3.5.8 | 🟧 | Documentation Only | | < 3.5.8 | ❌ | | diff --git a/README.md b/README.md index e3eec83..475fb2f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
Powered By
Open Ticket
Discord Invite Link -Open Ticket Version +Open Ticket Version Open Ticket Documentation Open Ticket License Open Ticket Stars diff --git a/config/general.json b/config/general.json index 5b06c7f..177aa15 100644 --- a/config/general.json +++ b/config/general.json @@ -2,7 +2,7 @@ "_INFO":{ "support":"https://otdocs.dj-dj.be", "discord":"https://discord.dj-dj.be", - "version":"open-ticket-v4.0.2" + "version":"open-ticket-v4.0.3" }, "token":"your bot token here! (or leave empty when using 'tokenFromENV')", diff --git a/config/panels.json b/config/panels.json index 91a2bfa..8a1093e 100644 --- a/config/panels.json +++ b/config/panels.json @@ -17,7 +17,7 @@ "image":"https://www.example.com/image.png (or leave empty)", "thumbnail":"https://www.example.com/image.png (or leave empty)", - "footer":"Open Ticket v4.0.2 (or leave empty)", + "footer":"Open Ticket v4.0.3 (or leave empty)", "fields":[ {"name":"field name","value":"field value","inline":false} ], diff --git a/index.js b/index.js index 4e8487b..85f8db8 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ process.argv.push(...flags) ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ - v4.0.2 - Made by DJj123dj & Contributors + v4.0.3 - Made by DJj123dj & Contributors Discord: https://discord.dj-dj.be Docs: https://otdocs.dj-dj.be diff --git a/languages/arabic.json b/languages/arabic.json index 25c87ad..3329551 100644 --- a/languages/arabic.json +++ b/languages/arabic.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.2", + "otversion": "v4.0.3", "translators": ["palestinian"], "lastedited": "06/12/2024", "language": "Arabic", diff --git a/languages/catalan.json b/languages/catalan.json index 1142f9b..ca680c0 100644 --- a/languages/catalan.json +++ b/languages/catalan.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["guillee3"], "lastedited":"21/08/2024", "language":"Catalan", diff --git a/languages/custom.json b/languages/custom.json index 05e89fa..a144e18 100644 --- a/languages/custom.json +++ b/languages/custom.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["DJj123dj"], "lastedited":"21/08/2024", "language":"Custom", diff --git a/languages/czech.json b/languages/czech.json index b5ad617..4626527 100644 --- a/languages/czech.json +++ b/languages/czech.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["spyeye_"], "lastedited":"21/08/2024", "language":"Czech", diff --git a/languages/danish.json b/languages/danish.json index 8348216..f71795c 100644 --- a/languages/danish.json +++ b/languages/danish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["the_gamer"], "lastedited":"26/09/2024", "language":"Danish", diff --git a/languages/dutch.json b/languages/dutch.json index d2dd253..7f7eeae 100644 --- a/languages/dutch.json +++ b/languages/dutch.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["DJj123dj"], "lastedited":"21/08/2024", "language":"Dutch", diff --git a/languages/english.json b/languages/english.json index 7009379..b762448 100644 --- a/languages/english.json +++ b/languages/english.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["DJj123dj"], "lastedited":"21/08/2024", "language":"English", diff --git a/languages/estonian.json b/languages/estonian.json index 0f15c49..49eb6e3 100644 --- a/languages/estonian.json +++ b/languages/estonian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["iamnotmega","ChatGPT"], "lastedited":"21/10/2024", "language":"Estonian", diff --git a/languages/finnish.json b/languages/finnish.json index 78b178f..6ecbf84 100644 --- a/languages/finnish.json +++ b/languages/finnish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["iamnotmega","ChatGPT"], "lastedited":"21/10/2024", "language":"Finnish", diff --git a/languages/french.json b/languages/french.json index 7d8c870..c5998ab 100644 --- a/languages/french.json +++ b/languages/french.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["guillee3"], "lastedited":"04/12/2024", "language":"French", diff --git a/languages/german.json b/languages/german.json index 04726f8..c63cdb9 100644 --- a/languages/german.json +++ b/languages/german.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["benzorich"], "lastedited":"06/10/2024", "language":"German", diff --git a/languages/hindi.json b/languages/hindi.json index 29d109a..1312f62 100644 --- a/languages/hindi.json +++ b/languages/hindi.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["an_developer"], "lastedited":"14/12/2024", "language":"Hindi", diff --git a/languages/hungarian.json b/languages/hungarian.json index d8cd1a0..e765d86 100644 --- a/languages/hungarian.json +++ b/languages/hungarian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["Kornel0706"], "lastedited":"22/08/2024", "language":"Hungarian", diff --git a/languages/indonesian.json b/languages/indonesian.json index 4cd0c9f..8ae3703 100644 --- a/languages/indonesian.json +++ b/languages/indonesian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["erxg"], "lastedited":"25/08/2024", "language":"Indonesian", diff --git a/languages/italian.json b/languages/italian.json index 7b978d5..4a39112 100644 --- a/languages/italian.json +++ b/languages/italian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["fraden1mvp."], "lastedited":"08/10/2024", "language":"Italian", diff --git a/languages/latvian.json b/languages/latvian.json index 7cbb23d..f30a92e 100644 --- a/languages/latvian.json +++ b/languages/latvian.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.2", + "otversion": "v4.0.3", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Latvian", diff --git a/languages/lithuanian.json b/languages/lithuanian.json index 9e21286..b1e8971 100644 --- a/languages/lithuanian.json +++ b/languages/lithuanian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["TsgIndrius"], "lastedited":"26/01/2025", "language":"Lithuanian", diff --git a/languages/norwegian.json b/languages/norwegian.json index d759c5c..f6da1f1 100644 --- a/languages/norwegian.json +++ b/languages/norwegian.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.2", + "otversion": "v4.0.3", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Norwegian", diff --git a/languages/polish.json b/languages/polish.json index 1963e3d..4864649 100644 --- a/languages/polish.json +++ b/languages/polish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["DanoGlez"], "lastedited":"28/01/2025", "language":"Polish", diff --git a/languages/portuguese.json b/languages/portuguese.json index 4b7d412..6dc7a77 100644 --- a/languages/portuguese.json +++ b/languages/portuguese.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["quiradon"], "lastedited":"20/08/2024", "language":"Portuguese", diff --git a/languages/romanian.json b/languages/romanian.json index bf467df..0181447 100644 --- a/languages/romanian.json +++ b/languages/romanian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["SankeDev"], "lastedited":"27/08/2024", "language":"Romanian", diff --git a/languages/russian.json b/languages/russian.json index 80b4bf3..1a43cee 100644 --- a/languages/russian.json +++ b/languages/russian.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.2", + "otversion": "v4.0.3", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Russian", diff --git a/languages/spanish.json b/languages/spanish.json index c36113a..3f27592 100644 --- a/languages/spanish.json +++ b/languages/spanish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["Redactado","Josuens"], "lastedited":"22/08/2024", "language":"Spanish", diff --git a/languages/swedish.json b/languages/swedish.json index c4067b7..ea79370 100644 --- a/languages/swedish.json +++ b/languages/swedish.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.2", + "otversion": "v4.0.3", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Svenska", diff --git a/languages/thai.json b/languages/thai.json index ebc52e7..a6ec3c9 100644 --- a/languages/thai.json +++ b/languages/thai.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["modshd"], "lastedited":"13/11/2024", "language":"Thai", diff --git a/languages/turkish.json b/languages/turkish.json index 858baf2..de2ba80 100644 --- a/languages/turkish.json +++ b/languages/turkish.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.2", + "otversion": "v4.0.3", "translators": ["palestinian"], "lastedited": "26/11/2024", "language": "Turkish", diff --git a/languages/ukrainian.json b/languages/ukrainian.json index 5262625..e9f88cb 100644 --- a/languages/ukrainian.json +++ b/languages/ukrainian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["Anderskiy"], "lastedited":"31/08/2024", "language":"Ukrainian", diff --git a/languages/vietnamese.json b/languages/vietnamese.json index 8c14c44..d64339c 100644 --- a/languages/vietnamese.json +++ b/languages/vietnamese.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.2", + "otversion":"v4.0.3", "translators":["ngocdiep2006"], "lastedited":"07/04/2025", "language":"Vietnamese", diff --git a/package.json b/package.json index 2844151..30b99c6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "open-ticket", "author": "DJdj Development", - "version": "4.0.2", + "version": "4.0.3", "description": "The most advanced open-source discord ticket bot with HTML transcripts, plugins, questions, claiming, pinning & more! Using discord.js v14 & JSON database! ", "keywords": [ "ticket-bot", @@ -28,7 +28,7 @@ "@types/node": "^22.5.0", "@types/terminal-kit": "^2.5.7", "ansis": "^2.3.0", - "discord.js": "^14.17.2", + "discord.js": "^14.19.1", "formatted-json-stringify": "^1.2.1", "terminal-kit": "^3.1.2", "typescript": "^5.5.4" diff --git a/src/commands/ticket.ts b/src/commands/ticket.ts index faab16c..40e2cc6 100644 --- a/src/commands/ticket.ts +++ b/src/commands/ticket.ts @@ -142,7 +142,8 @@ export const registerButtonResponders = async () => { instance.modal(await opendiscord.builders.modals.getSafe("opendiscord:ticket-questions").build("panel-button",{guild,channel,user,option})) }else{ //create ticket - await instance.defer("reply",true) + await instance.defer((generalConfig.data.system.replyOnTicketCreation) ? "reply" : "update",true) + const res = await opendiscord.actions.get("opendiscord:create-ticket").run("panel-button",{guild,user,answers:[],option}) if (!res.channel || !res.ticket){ //error @@ -196,7 +197,8 @@ export const registerDropdownResponders = async () => { instance.modal(await opendiscord.builders.modals.getSafe("opendiscord:ticket-questions").build("panel-dropdown",{guild,channel,user,option})) }else{ //create ticket - await instance.defer("reply",true) + await instance.defer((generalConfig.data.system.replyOnTicketCreation) ? "reply" : "update",true) + const res = await opendiscord.actions.get("opendiscord:create-ticket").run("panel-dropdown",{guild,user,answers:[],option}) if (!res.channel || !res.ticket){ //error @@ -264,7 +266,8 @@ export const registerModalResponders = async () => { }) //create ticket - await instance.defer("reply",true) + await instance.defer((generalConfig.data.system.replyOnTicketCreation) ? "reply" : "update",true) + const res = await opendiscord.actions.get("opendiscord:create-ticket").run(originalSource,{guild,user,answers,option}) if (!res.channel || !res.ticket){ //error diff --git a/src/core/api/main.ts b/src/core/api/main.ts index 6ba49b3..bcbc801 100644 --- a/src/core/api/main.ts +++ b/src/core/api/main.ts @@ -130,7 +130,7 @@ export class ODMain { constructor(){ this.versions = new ODVersionManager_Default() - this.versions.add(ODVersion.fromString("opendiscord:version","v4.0.2")) + this.versions.add(ODVersion.fromString("opendiscord:version","v4.0.3")) this.versions.add(ODVersion.fromString("opendiscord:api","v1.0.0")) this.versions.add(ODVersion.fromString("opendiscord:transcripts","v2.0.0")) this.versions.add(ODVersion.fromString("opendiscord:livestatus","v2.0.0")) diff --git a/src/core/api/modules/base.ts b/src/core/api/modules/base.ts index 4567b74..1c5d2db 100644 --- a/src/core/api/modules/base.ts +++ b/src/core/api/modules/base.ts @@ -648,8 +648,8 @@ export class ODHTTPGetRequest { this.throwOnError = throwOnError const newConfig = config ?? {} newConfig.method = "GET" - if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.0.2"}) - else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.0.2"} + if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.0.3"}) + else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.0.3"} this.config = newConfig } @@ -703,8 +703,8 @@ export class ODHTTPPostRequest { this.throwOnError = throwOnError const newConfig = config ?? {} newConfig.method = "POST" - if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.0.2"}) - else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.0.2"} + if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.0.3"}) + else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.0.3"} this.config = newConfig } diff --git a/src/core/api/modules/responder.ts b/src/core/api/modules/responder.ts index d09e13d..a732d2a 100644 --- a/src/core/api/modules/responder.ts +++ b/src/core/api/modules/responder.ts @@ -611,6 +611,7 @@ export class ODButtonResponderInstance { getMessageComponent(type:"button"|"string-dropdown"|"user-dropdown"|"channel-dropdown"|"role-dropdown"|"mentionable-dropdown", id:string|RegExp): discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null { let result: discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null = null this.message.components.forEach((row) => { + if (row.type != discord.ComponentType.ActionRow) return row.components.forEach((component) => { if (type == "button" && component.type == discord.ComponentType.Button && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component else if (type == "string-dropdown" && component.type == discord.ComponentType.StringSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component @@ -898,6 +899,7 @@ export class ODDropdownResponderInstance { getMessageComponent(type:"button"|"string-dropdown"|"user-dropdown"|"channel-dropdown"|"role-dropdown"|"mentionable-dropdown", id:string|RegExp): discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null { let result: discord.ButtonComponent|discord.StringSelectMenuComponent|discord.RoleSelectMenuComponent|discord.ChannelSelectMenuComponent|discord.MentionableSelectMenuComponent|discord.UserSelectMenuComponent|null = null this.message.components.forEach((row) => { + if (row.type != discord.ComponentType.ActionRow) return row.components.forEach((component) => { if (type == "button" && component.type == discord.ComponentType.Button && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component else if (type == "string-dropdown" && component.type == discord.ComponentType.StringSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component diff --git a/src/core/api/openticket/transcript.ts b/src/core/api/openticket/transcript.ts index 49e5ca9..9e86382 100644 --- a/src/core/api/openticket/transcript.ts +++ b/src/core/api/openticket/transcript.ts @@ -257,6 +257,7 @@ export class ODTranscriptCollector { const rows: ODTranscriptComponentRowData[] = [] msg.components.forEach((row) => { const components: ODTranscriptComponentRowData["components"] = [] + if (row.type != discord.ComponentType.ActionRow) return row.components.forEach((component) => { if (component.type == discord.ComponentType.Button){ components.push({ diff --git a/src/core/startup/migration.ts b/src/core/startup/migration.ts index 45db853..63354fb 100644 --- a/src/core/startup/migration.ts +++ b/src/core/startup/migration.ts @@ -13,5 +13,11 @@ export const migrations = [ for (const panel of (await globalDatabase.getCategory("opendiscord:panel-update") ?? [])){ globalDatabase.set("opendiscord:panel-message",panel.key,panel.value) } - }) + }), + + //MIGRATE TO v4.0.2 + new utilities.ODVersionMigration(api.ODVersion.fromString("opendiscord:version","v4.0.2"),async () => {},async () => {}), + + //MIGRATE TO v4.0.3 + new utilities.ODVersionMigration(api.ODVersion.fromString("opendiscord:version","v4.0.3"),async () => {},async () => {}) ] \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9c413d8..64d1f17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ INFORMATION: ============ - Open Ticket v4.0.2 - © DJdj Development + Open Ticket v4.0.3 - © DJdj Development support us: https://github.com/sponsors/DJj123dj discord: https://discord.dj-dj.be From a0e03be103860c7400b47ed83ff48014a3ddf429 Mon Sep 17 00:00:00 2001 From: JasperAtSchool Date: Tue, 29 Apr 2025 09:47:29 +0200 Subject: [PATCH 22/78] Added ordinal numbers to config checker. --- src/core/api/modules/checker.ts | 15 +++++++++++++-- src/core/startup/init.ts | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index 26af713..6588475 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -332,6 +332,17 @@ export class ODChecker extends ODManagerData { this.options = options ?? {} } + /**Get a human-readable number string. */ + #ordinalNumber(num:number){ + const i = Math.abs(Math.round(num)) + const cent = i % 100 + if (cent >= 10 && cent <= 20) return i+'th' + const dec = i % 10 + if (dec === 1) return i+'st' + if (dec === 2) return i+'nd' + if (dec === 3) return i+'rd' + return i+'th' + } /**Run this checker. Returns all errors*/ check(): ODCheckerResult { this.messages = [] @@ -343,12 +354,12 @@ export class ODChecker extends ODManagerData { messages:this.messages } } - /**Create a string from the location trace (path)*/ + /**Create a string from the location trace/path in a human readable format. */ locationTraceToString(trace:ODCheckerLocationTrace){ const final: ODCheckerLocationTrace = [] trace.forEach((t) => { if (typeof t == "number"){ - final.push(`:${t}`) + final.push(`:(${this.#ordinalNumber(t+1)})`) }else{ final.push(`."${t}"`) } diff --git a/src/core/startup/init.ts b/src/core/startup/init.ts index 66279de..a243ebe 100644 --- a/src/core/startup/init.ts +++ b/src/core/startup/init.ts @@ -117,7 +117,11 @@ export interface ODUtilities { * * It shouldn't be used by plugins because this is an internal API feature! */ - ODVersionMigration:new (version:api.ODVersion,func:() => void|Promise,afterInitFunc:() => void|Promise) => ODVersionMigration + ODVersionMigration:new (version:api.ODVersion,func:() => void|Promise,afterInitFunc:() => void|Promise) => ODVersionMigration, + /**## ordinalNumber `utility function` + * Get a human readable ordinal number (e.g. 1st, 2nd, 3rd, 4th, ...) from a Javascript number. + */ + ordinalNumber(num:number): string, } /**## ODVersionMigration `utility class` @@ -252,5 +256,15 @@ export const utilities: ODUtilities = { "LOREMIPSUM", //TODO ] }, - ODVersionMigration + ODVersionMigration, + ordinalNumber(num:number){ + const i = Math.abs(Math.round(num)) + const cent = i % 100 + if (cent >= 10 && cent <= 20) return i+'th' + const dec = i % 10 + if (dec === 1) return i+'st' + if (dec === 2) return i+'nd' + if (dec === 3) return i+'rd' + return i+'th' + } } \ No newline at end of file From 35fdafb21d7a07be94f679ce5bcaeaa7e2be692c Mon Sep 17 00:00:00 2001 From: JasperAtSchool Date: Tue, 29 Apr 2025 09:54:48 +0200 Subject: [PATCH 23/78] Removed deprecated API classes + properties --- src/core/api/modules/base.ts | 100 ++------------------------------- src/core/api/modules/client.ts | 2 - 2 files changed, 6 insertions(+), 96 deletions(-) diff --git a/src/core/api/modules/base.ts b/src/core/api/modules/base.ts index 1c5d2db..96633ff 100644 --- a/src/core/api/modules/base.ts +++ b/src/core/api/modules/base.ts @@ -63,16 +63,6 @@ export class ODId { get value(){ return this.#value } - /**The source of the id (text before `:`). (e.g. `openticket` for all built-in ids) - * - * @deprecated Replaced with `getNamespace()` and will be removed in `v4.1.0`. - */ - source: string - /**The identifier of the id (text after `:`). - * - * @deprecated Replaced with `getIdentifier()` and will be removed in `v4.1.0`. - */ - identifier: string /**The change listener for the parent `ODManager` of this `ODId`. */ #change: ((oldId:string,newId:string) => void)|null = null @@ -92,21 +82,9 @@ export class ODId { if (result.length > 0) this.#value = result.join("") else throw new ODSystemError("invalid ID at 'new ODID(id: "+id+")'") - - const splitted = this.#value.split(":") - if (splitted.length > 1){ - this.source = splitted[0] - splitted.shift() - this.identifier = splitted.join(":") - }else{ - this.identifier = splitted.join(":") - this.source = "" - } }else{ //id is ODId this.#value = id.#value - this.source = id.source - this.identifier = id.identifier } } @@ -171,47 +149,6 @@ export class ODManagerChangeHelper { } } -/**## ODManagerRedirectHelper `class` - * @deprecated ### Will be removed in Open Ticket `v4.1.0`! - * - * This is Open Ticket ticket manager redirect helper. - * - * It is used to redirect a source to another source when the id isn't found. - * - * It will be used in **Open Discord** to allow plugins from all projects to work seamlessly! - * ## **(❌ SYSTEM ONLY!!)** - */ -export class ODManagerRedirectHelper { - #data: {fromSource:string,toSource:string}[] = [] - - /****(❌ SYSTEM ONLY!!)** Add a redirect to this manager. Returns `true` when overwritten. */ - add(fromSource:string, toSource:string){ - const index = this.#data.findIndex((data) => data.fromSource === fromSource) - if (index > -1){ - //already exists - this.#data[index] = {fromSource,toSource} - return true - }else{ - //doesn't exist - this.#data.push({fromSource,toSource}) - return false - } - } - /****(❌ SYSTEM ONLY!!)** Remove a redirect from this manager. Returns `true` when it existed. */ - remove(fromSource:string, toSource:string){ - const index = this.#data.findIndex((data) => data.fromSource === fromSource && data.toSource == toSource) - if (index > -1){ - //already exists - this.#data.splice(index,1) - return true - }else return false - } - /**List all redirects from this manager. */ - list(){ - return [...this.#data] - } -} - /**## ODManagerData `class` * This is Open Ticket manager data. * @@ -261,11 +198,9 @@ export class ODManager extends ODManagerChangeHe #changeListeners: ODManagerCallback[] = [] /**An array storing all listeners when data is removed. */ #removeListeners: ODManagerCallback[] = [] - /**Handle all redirects in this `ODManager` */ - redirects: ODManagerRedirectHelper = new ODManagerRedirectHelper() - + constructor(debug?:ODDebugger, debugname?:string){ - super() + super() this.#debug = debug this.#debugname = debugname } @@ -333,15 +268,7 @@ export class ODManager extends ODManagerChangeHe const newId = new ODId(id) const data = this.#data.get(newId.value) if (data) return data - else{ - //DEPRECATED!!! - const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.getNamespace()) - if (!redirect) return null - else{ - const redirectId = new ODId(redirect.toSource+":"+newId.getIdentifier()) - return this.get(redirectId) - } - } + else return null } /**Remove data that matches the `ODId`. Returns the removed data. */ remove(id:ODValidId): DataType|null { @@ -349,15 +276,8 @@ export class ODManager extends ODManagerChangeHe const data = this.#data.get(newId.value) if (!data){ - //DEPRECATED!!! - const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.getNamespace()) - if (!redirect){ - if (this.#debug) this.#debug.debug("Removed "+this.#debugname+" from manager",[{key:"id",value:newId.value},{key:"found",value:"false"}]) - return null - }else{ - const redirectId = new ODId(redirect.toSource+":"+newId.getIdentifier()) - return this.remove(redirectId) - } + if (this.#debug) this.#debug.debug("Removed "+this.#debugname+" from manager",[{key:"id",value:newId.value},{key:"found",value:"false"}]) + return null }else{ this.#data.delete(newId.value) if (this.#debug) this.#debug.debug("Removed "+this.#debugname+" from manager",[{key:"id",value:newId.value},{key:"found",value:"true"}]) @@ -385,15 +305,7 @@ export class ODManager extends ODManagerChangeHe exists(id:ODValidId): boolean { const newId = new ODId(id) if (this.#data.has(newId.value)) return true - else{ - //DEPRECATED!!! - const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.getNamespace()) - if (!redirect) return false - else{ - const redirectId = new ODId(redirect.toSource+":"+newId.getIdentifier()) - return this.exists(redirectId) - } - } + else return false } /**Get all data inside this manager*/ getAll(): DataType[] { diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index 7175de1..94e8667 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -516,8 +516,6 @@ export interface ODSlashCommandUniversalCommand { * The builder for slash commands. Here you can add options to the command. */ export interface ODSlashCommandBuilder extends discord.ChatInputApplicationCommandData { - /**@deprecated `dmPermission` is deprecated. Use `context` instead! (Not using contexts might result in the slash command being re-registered on every startup!) */ - dmPermission?:boolean /**This field is required in Open Ticket for future compatibility. */ integrationTypes:discord.ApplicationIntegrationType[], /**This field is required in Open Ticket for future compatibility. */ From 83080a6e4dd85593c1a387a92d1970b40ccb387a Mon Sep 17 00:00:00 2001 From: JasperAtSchool Date: Tue, 29 Apr 2025 09:58:36 +0200 Subject: [PATCH 24/78] Removed openticket -> opendiscord migration utils --- src/core/api/modules/database.ts | 34 ++------------------------------ 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/core/api/modules/database.ts b/src/core/api/modules/database.ts index 1e1dbad..ab6d601 100644 --- a/src/core/api/modules/database.ts +++ b/src/core/api/modules/database.ts @@ -7,30 +7,6 @@ import nodepath from "path" import { ODDebugger } from "./console" import * as fjs from "formatted-json-stringify" -///////////////////////////////////////////////////////// -//TEMPORARY OPENTICKET => OPENDISCORD MIGRATION UTILITIES -///////////////////////////////////////////////////////// - -/** ## ❌ Temporary function. Will be removed on full OTv4 release! */ -export function TEMP_migrateDatabaseIdPrefix(id:string): string { - if (!id.startsWith("openticket:")) return id - return id.replaceAll("openticket:","opendiscord:") -} -/** ## ❌ Temporary function. Will be removed on full OTv4 release! */ -export function TEMP_migrateDatabaseValuePrefix(value:string): string { - return value.replaceAll('"openticket:','"opendiscord:') -} -/** ## ❌ Temporary function. Will be removed on full OTv4 release! */ -export function TEMP_migrateDatabaseStructurePrefix(structure:ODJsonDatabaseStructure): ODJsonDatabaseStructure { - return structure.map((data) => { - return { - category:TEMP_migrateDatabaseIdPrefix(data.category), - key:TEMP_migrateDatabaseIdPrefix(data.key), - value:JSON.parse(TEMP_migrateDatabaseValuePrefix(JSON.stringify(data.value))) - } - }) -} - /**## ODDatabaseManager `class` * This is an Open Ticket database manager. * @@ -118,10 +94,7 @@ export class ODJsonDatabase extends ODDatabase { /**Init the database. */ init(): ODPromiseVoid { - //this.#system.getData() - //TEMPORARY!!! - const newData = TEMP_migrateDatabaseStructurePrefix(this.#system.getData()) - this.#system.setData(newData) + this.#system.getData() } /**Set/overwrite the value of `category` & `key`. Returns `true` when overwritten! * @example @@ -226,10 +199,7 @@ export class ODFormattedJsonDatabase extends ODDatabase { /**Init the database. */ init(): ODPromiseVoid { - //this.#system.getData() - //TEMPORARY!!! - const newData = TEMP_migrateDatabaseStructurePrefix(this.#system.getData()) - this.#system.setData(newData) + this.#system.getData() } /**Set/overwrite the value of `category` & `key`. Returns `true` when overwritten! * @example From 05919bb57ec28dbf3aa1f62e7dd6932f4d615929 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:51:39 +0200 Subject: [PATCH 25/78] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 475fb2f..dd65faa 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ The bot is translated in more than 27 Languages and has been battle > ### 📦 Resources > Not all resources are accurate yet! We are working on this.
-> Open Ticket Tutorial -> Open Ticket Docs +> Open Ticket Tutorial +> Open Ticket Docs > Open Ticket Plugins ### ❤️ Sponsors From b5dd7158dfd8d16838f1eb68d675dd66b91a5a9e Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:56:54 +0200 Subject: [PATCH 26/78] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 475fb2f..c8d7009 100644 --- a/README.md +++ b/README.md @@ -108,14 +108,14 @@ A list of people that contributed or provided the most support for Open Ticket. Profile Picture Profile Picture Profile Picture -Profile Picture +Profile Picture 💻 DJj123dj 💬 smetsliam 💬 Frank Vissers 💬 Sanke -🧩 Roppl3r +🧩 Guillee3 From 951aff093afebba7fc99f6a82b06859c91be8e5d Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 1 May 2025 22:01:04 +0200 Subject: [PATCH 27/78] Added 4th & 5th questions to quick setup --- src/core/cli/quickSetup.ts | 131 +++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 5 deletions(-) diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 8c7c636..e742c17 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -4,6 +4,60 @@ import ansis from "ansis" import * as discord from "discord.js" import {renderHeader} from "./cli" +interface ODQuickSetupVariables { + client?:api.ODClientManager, + guild?:discord.Guild, + globalAdmins?:string[], + mainColor?:discord.ColorResolvable +} +const quickSetupStorage: ODQuickSetupVariables = {} +const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { + style:terminal.white, + selectedStyle:terminal.bgBlue.white +} +const presetColors = new Map([ + ["dark red",discord.Colors.DarkRed], + ["red",0xff0000], + ["light red",0xf06c6c], + ["dark orange",0xed510e], + ["orange",0xed6f0e], + ["light orange",0xf0b06c], + ["openticket",0xf8ba00], + ["dark yellow",0xdeb100], + ["yellow",0xffff00], + ["light yellow",0xffff8c], + ["banana",0xffe896], + ["lime",0xa8e312], + ["dark green",0x009600], + ["green",0x00ff00], + ["light green",0x76f266], + ["dark cyan",0x00abab], + ["cyan",0x00ffff], + ["light cyan",0x63ffff], + ["aquamarine",0x7fffd4], + ["dark skyblue",0x006bc9], + ["skyblue",0x0095ff], + ["light skyblue",0x40bfff], + ["dark blue",0x00006e], + ["blue",0x0000ff], + ["light blue",0x5353fc], + ["blurple",0x5865F2], + ["dark purple",0x3f009e], + ["purple",0x8000ff], + ["light purple",0x9257eb], + ["dark pink",0xb82ab0], + ["pink",0xff6bf8], + ["light pink",0xff9cfa], + ["magenta",0xff00ff], + ["black",0x000000], + ["brown",0x806050], + ["dark gray",0x4f4f4f], + ["gray",0x808080], + ["light gray",0xb3b3b3], + ["white",0xffffff], + ["invisible",0x393A41] +]) + export async function renderQuickSetup(backFn:() => api.ODPromiseVoid){ if (quickSetupRequiresReset()) await renderQuickSetupWarning(backFn) else await renderQuickSetupWelcome(backFn) @@ -173,12 +227,16 @@ async function renderQuickSetupBotToken(backFn:() => api.ODPromiseVoid){ terminal.gray("Your bot should be online with the status 'Configuring Open Ticket...'.") await utilities.timer(3000) //continue - await renderQuickSetupServer(result,async () => {await renderQuickSetupBotToken(backFn)}) + quickSetupStorage.client = result + await renderQuickSetupServer(async () => {await renderQuickSetupBotToken(backFn)}) } } } -async function renderQuickSetupServer(client:api.ODClientManager,backFn:() => api.ODPromiseVoid){ +async function renderQuickSetupServer(backFn:() => api.ODPromiseVoid){ + const {client} = quickSetupStorage + if (!client) return + renderHeader("⏱️ Open Ticket Quick Setup: Discord Server") terminal.bold.blue("(Step 3) Please select a Discord Server to use.\n") @@ -199,15 +257,78 @@ async function renderQuickSetupServer(client:api.ODClientManager,backFn:() => ap }).promise if (answer.canceled) return backFn() - if (answer.selectedIndex == 0) return await renderQuickSetupServer(client,backFn) + if (answer.selectedIndex == 0) return await renderQuickSetupServer(backFn) const server = guilds[answer.selectedIndex-1] - await renderQuickSetupAdminRoles(client,server,[],async () => {await renderQuickSetupServer(client,backFn)}) + quickSetupStorage.guild = server + await renderQuickSetupAdminRoles([],async () => {await renderQuickSetupServer(backFn)}) } -async function renderQuickSetupAdminRoles(client:api.ODClientManager,guild:discord.Guild,selectedAdmins:string[],backFn:() => api.ODPromiseVoid){ +async function renderQuickSetupAdminRoles(selectedAdmins:string[],backFn:() => api.ODPromiseVoid,cachedRoles?:discord.Role[]){ + const {client,guild} = quickSetupStorage + if (!client || !guild) return + renderHeader("⏱️ Open Ticket Quick Setup: Admin Roles") terminal.bold.blue("(Step 4) Please select all 'Global Admins' roles to use.\n") terminal.gray("Users with one of these roles will be able to access & interact with all tickets.\n\n") + const roles = cachedRoles ?? (await guild.roles.fetch()).toJSON().sort((a,b) => b.position-a.position) + const nameList = roles.map((r) => r.name) + const longestName = utilities.getLongestLength(nameList) + const roleList = roles.map((r) => selectedAdmins.includes(r.id) ? ansis.green("(✅) "+r.name.padEnd(longestName+5," ")+ansis.gray(" ("+r.id+")")) : r.name.padEnd(longestName+5," ")+ansis.gray(" ("+r.id+")")) + + const answer = await terminal.singleColumnMenu([ansis.green("🔄 "),ansis.green("🆗 "),...roleList],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return backFn() + if (answer.selectedIndex == 0) return await renderQuickSetupAdminRoles(selectedAdmins,backFn) + if (answer.selectedIndex == 1){ + quickSetupStorage.globalAdmins = selectedAdmins + return await renderQuickSetupColorPicker(backFn) + } + const adminRole = roles[answer.selectedIndex-2] + const index = selectedAdmins.findIndex((r) => r == adminRole.id) + if (index > -1) selectedAdmins.splice(index,1) + else selectedAdmins.push(adminRole.id) + return await renderQuickSetupAdminRoles(selectedAdmins,backFn,roles) +} + +async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid,tryAgain?:boolean){ + const {client,guild,globalAdmins} = quickSetupStorage + if (!client || !guild || !globalAdmins) return + + renderHeader("⏱️ Open Ticket Quick Setup: Main Color") + + terminal.bold.blue("(Step 5) Please insert a valid hex-color to use in all embeds.\n") + terminal.gray("You can also choose from existing presets. (e.g. red, green, blue, ...)\n\n") + terminal.gray(tryAgain ? ansis.bold.red("Invalid color, please try again!\n")+ansis.gray("> ") : "> ") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true, + autoComplete:Array.from(presetColors.keys()), + autoCompleteHint:true, + autoCompleteMenu:autoCompleteMenuOpts as Terminal.Autocompletion + }).promise + + if (typeof answer != "string") return await backFn() + else{ + if (!Array.from(presetColors.keys()).includes(answer) && !/^#[0-9a-f]{6}$/.test(answer)) return await renderQuickSetupColorPicker(backFn,true) + let color: discord.ColorResolvable + if (Array.from(presetColors.keys()).includes(answer)){ + color = presetColors.get(answer) as number + }else{ + color = answer as `#${string}` + } + quickSetupStorage.mainColor = color + //CONTINUE TO NEXT QUESTION + console.log(color) + } } From f117943e000ffd07a9fb74634d60ea88c09acd7f Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Fri, 2 May 2025 17:51:52 +0200 Subject: [PATCH 28/78] Added 6th, 7th & 8th questions to quick setup CLI --- src/core/cli/quickSetup.ts | 127 ++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 3 deletions(-) diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index e742c17..941e121 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -8,7 +8,11 @@ interface ODQuickSetupVariables { client?:api.ODClientManager, guild?:discord.Guild, globalAdmins?:string[], - mainColor?:discord.ColorResolvable + mainColor?:discord.ColorResolvable, + language?:string, + slashCommands?:boolean, + textCommands?:boolean, + status?:api.ODJsonConfig_DefaultStatusType } const quickSetupStorage: ODQuickSetupVariables = {} const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { @@ -328,7 +332,124 @@ async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid,tryAga color = answer as `#${string}` } quickSetupStorage.mainColor = color - //CONTINUE TO NEXT QUESTION - console.log(color) + await renderQuickSetupLanguage(async () => {await renderQuickSetupColorPicker(backFn)}) } } + +async function renderQuickSetupLanguage(backFn:() => api.ODPromiseVoid,tryAgain?:boolean){ + renderHeader("⏱️ Open Ticket Quick Setup: Language") + + terminal.bold.blue("(Step 6) What language would you like to use in the bot?\n") + terminal.gray("View a list of available languages here: https://otgithub.dj-dj.be/tree/main/README.md#-translators\n\n") + terminal.gray(tryAgain ? ansis.bold.red("Language not found, please try again!\n")+ansis.gray("> ") : "> ") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true, + autoComplete:opendiscord.defaults.getDefault("languageList"), + autoCompleteHint:true, + autoCompleteMenu:autoCompleteMenuOpts as Terminal.Autocompletion + }).promise + + if (typeof answer != "string") return await backFn() + else{ + if (!opendiscord.defaults.getDefault("languageList").includes(answer.toLowerCase())) return await renderQuickSetupLanguage(backFn,true) + quickSetupStorage.language = answer.toLowerCase() + await renderQuickSetupCommandTypes(async () => {await renderQuickSetupLanguage(backFn)}) + } +} + +async function renderQuickSetupCommandTypes(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Command Types") + + terminal.bold.blue("(Step 7) Would you like to use slash commands, text commands or both?\n") + terminal.gray("Slash commands are recommended.\n\n") + + const answer = await terminal.singleColumnMenu([ + "Use Slash Commands", + "Use Text Commands", + "Use Both Slash & Text Commands", + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else if (answer.selectedIndex == 0){ + quickSetupStorage.slashCommands = true + quickSetupStorage.textCommands = false + }else if (answer.selectedIndex == 1){ + quickSetupStorage.slashCommands = false + quickSetupStorage.textCommands = true + }else if (answer.selectedIndex == 2){ + quickSetupStorage.slashCommands = true + quickSetupStorage.textCommands = true + } + await renderQuickSetupStatusType(async () => {await renderQuickSetupCommandTypes(backFn)}) +} + +async function renderQuickSetupStatusType(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Status Type") + + terminal.bold.blue("(Step 8) Please select the type of status you want to use.\n") + terminal.gray("The status will be shown below the bot name in the userlist.\n\n") + + const answer = await terminal.singleColumnMenu([ + "Disabled", + "Custom", + "Listening To ...", + "Watching ...", + "Playing ..." + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else if (answer.selectedIndex == 0) quickSetupStorage.status = {enabled:false,status:"online",type:"custom",text:""} + else if (answer.selectedIndex == 1) quickSetupStorage.status = {enabled:true,status:"online",type:"custom",text:""} + else if (answer.selectedIndex == 2) quickSetupStorage.status = {enabled:true,status:"online",type:"listening",text:""} + else if (answer.selectedIndex == 3) quickSetupStorage.status = {enabled:true,status:"online",type:"watching",text:""} + else if (answer.selectedIndex == 4) quickSetupStorage.status = {enabled:true,status:"online",type:"playing",text:""} + + if (answer.selectedIndex == 0) await renderQuickSetupLOREMIPSUM9(async () => {await renderQuickSetupStatusType(backFn)}) + else await renderQuickSetupStatusText(async () => {await renderQuickSetupStatusType(backFn)}) +} + +async function renderQuickSetupStatusText(backFn:() => api.ODPromiseVoid){ + const {status} = quickSetupStorage + if (!status) return + + renderHeader("⏱️ Open Ticket Quick Setup: Status Text") + + terminal.bold.blue("(Step 8.1) What text would you like to display in the status?\n") + terminal.gray("This will be appended after the type you have chosen in the previous question.\n\n> ") + terminal.gray(status.type == "listening" ? "Listening To " : (status.type == "playing" ? "Playing " : (status.type == "watching" ? "Watching " : ""))) + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true + }).promise + + if (typeof answer != "string") return await backFn() + else{ + status.text = answer + await renderQuickSetupLOREMIPSUM9(async () => {await renderQuickSetupStatusText(backFn)}) + } +} + +async function renderQuickSetupLOREMIPSUM9(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") + + console.log("todo",quickSetupStorage) +} From 03b3495eb26dbd70411add96f8eb7a08d42832fb Mon Sep 17 00:00:00 2001 From: JasperAtSchool Date: Fri, 6 Jun 2025 11:14:21 +0200 Subject: [PATCH 29/78] (v4.1) Added new string config checker validators --- src/core/api/modules/checker.ts | 39 ++++++++++++++++++++++++++++++++- src/core/cli/quickSetup.ts | 24 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index 6588475..3e70b11 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -535,8 +535,22 @@ export interface ODCheckerStringStructureOptions extends ODCheckerStructureOptio endsWith?:string, /**This string needs to contain ... */ contains?:string, + /**This string is not allowed to contain ... */ + invertedContains?:string, /**You need to choose between ... */ choices?:string[], + /**This string needs to be in lowercase. */ + lowercaseOnly?:boolean, + /**This string needs to be in uppercase. */ + uppercaseOnly?:boolean, + /**This string shouldn't contain any special characters (allowed: A-Z, a-z, 0-9, space, a few punctuation marks, ...). */ + noSpecialCharacters?:boolean, + /**Do not allow any spaces in this string. */ + withoutSpaces?:boolean, + /**Give a warning when a sentence doesn't start with a capital letter. Or require every word to start with a capital letter. (Ignores numbers, unicode characters, ...) */ + capitalLetterWarning?:false|"sentence"|"word" + /**Give a warning when a sentence doesn't end with a punctuation letter (.,?!) */ + punctuationWarning?:boolean /**The string needs to match this regex */ regex?:RegExp, /**Provide an optional list for autocomplete when using the Interactive Setup CLI. Defaults to the `choices` option. */ @@ -582,13 +596,36 @@ export class ODCheckerStringStructure extends ODCheckerStructure { }else if (typeof this.options.contains != "undefined" && !value.includes(this.options.contains)){ checker.createMessage("opendiscord:string-contains","error",`This string needs to contain "${this.options.contains}"!`,lt,null,[`"${this.options.contains}"`],this.id,(this.options.docs ?? null)) return false + }else if (typeof this.options.invertedContains != "undefined" && value.includes(this.options.invertedContains)){ + checker.createMessage("opendiscord:string-inverted-contains","error",`This string is not allowed to contain "${this.options.invertedContains}"!`,lt,null,[`"${this.options.invertedContains}"`],this.id,(this.options.docs ?? null)) + return false }else if (typeof this.options.choices != "undefined" && !this.options.choices.includes(value)){ checker.createMessage("opendiscord:string-choices","error",`This string can only be one of the following values: "${this.options.choices.join(`", "`)}"!`,lt,null,[`"${this.options.choices.join(`", "`)}"`],this.id,(this.options.docs ?? null)) return false + }else if (this.options.lowercaseOnly && value !== value.toLowerCase()){ + checker.createMessage("opendiscord:string-lowercase","error",`This string must be written in lowercase only!`,lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (this.options.uppercaseOnly && value !== value.toUpperCase()){ + checker.createMessage("opendiscord:string-uppercase","error",`This string must be written in uppercase only!`,lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (this.options.noSpecialCharacters && !/^[A-Za-z0-9 ]*$/.test(value)){ + checker.createMessage("opendiscord:string-special-characters","error",`This string is not allowed to contain any special characters! (a-z, 0-9 & space only)`,lt,null,[],this.id,(this.options.docs ?? null)) + return false + }else if (this.options.withoutSpaces && value.includes(" ")){ + checker.createMessage("opendiscord:string-no-spaces","error",`This string is not allowed to contain spaces!`,lt,null,[],this.id,(this.options.docs ?? null)) + return false }else if (typeof this.options.regex != "undefined" && !this.options.regex.test(value)){ checker.createMessage("opendiscord:string-regex","error","This string is invalid!",lt,null,[],this.id,(this.options.docs ?? null)) return false - }else return super.check(checker,value,locationTrace) + }else{ + //warnings + if ((this.options.capitalLetterWarning == "word" && !value.split(" ").every((word) => word.length == 0 || /^[^a-z].*/.test(word)))) checker.createMessage("opendiscord:string-capital-word","warning",`It's recommended that each word in this string starts with a capital letter!`,lt,null,[],this.id,(this.options.docs ?? null)) + if ((this.options.capitalLetterWarning == "sentence" && !value.split(/ *[.?!] */).every((sentence) => sentence.length == 0 || /^[^a-z].*/.test(sentence)))) checker.createMessage("opendiscord:string-capital-word","warning",`It looks like some sentences in this string don't start with a capital letter!`,lt,null,[],this.id,(this.options.docs ?? null)) + + //TODO: punctuation!!! + + return super.check(checker,value,locationTrace) + } } } diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 941e121..999f1c9 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -453,3 +453,27 @@ async function renderQuickSetupLOREMIPSUM9(backFn:() => api.ODPromiseVoid){ console.log("todo",quickSetupStorage) } + +async function renderQuickSetupLOREMIPSUM10(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") + + console.log("todo",quickSetupStorage) +} + +async function renderQuickSetupLOREMIPSUM11(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") + + console.log("todo",quickSetupStorage) +} + +async function renderQuickSetupLOREMIPSUM12(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") + + console.log("todo",quickSetupStorage) +} + +async function renderQuickSetupLOREMIPSUM13(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") + + console.log("todo",quickSetupStorage) +} \ No newline at end of file From 72627a7058c837a5311f7db5f73ab0d5831d6a12 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 11 Jun 2025 21:10:29 +0200 Subject: [PATCH 30/78] Added new config checker utility function createTemporaryCheckerEnvironment() is able to help you with using `ODCheckerStructure` validators without officially registering them in opendiscord.checkers. --- src/core/api/modules/checker.ts | 4 ++++ src/core/startup/pluginLauncher.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index 3e70b11..8b6f8d7 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -82,6 +82,10 @@ export class ODCheckerManager extends ODManager { messages:final } } + /**Create temporary and unlisted `ODConfig`, `ODChecker` & `ODCheckerStorage` classes. This will help you use a `ODCheckerStructure` validator without officially registering it in `opendiscord.checkers`. */ + createTemporaryCheckerEnvironment(){ + return new ODChecker("opendiscord:temporary-environment",new ODCheckerStorage(),0,new ODConfig("opendiscord:temporary-environment",{}),new ODCheckerStructure("opendiscord:temporary-environment",{})) + } } /**## ODCheckerStorage `class` diff --git a/src/core/startup/pluginLauncher.ts b/src/core/startup/pluginLauncher.ts index 1d3401c..f8e219f 100644 --- a/src/core/startup/pluginLauncher.ts +++ b/src/core/startup/pluginLauncher.ts @@ -82,7 +82,7 @@ export const loadAllPlugins = async () => { } }) - //sorted plugins (based on priority) + //sorted plugins (sorted on priority. All plugins are loaded & enabled) const sortedPlugins = opendiscord.plugins.getAll().sort((a,b) => { return (b.priority - a.priority) }) From dd3c159413ebf01cac171856092ba672ce0ae415 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 11 Jun 2025 21:12:20 +0200 Subject: [PATCH 31/78] Added new questions to quick setup CLI Added: channel logs, ticket category, ticket creation count, ticket option configuration (name, descr, button, prefix & suffix) --- src/core/cli/quickSetup.ts | 386 +++++++++++++++++++++++++++++++++---- 1 file changed, 353 insertions(+), 33 deletions(-) diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 999f1c9..c45cd29 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -12,9 +12,20 @@ interface ODQuickSetupVariables { language?:string, slashCommands?:boolean, textCommands?:boolean, - status?:api.ODJsonConfig_DefaultStatusType + status?:api.ODJsonConfig_DefaultStatusType, + logChannel?:string|null, + ticketCategory?:string|null, + ticketOptions:({ + name:string, + description:string, + buttonType:"label-emoji"|"emoji"|"label", + buttonColor:api.ODValidButtonColor, + buttonEmoji:string|null, + channelPrefix:string, + channelSuffix:api.ODJsonConfig_DefaultOptionTicketChannelType["suffix"] + }|null)[] } -const quickSetupStorage: ODQuickSetupVariables = {} +const quickSetupStorage: ODQuickSetupVariables = {ticketOptions:[]} const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, selectedStyle:terminal.bgBlue.white @@ -332,7 +343,7 @@ async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid,tryAga color = answer as `#${string}` } quickSetupStorage.mainColor = color - await renderQuickSetupLanguage(async () => {await renderQuickSetupColorPicker(backFn)}) + return await renderQuickSetupLanguage(async () => {await renderQuickSetupColorPicker(backFn)}) } } @@ -340,7 +351,7 @@ async function renderQuickSetupLanguage(backFn:() => api.ODPromiseVoid,tryAgain? renderHeader("⏱️ Open Ticket Quick Setup: Language") terminal.bold.blue("(Step 6) What language would you like to use in the bot?\n") - terminal.gray("View a list of available languages here: https://otgithub.dj-dj.be/tree/main/README.md#-translators\n\n") + terminal.gray("View a list of available languages here: https://otgithub.dj-dj.be#-translators\n\n") terminal.gray(tryAgain ? ansis.bold.red("Language not found, please try again!\n")+ansis.gray("> ") : "> ") const answer = await terminal.inputField({ @@ -356,7 +367,7 @@ async function renderQuickSetupLanguage(backFn:() => api.ODPromiseVoid,tryAgain? else{ if (!opendiscord.defaults.getDefault("languageList").includes(answer.toLowerCase())) return await renderQuickSetupLanguage(backFn,true) quickSetupStorage.language = answer.toLowerCase() - await renderQuickSetupCommandTypes(async () => {await renderQuickSetupLanguage(backFn)}) + return await renderQuickSetupCommandTypes(async () => {await renderQuickSetupLanguage(backFn)}) } } @@ -390,7 +401,7 @@ async function renderQuickSetupCommandTypes(backFn:() => api.ODPromiseVoid){ quickSetupStorage.slashCommands = true quickSetupStorage.textCommands = true } - await renderQuickSetupStatusType(async () => {await renderQuickSetupCommandTypes(backFn)}) + return await renderQuickSetupStatusType(async () => {await renderQuickSetupCommandTypes(backFn)}) } async function renderQuickSetupStatusType(backFn:() => api.ODPromiseVoid){ @@ -421,8 +432,8 @@ async function renderQuickSetupStatusType(backFn:() => api.ODPromiseVoid){ else if (answer.selectedIndex == 3) quickSetupStorage.status = {enabled:true,status:"online",type:"watching",text:""} else if (answer.selectedIndex == 4) quickSetupStorage.status = {enabled:true,status:"online",type:"playing",text:""} - if (answer.selectedIndex == 0) await renderQuickSetupLOREMIPSUM9(async () => {await renderQuickSetupStatusType(backFn)}) - else await renderQuickSetupStatusText(async () => {await renderQuickSetupStatusType(backFn)}) + if (answer.selectedIndex == 0) return await renderQuickSetupLogs(async () => {await renderQuickSetupStatusType(backFn)}) + else return await renderQuickSetupStatusText(async () => {await renderQuickSetupStatusType(backFn)}) } async function renderQuickSetupStatusText(backFn:() => api.ODPromiseVoid){ @@ -444,36 +455,345 @@ async function renderQuickSetupStatusText(backFn:() => api.ODPromiseVoid){ if (typeof answer != "string") return await backFn() else{ status.text = answer - await renderQuickSetupLOREMIPSUM9(async () => {await renderQuickSetupStatusText(backFn)}) + return await renderQuickSetupLogs(async () => {await renderQuickSetupStatusText(backFn)}) } } -async function renderQuickSetupLOREMIPSUM9(backFn:() => api.ODPromiseVoid){ +async function renderQuickSetupLogs(backFn:() => api.ODPromiseVoid){ + const {client,guild} = quickSetupStorage + if (!client || !guild) return + + renderHeader("⏱️ Open Ticket Quick Setup: Channel Logs") + + terminal.bold.blue("(Step 9) Please select the 'Text Channel' to use for logs.\n") + terminal.gray("All logs of the bot will be sent here. Make sure only admins can access this channel.\n\n") + + const rawChannels = (await guild.channels.fetch()).toJSON().filter((c) => c !== null && c.isTextBased()) + const channels = rawChannels.sort((a,b) => (a.position + 50*((a.parent?.position ?? -1)+1)) - (b.position + 50*((b.parent?.position ?? -1)+1))) + const nameList = channels.map((r) => (r.parent ? r.parent.name+" > " : "")+r.name) + const longestName = utilities.getLongestLength(nameList) + const channelList = channels.map((r) => ((r.parent ? r.parent.name+" > " : "")+r.name).padEnd(longestName+5," ")+ansis.gray(" ("+r.id+")")) + + const answer = await terminal.singleColumnMenu([ansis.green("🔄 "),ansis.red("❌ "),...channelList],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return backFn() + else if (answer.selectedIndex == 0) return await renderQuickSetupLogs(backFn) + else if (answer.selectedIndex == 1){ + quickSetupStorage.logChannel = null + return await renderQuickSetupTicketCategory(async () => {await renderQuickSetupLogs(backFn)}) + }else{ + const logChannel = channels[answer.selectedIndex-2] + quickSetupStorage.logChannel = logChannel.id + return await renderQuickSetupTicketCategory(async () => {await renderQuickSetupLogs(backFn)}) + } +} + + +async function renderQuickSetupTicketCategory(backFn:() => api.ODPromiseVoid){ + const {client,guild} = quickSetupStorage + if (!client || !guild) return + + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Category") + + terminal.bold.blue("(Step 10) Please select which 'Category' you would like tickets to be created in.\n") + terminal.gray("When no category is selected, tickets will appear at the top of the channel list.\n\n") + + const rawCategories = (await guild.channels.fetch()).toJSON().filter((c) => c !== null && c.type == discord.ChannelType.GuildCategory) + const categories = rawCategories.sort((a,b) => a.position-b.position) + const nameList = categories.map((r) => r.name) + const longestName = utilities.getLongestLength(nameList) + const categoryList = categories.map((r) => r.name.padEnd(longestName+5," ")+ansis.gray(" ("+r.id+")")) + + const answer = await terminal.singleColumnMenu([ansis.green("🔄 "),ansis.red("❌ "),...categoryList],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return backFn() + else if (answer.selectedIndex == 0) return await renderQuickSetupTicketCategory(backFn) + else if (answer.selectedIndex == 1){ + quickSetupStorage.ticketCategory = null + return await renderQuickSetupTicketCount(async () => {await renderQuickSetupTicketCategory(backFn)}) + }else{ + const ticketCategory = categories[answer.selectedIndex-2] + quickSetupStorage.ticketCategory = ticketCategory.id + return await renderQuickSetupTicketCount(async () => {await renderQuickSetupTicketCategory(backFn)}) + } +} + +async function renderQuickSetupTicketCount(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration") + + terminal.bold.blue("(Step 11) How many ticket types would you like to create?\n") + terminal.gray("You can always add more ticket types afterwards.\n\n") + + const answer = await terminal.singleColumnMenu([ + "1️⃣ 1 Ticket Option", + "2️⃣ 2 Ticket Options", + "3️⃣ 3 Ticket Options", + "4️⃣ 4 Ticket Options", + "5️⃣ 5 Ticket Options", + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + const ticketCount = answer.selectedIndex+1 + quickSetupStorage.ticketOptions = [] + for (let i = 0; i < ticketCount; i++){ + quickSetupStorage.ticketOptions.push(null) + } + return await renderQuickSetupCreateTicketName(0,ticketCount,async () => {await renderQuickSetupTicketCount(backFn)}) + } +} + +async function renderQuickSetupCreateTicketName(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") + + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the name of this ticket option\n") + terminal.gray("Recommendation: Clean, short, obvious name, not more than ±30 characters. \n\n") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true, + }).promise + + if (typeof answer != "string"){ + //delete option ticket from list when going back to menu or previous ticket settings + quickSetupStorage.ticketOptions[ticketIndex] = null + return await backFn() + }else if (answer.length == 0){ + terminal.red.bold("\n\n❌ Please insert a valid ticket option name.\n") + await utilities.timer(2000) + return await renderQuickSetupCreateTicketName(ticketIndex,requiredTickets,backFn) + }else{ + quickSetupStorage.ticketOptions[ticketIndex] = { + name:answer, + description:"", + buttonType:"label", + buttonEmoji:null, + buttonColor:"gray", + channelPrefix:"ticket-", + channelSuffix:"user-name" + } + return await renderQuickSetupCreateTicketDescription(ticketIndex,requiredTickets,async () => {await renderQuickSetupCreateTicketName(ticketIndex,requiredTickets,backFn)}) + } +} + +async function renderQuickSetupCreateTicketDescription(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") + + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the description of this ticket option\n") + terminal.gray("Recommendation: Use '\\n' (backslash-n) for a newline.\n\n") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true, + }).promise + + if (typeof answer != "string") return await backFn() + else{ + const ticketOption = quickSetupStorage.ticketOptions[ticketIndex] + if (ticketOption) ticketOption.description = answer.replaceAll("\\n","\n") + return await renderQuickSetupCreateTicketButtonType(ticketIndex,requiredTickets,async () => {await renderQuickSetupCreateTicketDescription(ticketIndex,requiredTickets,backFn)}) + } +} + +async function renderQuickSetupCreateTicketButtonType(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") + + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) How would you like to display the ticket name in the button/dropdown?\n") + terminal.gray("You will be able to choose between dropdown/buttons when configuring panels.\n\n") + + const answer = await terminal.singleColumnMenu([ + "Emoji + Label/Name", + "Label/Name Only", + "Emoji Only" + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + const ticketOption = quickSetupStorage.ticketOptions[ticketIndex] + if (ticketOption) ticketOption.buttonType = (answer.selectedIndex == 0) ? "label-emoji" : ((answer.selectedIndex == 1) ? "label" : "emoji") + if (answer.selectedIndex == 0 || answer.selectedIndex == 2){ + return await renderQuickSetupCreateTicketButtonEmoji(ticketIndex,requiredTickets,async () => {await renderQuickSetupCreateTicketButtonType(ticketIndex,requiredTickets,backFn)}) + }else{ + return await renderQuickSetupCreateTicketButtonColor(ticketIndex,requiredTickets,async () => {await renderQuickSetupCreateTicketButtonType(ticketIndex,requiredTickets,backFn)}) + } + } +} + +async function renderQuickSetupCreateTicketButtonEmoji(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") + + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the button emoji of this ticket option.\n") + terminal.gray("Only 1 emoji allowed. Tip: Insert custom emoji's via the following syntax: <:12345678910:emoji_name>\n\n") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true, + }).promise + + if (typeof answer != "string") return await backFn() + else{ + //check emoji using a local config checker instance + const isEmojiValid = (new api.ODCheckerCustomStructure_EmojiString("opendiscord:emoji-checker",1,1,true)).check(opendiscord.checkers.createTemporaryCheckerEnvironment(),answer,["emoji"]) + if (!isEmojiValid){ + terminal.red.bold("\n\n❌ Please insert a valid emoji.\n") + await utilities.timer(2000) + return await renderQuickSetupCreateTicketButtonEmoji(ticketIndex,requiredTickets,backFn) + } + + const ticketOption = quickSetupStorage.ticketOptions[ticketIndex] + if (ticketOption) ticketOption.buttonEmoji = answer + return await renderQuickSetupCreateTicketButtonColor(ticketIndex,requiredTickets,async () => {await renderQuickSetupCreateTicketButtonEmoji(ticketIndex,requiredTickets,backFn)}) + } +} + +async function renderQuickSetupCreateTicketButtonColor(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") + + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) What color would you like the button to be?\n") + terminal.gray("This will not apply when choosing 'dropdown' mode in the panel configuration.\n\n") + + const answer = await terminal.singleColumnMenu([ + "Gray (Default)", + "Blue", + "Red", + "Green" + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + const ticketOption = quickSetupStorage.ticketOptions[ticketIndex] + if (ticketOption) ticketOption.buttonColor = (answer.selectedIndex == 0) ? "gray" : ((answer.selectedIndex == 1) ? "blue" : ((answer.selectedIndex == 2) ? "red" : "green")) + return await renderQuickSetupCreateTicketChannelPrefix(ticketIndex,requiredTickets,async () => {await renderQuickSetupCreateTicketButtonType(ticketIndex,requiredTickets,backFn)}) + } +} + +async function renderQuickSetupCreateTicketChannelPrefix(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") + + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the channel prefix of this ticket option.\n") + terminal.gray("Examples: 'ticket-', 'question-', 'test-channel-', ...\n\n") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true, + }).promise + + if (typeof answer != "string") return await backFn() + else if (answer.length == 0){ + terminal.red.bold("\n\n❌ Please insert a valid ticket option channel prefix.\n") + await utilities.timer(2000) + return await renderQuickSetupCreateTicketChannelPrefix(ticketIndex,requiredTickets,backFn) + }else{ + const ticketOption = quickSetupStorage.ticketOptions[ticketIndex] + if (ticketOption) ticketOption.channelPrefix = answer + return await renderQuickSetupCreateTicketChannelSuffix(ticketIndex,requiredTickets,async () => {await renderQuickSetupCreateTicketChannelPrefix(ticketIndex,requiredTickets,backFn)}) + } +} + +async function renderQuickSetupCreateTicketChannelSuffix(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") + + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please select the channel suffix mode of this ticket option.\n") + terminal.gray("The suffix is appended after the prefix and will be generated on ticket creation.\n\n") + + const answer = await terminal.singleColumnMenu([ + "Username (e.g. #ticket-DJj123dj, #question-wumpus)", + "User Id (e.g. #ticket-123456789, #question-01020304)", + "Random Number (e.g. #ticket-1234, #question-1411)", + "Random Hex (e.g. #ticket-f8ba, #question-01f3)", + "Dynamic Counter (e.g. #ticket-1, #question-23)", + "Fixed Counter (e.g. #ticket-0001, #question-0023)" + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + const ticketOption = quickSetupStorage.ticketOptions[ticketIndex] + if (ticketOption){ + if (answer.selectedIndex == 0) ticketOption.channelSuffix = "user-name" + else if (answer.selectedIndex == 1) ticketOption.channelSuffix = "user-id" + else if (answer.selectedIndex == 2) ticketOption.channelSuffix = "random-number" + else if (answer.selectedIndex == 3) ticketOption.channelSuffix = "random-hex" + else if (answer.selectedIndex == 4) ticketOption.channelSuffix = "counter-dynamic" + else if (answer.selectedIndex == 5) ticketOption.channelSuffix = "counter-fixed" + } + + //create next ticket + if (ticketIndex+1 < requiredTickets) return await renderQuickSetupCreateTicketName(ticketIndex+1,requiredTickets,async () => {await renderQuickSetupCreateTicketChannelSuffix(ticketIndex,requiredTickets,backFn)}) + else return await renderQuickSetupLOREMIPSUM(async () => {await renderQuickSetupCreateTicketChannelSuffix(ticketIndex,requiredTickets,backFn)}) + } +} + +async function renderQuickSetupLOREMIPSUM(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") console.log("todo",quickSetupStorage) } -async function renderQuickSetupLOREMIPSUM10(backFn:() => api.ODPromiseVoid){ - renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") - - console.log("todo",quickSetupStorage) -} - -async function renderQuickSetupLOREMIPSUM11(backFn:() => api.ODPromiseVoid){ - renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") - - console.log("todo",quickSetupStorage) -} - -async function renderQuickSetupLOREMIPSUM12(backFn:() => api.ODPromiseVoid){ - renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") - - console.log("todo",quickSetupStorage) -} - -async function renderQuickSetupLOREMIPSUM13(backFn:() => api.ODPromiseVoid){ - renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") - - console.log("todo",quickSetupStorage) -} \ No newline at end of file +/** Steps Todo + * - S12: Enable Autoclose => select to enable autoclose in all tickets + * - S13: Enable Cooldown => select to enable cooldown in all tickets (with a specific duration from dropdown) + * + * - S15: Panel Name + * - S16: Panel Description + * - S17: Panel Mode => (dropdown/buttons) + * - S18: Panel Auto Describe Options => (dropdown: disabled, in text, in embed fields, in embed description) + * - S19: Panel Max Tickets Warning + * - S20: TODO!! => extra's in general.json "system" => e.g. removeParticipantsOnClose, reply Ticket creation, ... + * + * - Ticket configuration => per-ticket configuration + * - ticket name + * - ticket description + * - button type => (dropdown: label+emoji, label-only or emoji-only) + * - button emoji + * - ticket prefix + * - ticket suffix (dropdown) + * - (option ID autogenerated from name BE AWARE OF TICKETS WITH SAME NAME!! + remove unicode, spaces & special chars from id) + * - (embed autofilled with name+desc+color, thumbnail will automatically be set to the server icon) + * - (ping will be @here) + */ \ No newline at end of file From 9f4999b89b6d19906b7993e89bccb5acd0cb75a9 Mon Sep 17 00:00:00 2001 From: sdehaarte Date: Wed, 11 Jun 2025 20:06:43 -0400 Subject: [PATCH 32/78] Feature Reaction Role Logs. Adds logging functionality for reaction roles. This helps track when users add or remove reaction roles. --- src/actions/reactionRole.ts | 7 +++++++ src/builders/embeds.ts | 37 +++++++++++++++++++++++++++++++++++++ src/builders/messages.ts | 10 ++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/actions/reactionRole.ts b/src/actions/reactionRole.ts index 80daaad..f947bdf 100644 --- a/src/actions/reactionRole.ts +++ b/src/actions/reactionRole.ts @@ -4,6 +4,8 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" +const generalConfig = opendiscord.configs.get("opendiscord:general") + export const registerActions = async () => { opendiscord.actions.add(new api.ODAction("opendiscord:reaction-role")) opendiscord.actions.get("opendiscord:reaction-role").workers.add([ @@ -79,6 +81,11 @@ export const registerActions = async () => { //update instance & finish event instance.result = result + + if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.roleAdding.logs){ + const logChannel = opendiscord.posts.get("opendiscord:logs") + if (logChannel) { logChannel.send( await opendiscord.builders.messages.getSafe("opendiscord:reaction-role-logs").build(source,{guild,user,role,result}))}} + await opendiscord.events.get("afterRolesUpdated").emit([user,role]) }), new api.ODWorker("opendiscord:logs",0,(instance,params,source,cancel) => { diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index f06dde2..9ff53b4 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -1107,8 +1107,45 @@ const roleEmbeds = () => { else instance.setDescription(lang.getTranslation("actions.descriptions.rolesEmpty")) }) ) + + //REACTION ROLE LOGS + embeds.add(new api.ODEmbed("opendiscord:reaction-role-logs")) + embeds.get("opendiscord:reaction-role-logs")!.workers.add( + new api.ODWorker("opendiscord:reaction-role-logs",0,async (instance,params,source) => { + const {user,result} = params + + const newResult = result + .filter(r => r.action != null && r.role?.id) + .sort((a, b) => { + if (a.action === "added" && b.action === "removed") return -1; + if (a.action === "removed" && b.action === "added") return 1; + return 0; + }) + .map(r => { + return (r.action === "added") + ? `🟢${lang.getTranslation("params.uppercase.added")} ${discord.roleMention(r.role.id)}` + : `🔴 ${lang.getTranslation("params.uppercase.removed")} ${discord.roleMention(r.role.id)}` + }) + + let baseDescription = lang.getTranslationWithParams("actions.logs.roleLog", [discord.userMention(user.id)]) + if (!baseDescription || baseDescription === "null") { + baseDescription = `Roles updated for ${discord.userMention(user.id)}` + } + + const fullDescription = (newResult.length > 0) ? `${baseDescription}\n\n${newResult.join("\n")}` : `${baseDescription}\n\n${lang.getTranslation("actions.descriptions.rolesEmpty")}` + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("📋", lang.getTranslation("actions.titles.roles"))) + instance.setThumbnail(user.displayAvatarURL()) + instance.setAuthor(user.displayName, user.displayAvatarURL()) + instance.setTimestamp(new Date()) + instance.setDescription(fullDescription) + }) + ) } +export default roleEmbeds + const clearEmbeds = () => { //CLEAR VERIFY MESSAGE embeds.add(new api.ODEmbed("opendiscord:clear-verify-message")) diff --git a/src/builders/messages.ts b/src/builders/messages.ts index 3b1531f..b618b0d 100644 --- a/src/builders/messages.ts +++ b/src/builders/messages.ts @@ -964,6 +964,16 @@ const roleMessages = () => { instance.setEphemeral(true) }) ) + + //REACTION ROLE LOGS + messages.add(new api.ODMessage("opendiscord:reaction-role-logs")) + messages.get("opendiscord:reaction-role-logs")!.workers.add( + new api.ODWorker("opendiscord:reaction-role-logs",0,async(instance,params,source) => { + const {guild,user,role,result} = params + const embed = await embeds.getSafe("opendiscord:reaction-role-logs").build(source,{guild,user,role,result}) + instance.addEmbed(embed) + }) + ) } const clearMessages = () => { From 31b94718f1695c9221b6f20b0f8bc42208199138 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:47:09 +0200 Subject: [PATCH 33/78] Repositioned + Tested Reaction Role Logs --- src/actions/reactionRole.ts | 18 +++++--- src/builders/embeds.ts | 77 ++++++++++++++++++++------------ src/builders/messages.ts | 16 +++++-- src/core/api/defaults/builder.ts | 6 +++ 4 files changed, 79 insertions(+), 38 deletions(-) diff --git a/src/actions/reactionRole.ts b/src/actions/reactionRole.ts index f947bdf..c9cff4b 100644 --- a/src/actions/reactionRole.ts +++ b/src/actions/reactionRole.ts @@ -81,13 +81,21 @@ export const registerActions = async () => { //update instance & finish event instance.result = result - - if (generalConfig.data.system.logs.enabled && generalConfig.data.system.messages.roleAdding.logs){ - const logChannel = opendiscord.posts.get("opendiscord:logs") - if (logChannel) { logChannel.send( await opendiscord.builders.messages.getSafe("opendiscord:reaction-role-logs").build(source,{guild,user,role,result}))}} - await opendiscord.events.get("afterRolesUpdated").emit([user,role]) }), + new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,user,option,overwriteMode} = params + if (!instance.role || !instance.result) return + + //to logs + if (generalConfig.data.system.logs.enabled && (generalConfig.data.system.messages.roleAdding.logs || generalConfig.data.system.messages.roleRemoving.logs)){ + const logChannel = opendiscord.posts.get("opendiscord:logs") + if (logChannel) logChannel.send(await opendiscord.builders.messages.getSafe("opendiscord:reaction-role-logs").build(source,{guild,user,role:instance.role,result:instance.result})) + } + + //to dm + if (generalConfig.data.system.messages.roleAdding.dm || generalConfig.data.system.messages.roleRemoving.dm) await opendiscord.client.sendUserDm(user,await opendiscord.builders.messages.getSafe("opendiscord:reaction-role-dm").build(source,{guild,user,role:instance.role,result:instance.result})) + }), new api.ODWorker("opendiscord:logs",0,(instance,params,source,cancel) => { const {guild,user,option} = params opendiscord.log(user.displayName+" updated his roles!","info",[ diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index 9ff53b4..f06d209 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -1108,38 +1108,57 @@ const roleEmbeds = () => { }) ) - //REACTION ROLE LOGS - embeds.add(new api.ODEmbed("opendiscord:reaction-role-logs")) - embeds.get("opendiscord:reaction-role-logs")!.workers.add( - new api.ODWorker("opendiscord:reaction-role-logs",0,async (instance,params,source) => { - const {user,result} = params - - const newResult = result - .filter(r => r.action != null && r.role?.id) - .sort((a, b) => { - if (a.action === "added" && b.action === "removed") return -1; - if (a.action === "removed" && b.action === "added") return 1; - return 0; - }) - .map(r => { - return (r.action === "added") - ? `🟢${lang.getTranslation("params.uppercase.added")} ${discord.roleMention(r.role.id)}` - : `🔴 ${lang.getTranslation("params.uppercase.removed")} ${discord.roleMention(r.role.id)}` - }) - - let baseDescription = lang.getTranslationWithParams("actions.logs.roleLog", [discord.userMention(user.id)]) - if (!baseDescription || baseDescription === "null") { - baseDescription = `Roles updated for ${discord.userMention(user.id)}` - } - - const fullDescription = (newResult.length > 0) ? `${baseDescription}\n\n${newResult.join("\n")}` : `${baseDescription}\n\n${lang.getTranslation("actions.descriptions.rolesEmpty")}` + //REACTION ROLE DM + embeds.add(new api.ODEmbed("opendiscord:reaction-role-dm")) + embeds.get("opendiscord:reaction-role-dm").workers.add( + new api.ODWorker("opendiscord:reaction-role-dm",0,async (instance,params,source) => { + const {guild,user,role,result} = params instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("📋", lang.getTranslation("actions.titles.roles"))) - instance.setThumbnail(user.displayAvatarURL()) - instance.setAuthor(user.displayName, user.displayAvatarURL()) + instance.setTitle(utilities.emojiTitle("🎨",lang.getTranslation("actions.titles.roles"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setTimestamp(new Date()) - instance.setDescription(fullDescription) + + const newResult = result.filter((r) => r.action != null).sort((a,b) => { + if (a.action == "added" && b.action == "removed") return -1 + else if (a.action == "removed" && b.action == "added") return 1 + else return 0 + }).map((r) => { + return (r.action == "added") ? "🟢 "+lang.getTranslation("params.uppercase.added")+" @"+r.role.name : "🔴 "+lang.getTranslation("params.uppercase.removed")+" @"+r.role.name + }) + + //TODO TRANSLATION!!! + const baseDescription = ("Your roles in our server have been updated!") + + if (newResult.length > 0) instance.setDescription(baseDescription+"\n\n"+newResult.join("\n")) + else instance.setDescription(baseDescription+"\n"+lang.getTranslation("actions.descriptions.rolesEmpty")) + }) + ) + + //REACTION ROLE LOGS + embeds.add(new api.ODEmbed("opendiscord:reaction-role-logs")) + embeds.get("opendiscord:reaction-role-logs").workers.add( + new api.ODWorker("opendiscord:reaction-role-logs",0,async (instance,params,source) => { + const {guild,user,role,result} = params + + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🎨",lang.getTranslation("actions.titles.roles"))) + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setTimestamp(new Date()) + + const newResult = result.filter((r) => r.action != null).sort((a,b) => { + if (a.action == "added" && b.action == "removed") return -1 + else if (a.action == "removed" && b.action == "added") return 1 + else return 0 + }).map((r) => { + return (r.action == "added") ? "🟢 "+lang.getTranslation("params.uppercase.added")+" "+discord.roleMention(r.role.id) : "🔴 "+lang.getTranslation("params.uppercase.removed")+" "+discord.roleMention(r.role.id) + }) + + //TODO TRANSLATION!!! + const baseDescription = ("{0} has updated their roles!").replace("{0}",discord.userMention(user.id)) + + if (newResult.length > 0) instance.setDescription(baseDescription+"\n\n"+newResult.join("\n")) + else instance.setDescription(baseDescription+"\n"+lang.getTranslation("actions.descriptions.rolesEmpty")) }) ) } diff --git a/src/builders/messages.ts b/src/builders/messages.ts index b618b0d..a371e28 100644 --- a/src/builders/messages.ts +++ b/src/builders/messages.ts @@ -965,13 +965,21 @@ const roleMessages = () => { }) ) + //REACTION ROLE DM + messages.add(new api.ODMessage("opendiscord:reaction-role-dm")) + messages.get("opendiscord:reaction-role-dm").workers.add( + new api.ODWorker("opendiscord:reaction-role-dm",0,async (instance,params,source) => { + const {guild,user,role,result} = params + instance.addEmbed(await embeds.getSafe("opendiscord:reaction-role-dm").build(source,{guild,user,role,result})) + }) + ) + //REACTION ROLE LOGS messages.add(new api.ODMessage("opendiscord:reaction-role-logs")) - messages.get("opendiscord:reaction-role-logs")!.workers.add( - new api.ODWorker("opendiscord:reaction-role-logs",0,async(instance,params,source) => { + messages.get("opendiscord:reaction-role-logs").workers.add( + new api.ODWorker("opendiscord:reaction-role-logs",0,async (instance,params,source) => { const {guild,user,role,result} = params - const embed = await embeds.getSafe("opendiscord:reaction-role-logs").build(source,{guild,user,role,result}) - instance.addEmbed(embed) + instance.addEmbed(await embeds.getSafe("opendiscord:reaction-role-logs").build(source,{guild,user,role,result})) }) ) } diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index 6d556c3..b105a18 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -281,6 +281,9 @@ export interface ODEmbedManagerIds_Default { "opendiscord:transcript-error":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler,reason:string|null},workers:"opendiscord:transcript-error"}, "opendiscord:reaction-role":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:ODRoleUpdateResult[]},workers:"opendiscord:reaction-role"}, + "opendiscord:reaction-role-dm":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:ODRoleUpdateResult[]},workers:"opendiscord:reaction-role-dm"}, + "opendiscord:reaction-role-logs":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:ODRoleUpdateResult[]},workers:"opendiscord:reaction-role-logs"}, + "opendiscord:clear-verify-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"opendiscord:clear-verify-message"}, "opendiscord:clear-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"opendiscord:clear-message"}, "opendiscord:clear-logs":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"opendiscord:clear-logs"}, @@ -412,6 +415,9 @@ export interface ODMessageManagerIds_Default { "opendiscord:transcript-error":{source:"slash"|"text"|"ticket-message"|"reopen-message"|"close-message"|"autoclose-message"|"autodelete"|"clear"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,compiler:ODTranscriptCompiler,reason:string|null},workers:"opendiscord:transcript-error"}, "opendiscord:reaction-role":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:ODRoleUpdateResult[]},workers:"opendiscord:reaction-role"}, + "opendiscord:reaction-role-dm":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:ODRoleUpdateResult[]},workers:"opendiscord:reaction-role-dm"}, + "opendiscord:reaction-role-logs":{source:"panel-button"|"other",params:{guild:discord.Guild,user:discord.User,role:ODRole,result:ODRoleUpdateResult[]},workers:"opendiscord:reaction-role-logs"}, + "opendiscord:clear-verify-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"opendiscord:clear-verify-message"}, "opendiscord:clear-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"opendiscord:clear-message"}, "opendiscord:clear-logs":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:string[]},workers:"opendiscord:clear-logs"}, From 0a29e1364092524b3b70232bdf61cb032f8b68ec Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:37:57 +0200 Subject: [PATCH 34/78] Added new questions to quick setup CLI + fixes Added question 12, 13 & 14 (autoclose, cooldown, close participants policy. Also fixed backFn-bugs and rewrote a few sentences for better spelling. Also added some UI improvements to string-inputs --- src/core/cli/quickSetup.ts | 192 ++++++++++++++++++++++++++++--------- 1 file changed, 149 insertions(+), 43 deletions(-) diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index c45cd29..5ebc9d9 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -23,8 +23,13 @@ interface ODQuickSetupVariables { buttonEmoji:string|null, channelPrefix:string, channelSuffix:api.ODJsonConfig_DefaultOptionTicketChannelType["suffix"] - }|null)[] + }|null)[], + autocloseHours?:number|null, + cooldownMinutes?:number|null, + removeParticipantsOnClose?:boolean } +const stepCount = (count:number) => "(Step "+count+"/20) " + const quickSetupStorage: ODQuickSetupVariables = {ticketOptions:[]} const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, @@ -93,7 +98,7 @@ async function renderQuickSetupWarning(backFn:() => api.ODPromiseVoid) { renderHeader("⏱️ Open Ticket Quick Setup: Warning") terminal.bold(ansis.yellow("WARNING! ")+ansis.red("By using the 'Quick Setup' feature, your current config will be completely resetted!")) - terminal.gray("\n\nAre you sure you want to continue?") + terminal.gray("\nAre you sure you want to continue?\n") const answer = await terminal.singleColumnMenu([ ansis.green("✅ No, take me back."), @@ -119,11 +124,11 @@ async function renderQuickSetupWelcome(backFn:() => api.ODPromiseVoid){ "Hi there! Thank you for downloading and installing Open Ticket.", "You have chosen to configure the bot using the 'Quick Setup CLI'.", "", - "This program will help you with configuring Open Ticket using a step-by-step method.", - "If you've ever used Google Forms, then this will probably be very easy for you 😉.", + "This tool will help you with configuring Open Ticket using a step-by-step method.", + ansis.gray("You can "+ansis.red.bold("navigate using the arrow-keys")+" and "+ansis.red.bold("go back using ESC")+"."), "", - ansis.magenta("The configuration should normally only take around 5 minutes."), - ansis.magenta("Once you've completed the form, the bot is technically ready for usage!") + ansis.magenta("The configuration should normally only take around 6 minutes."), + ansis.magenta("Once you've completed the form, the bot is ready for usage!") ].join("\n")+"\n\n") const answer = await terminal.singleColumnMenu([ @@ -144,7 +149,7 @@ async function renderQuickSetupWelcome(backFn:() => api.ODPromiseVoid){ async function renderQuickSetupDevPortal(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Discord Bot & Developer Portal") - terminal.bold.blue("(Step 1) Have you already created a Discord bot to use for Open Ticket?\n") + terminal.bold.blue(stepCount(1)+"Have you already created a Discord bot you can use for Open Ticket?\n") const answer = await terminal.singleColumnMenu([ "✅ Yes I have, and it has been invited to the server.", @@ -169,10 +174,10 @@ async function renderQuickSetupDevPortalGuide(variation:0|1,backFn:() => api.ODP renderHeader("⏱️ Open Ticket Quick Setup: Discord Bot & Developer Portal") if (variation == 0){ - terminal.bold.blue("(Step 1.1) You've mentioned that you don't know how to create a Discord bot.\n\n") + terminal.bold.blue(stepCount(1.1)+"You've mentioned that you don't know how to create a Discord bot.\n\n") terminal.gray("Please visit the following URL for a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n")) }else{ - terminal.bold.blue("(Step 1.2) You've mentioned that you've never seen Discord bot before.\n\n") + terminal.bold.blue(stepCount(1.2)+"You've mentioned that you've never seen Discord bot before.\n\n") terminal.gray("How did you even download Open Ticket 🤪? But all jokes aside, we have a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n")) } @@ -217,8 +222,8 @@ async function quickSetupLogin(token:string): Promise async function renderQuickSetupBotToken(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Bot Token") - terminal.bold.blue("(Step 2) Please insert the token of your discord bot.\n") - terminal.gray("It will be safely stored in the 'config/general.json' file.\n\n> ") + terminal.bold.blue(stepCount(2)+"Please insert the token of your discord bot.\n") + terminal.gray("This is used to configure the bot and is then stored securely in the './config/general.json' file.\n\n> ") const answer = await terminal.inputField({ style:terminal.white, @@ -254,7 +259,7 @@ async function renderQuickSetupServer(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Discord Server") - terminal.bold.blue("(Step 3) Please select a Discord Server to use.\n") + terminal.bold.blue(stepCount(3)+"Please select a Discord Server to use.\n") terminal.gray("The bot will only work in this server.\n\n") const guilds = await client.getGuilds() @@ -284,7 +289,7 @@ async function renderQuickSetupAdminRoles(selectedAdmins:string[],backFn:() => a renderHeader("⏱️ Open Ticket Quick Setup: Admin Roles") - terminal.bold.blue("(Step 4) Please select all 'Global Admins' roles to use.\n") + terminal.bold.blue(stepCount(4)+"Please select all 'Global Admins' roles to use.\n") terminal.gray("Users with one of these roles will be able to access & interact with all tickets.\n\n") const roles = cachedRoles ?? (await guild.roles.fetch()).toJSON().sort((a,b) => b.position-a.position) @@ -305,7 +310,7 @@ async function renderQuickSetupAdminRoles(selectedAdmins:string[],backFn:() => a if (answer.selectedIndex == 0) return await renderQuickSetupAdminRoles(selectedAdmins,backFn) if (answer.selectedIndex == 1){ quickSetupStorage.globalAdmins = selectedAdmins - return await renderQuickSetupColorPicker(backFn) + return await renderQuickSetupColorPicker(async () => {await renderQuickSetupAdminRoles(selectedAdmins,backFn,cachedRoles)}) } const adminRole = roles[answer.selectedIndex-2] const index = selectedAdmins.findIndex((r) => r == adminRole.id) @@ -314,15 +319,14 @@ async function renderQuickSetupAdminRoles(selectedAdmins:string[],backFn:() => a return await renderQuickSetupAdminRoles(selectedAdmins,backFn,roles) } -async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid,tryAgain?:boolean){ +async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid){ const {client,guild,globalAdmins} = quickSetupStorage if (!client || !guild || !globalAdmins) return renderHeader("⏱️ Open Ticket Quick Setup: Main Color") - terminal.bold.blue("(Step 5) Please insert a valid hex-color to use in all embeds.\n") - terminal.gray("You can also choose from existing presets. (e.g. red, green, blue, ...)\n\n") - terminal.gray(tryAgain ? ansis.bold.red("Invalid color, please try again!\n")+ansis.gray("> ") : "> ") + terminal.bold.blue(stepCount(5)+"Please insert a valid hex-color to use in all embeds.\n") + terminal.gray("You can also choose from existing presets. (e.g. red, green, blue, ...)\n\n> ") const answer = await terminal.inputField({ style:terminal.white, @@ -335,7 +339,12 @@ async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid,tryAga if (typeof answer != "string") return await backFn() else{ - if (!Array.from(presetColors.keys()).includes(answer) && !/^#[0-9a-f]{6}$/.test(answer)) return await renderQuickSetupColorPicker(backFn,true) + if (!Array.from(presetColors.keys()).includes(answer) && !/^#[0-9a-f]{6}$/.test(answer)){ + terminal.red.bold("\n\n❌ Please insert a valid hex-color or a color from the list. (TIP: use tab for autocomplete)\n") + await utilities.timer(2000) + return await renderQuickSetupColorPicker(backFn) + } + let color: discord.ColorResolvable if (Array.from(presetColors.keys()).includes(answer)){ color = presetColors.get(answer) as number @@ -347,12 +356,11 @@ async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid,tryAga } } -async function renderQuickSetupLanguage(backFn:() => api.ODPromiseVoid,tryAgain?:boolean){ +async function renderQuickSetupLanguage(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Language") - terminal.bold.blue("(Step 6) What language would you like to use in the bot?\n") - terminal.gray("View a list of available languages here: https://otgithub.dj-dj.be#-translators\n\n") - terminal.gray(tryAgain ? ansis.bold.red("Language not found, please try again!\n")+ansis.gray("> ") : "> ") + terminal.bold.blue(stepCount(6)+"What language would you like to use in the bot?\n") + terminal.gray("View a list of available languages here: https://otgithub.dj-dj.be#-translators\n\n> ") const answer = await terminal.inputField({ style:terminal.white, @@ -365,7 +373,12 @@ async function renderQuickSetupLanguage(backFn:() => api.ODPromiseVoid,tryAgain? if (typeof answer != "string") return await backFn() else{ - if (!opendiscord.defaults.getDefault("languageList").includes(answer.toLowerCase())) return await renderQuickSetupLanguage(backFn,true) + if (!opendiscord.defaults.getDefault("languageList").includes(answer.toLowerCase())){ + terminal.red.bold("\n\n❌ Please insert an available language from the list. (TIP: use tab for autocomplete)\n") + await utilities.timer(2000) + return await renderQuickSetupLanguage(backFn) + } + quickSetupStorage.language = answer.toLowerCase() return await renderQuickSetupCommandTypes(async () => {await renderQuickSetupLanguage(backFn)}) } @@ -374,7 +387,7 @@ async function renderQuickSetupLanguage(backFn:() => api.ODPromiseVoid,tryAgain? async function renderQuickSetupCommandTypes(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Command Types") - terminal.bold.blue("(Step 7) Would you like to use slash commands, text commands or both?\n") + terminal.bold.blue(stepCount(7)+"Would you like to use slash commands, text commands or both?\n") terminal.gray("Slash commands are recommended.\n\n") const answer = await terminal.singleColumnMenu([ @@ -407,7 +420,7 @@ async function renderQuickSetupCommandTypes(backFn:() => api.ODPromiseVoid){ async function renderQuickSetupStatusType(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Status Type") - terminal.bold.blue("(Step 8) Please select the type of status you want to use.\n") + terminal.bold.blue(stepCount(8)+"Please select the type of status you want to use.\n") terminal.gray("The status will be shown below the bot name in the userlist.\n\n") const answer = await terminal.singleColumnMenu([ @@ -442,7 +455,7 @@ async function renderQuickSetupStatusText(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Status Text") - terminal.bold.blue("(Step 8.1) What text would you like to display in the status?\n") + terminal.bold.blue(stepCount(8.1)+"What text would you like to display in the status?\n") terminal.gray("This will be appended after the type you have chosen in the previous question.\n\n> ") terminal.gray(status.type == "listening" ? "Listening To " : (status.type == "playing" ? "Playing " : (status.type == "watching" ? "Watching " : ""))) @@ -465,7 +478,7 @@ async function renderQuickSetupLogs(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Channel Logs") - terminal.bold.blue("(Step 9) Please select the 'Text Channel' to use for logs.\n") + terminal.bold.blue(stepCount(9)+"Please select the 'Text Channel' to use for logs.\n") terminal.gray("All logs of the bot will be sent here. Make sure only admins can access this channel.\n\n") const rawChannels = (await guild.channels.fetch()).toJSON().filter((c) => c !== null && c.isTextBased()) @@ -502,7 +515,7 @@ async function renderQuickSetupTicketCategory(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Ticket Category") - terminal.bold.blue("(Step 10) Please select which 'Category' you would like tickets to be created in.\n") + terminal.bold.blue(stepCount(10)+"Please select which 'Category' you would like tickets to be created in.\n") terminal.gray("When no category is selected, tickets will appear at the top of the channel list.\n\n") const rawCategories = (await guild.channels.fetch()).toJSON().filter((c) => c !== null && c.type == discord.ChannelType.GuildCategory) @@ -535,8 +548,8 @@ async function renderQuickSetupTicketCategory(backFn:() => api.ODPromiseVoid){ async function renderQuickSetupTicketCount(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration") - terminal.bold.blue("(Step 11) How many ticket types would you like to create?\n") - terminal.gray("You can always add more ticket types afterwards.\n\n") + terminal.bold.blue(stepCount(11)+"How many ticket options/types would you like to create?\n") + terminal.gray("You can always add more ticket options/types in the config afterwards.\n\n") const answer = await terminal.singleColumnMenu([ "1️⃣ 1 Ticket Option", @@ -568,7 +581,7 @@ async function renderQuickSetupCreateTicketName(ticketIndex:number,requiredTicke renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the name of this ticket option\n") - terminal.gray("Recommendation: Clean, short, obvious name, not more than ±30 characters. \n\n") + terminal.gray("Recommendation: Clean, short, obvious name, not more than ±30 characters.\n\n> ") const answer = await terminal.inputField({ style:terminal.white, @@ -602,7 +615,7 @@ async function renderQuickSetupCreateTicketDescription(ticketIndex:number,requir renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the description of this ticket option\n") - terminal.gray("Recommendation: Use '\\n' (backslash-n) for a newline.\n\n") + terminal.gray("Recommendation: Use '\\n' (backslash-n) for a newline.\n\n> ") const answer = await terminal.inputField({ style:terminal.white, @@ -653,7 +666,7 @@ async function renderQuickSetupCreateTicketButtonEmoji(ticketIndex:number,requir renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the button emoji of this ticket option.\n") - terminal.gray("Only 1 emoji allowed. Tip: Insert custom emoji's via the following syntax: <:12345678910:emoji_name>\n\n") + terminal.gray("Only 1 emoji allowed. Tip: Insert custom emoji's via the following syntax: <:12345678910:emoji_name>\n\n> ") const answer = await terminal.inputField({ style:terminal.white, @@ -709,7 +722,7 @@ async function renderQuickSetupCreateTicketChannelPrefix(ticketIndex:number,requ renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the channel prefix of this ticket option.\n") - terminal.gray("Examples: 'ticket-', 'question-', 'test-channel-', ...\n\n") + terminal.gray("Examples: 'ticket-', 'question-', 'test-channel-', ...\n\n> ") const answer = await terminal.inputField({ style:terminal.white, @@ -765,7 +778,99 @@ async function renderQuickSetupCreateTicketChannelSuffix(ticketIndex:number,requ //create next ticket if (ticketIndex+1 < requiredTickets) return await renderQuickSetupCreateTicketName(ticketIndex+1,requiredTickets,async () => {await renderQuickSetupCreateTicketChannelSuffix(ticketIndex,requiredTickets,backFn)}) - else return await renderQuickSetupLOREMIPSUM(async () => {await renderQuickSetupCreateTicketChannelSuffix(ticketIndex,requiredTickets,backFn)}) + else return await renderQuickSetupAutoclose(async () => {await renderQuickSetupCreateTicketChannelSuffix(ticketIndex,requiredTickets,backFn)}) + } +} + +async function renderQuickSetupAutoclose(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Autoclose") + + terminal.bold.blue(stepCount(12)+"Would you like to enable autoclosing tickets?\n") + terminal.gray("Applies to all created tickets. You can always change/disable autoclose per ticket-option in the config afterwards.\n\n") + + const answer = await terminal.singleColumnMenu([ + ansis.red("❌ "), + "1 Hour Inactivity", + "2 Hours Inactivity", + "4 Hours Inactivity", + "8 Hours Inactivity", + "12 Hours Inactivity", + "1 Day Inactivity", + "2 Days Inactivity", + "3 Days Inactivity", + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + if (answer.selectedIndex == 0) quickSetupStorage.autocloseHours = null + else quickSetupStorage.autocloseHours = [1,2,4,8,12,24,48,72][answer.selectedIndex-1] + return await renderQuickSetupCooldown(async () => {await renderQuickSetupAutoclose(backFn)}) + } +} + +async function renderQuickSetupCooldown(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Cooldown") + + terminal.bold.blue(stepCount(13)+"Would you like to enable ticket creation cooldown?\n") + terminal.gray("Applies to all created tickets. You can always change/disable cooldown per ticket-option in the config afterwards.\n\n") + + const answer = await terminal.singleColumnMenu([ + ansis.red("❌ "), + "1 Minute Cooldown", + "2 Minutes Cooldown", + "5 Minutes Cooldown", + "10 Minutes Cooldown", + "15 Minutes Cooldown", + "30 Minutes Cooldown", + "1 Hour Cooldown", + "2 Hours Cooldown", + "3 Hours Cooldown", + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + if (answer.selectedIndex == 0) quickSetupStorage.cooldownMinutes = null + else quickSetupStorage.cooldownMinutes = [1,2,5,10,15,30,60,120,180][answer.selectedIndex-1] + return await renderQuickSetupCloseParticipants(async () => {await renderQuickSetupCooldown(backFn)}) + } +} + +async function renderQuickSetupCloseParticipants(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Close Configuration") + + terminal.bold.blue(stepCount(14)+"Would you like to remove all ticket participants when closing the ticket?\n") + terminal.gray("When a ticket is closed, only admins can read/write in the ticket. Reopen ticket to restore read/write perms.\n\n") + + const answer = await terminal.singleColumnMenu([ + "❌ No, don't remove ticket participants on close", + "✅ Yes, remove ticket participants on close", + ],{ + leftPadding:"> ", + style:terminal.gray, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + quickSetupStorage.removeParticipantsOnClose = (answer.selectedIndex == 1) + return await renderQuickSetupLOREMIPSUM(async () => {await renderQuickSetupCloseParticipants(backFn)}) } } @@ -776,16 +881,17 @@ async function renderQuickSetupLOREMIPSUM(backFn:() => api.ODPromiseVoid){ } /** Steps Todo - * - S12: Enable Autoclose => select to enable autoclose in all tickets - * - S13: Enable Cooldown => select to enable cooldown in all tickets (with a specific duration from dropdown) + * - S15: Enable Reply on ticket creation + * - S16: Panel Name + * - S17: Panel Description + * - S18: Panel Mode => (dropdown/buttons) + * - S19: Panel Auto Describe Options => (dropdown: disabled, in text, in embed fields, in embed description) + * - S20: Panel Max Tickets Warning + * * - * - S15: Panel Name - * - S16: Panel Description - * - S17: Panel Mode => (dropdown/buttons) - * - S18: Panel Auto Describe Options => (dropdown: disabled, in text, in embed fields, in embed description) - * - S19: Panel Max Tickets Warning * - S20: TODO!! => extra's in general.json "system" => e.g. removeParticipantsOnClose, reply Ticket creation, ... * + * ALREADY FINISHED: * - Ticket configuration => per-ticket configuration * - ticket name * - ticket description From 2cc2ccc68e9326ad8a709c901a2021d883494068 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:45:28 +0200 Subject: [PATCH 35/78] Added ODQuickMessage builders --- src/core/api/modules/builder.ts | 132 ++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 50 deletions(-) diff --git a/src/core/api/modules/builder.ts b/src/core/api/modules/builder.ts index b62ce42..dfb05ea 100644 --- a/src/core/api/modules/builder.ts +++ b/src/core/api/modules/builder.ts @@ -257,15 +257,7 @@ export class ODQuickButton { /**The id of this button. */ id: ODId /**The current data of this button */ - data: Partial = { - customId:"", - mode:"button", - url:null, - color:null, - label:null, - emoji:null, - disabled:false - } + data: Partial constructor(id:ODValidId,data:Partial){ this.id = new ODId(id) @@ -277,12 +269,13 @@ export class ODQuickButton { try { //create the discord.js button const button = new discord.ButtonBuilder() - if (this.data.mode == "button") button.setCustomId(this.data.customId ?? "od:unknown-button") + if (this.data.mode == "button" || (!this.data.mode && this.data.customId)) button.setCustomId(this.data.customId ?? "od:unknown-button") if (this.data.mode == "url") button.setStyle(discord.ButtonStyle.Link) else if (this.data.color == "gray") button.setStyle(discord.ButtonStyle.Secondary) else if (this.data.color == "blue") button.setStyle(discord.ButtonStyle.Primary) else if (this.data.color == "green") button.setStyle(discord.ButtonStyle.Success) else if (this.data.color == "red") button.setStyle(discord.ButtonStyle.Danger) + else button.setStyle(discord.ButtonStyle.Secondary) if (this.data.url) button.setURL(this.data.url) if (this.data.label) button.setLabel(this.data.label) if (this.data.emoji) button.setEmoji(this.data.emoji) @@ -564,21 +557,7 @@ export class ODQuickDropdown { /**The id of this dropdown. */ id: ODId /**The current data of this dropdown */ - data: Partial = { - customId:"", - type:"string", - placeholder:null, - minValues:null, - maxValues:null, - disabled:false, - channelTypes:[], - - options:[], - users:[], - roles:[], - channels:[], - mentionables:[] - } + data: Partial constructor(id:ODValidId,data:Partial){ this.id = new ODId(id) @@ -804,12 +783,7 @@ export class ODQuickFile { /**The id of this file. */ id: ODId /**The current data of this file */ - data: Partial = { - file:"", - name:"file.txt", - description:null, - spoiler:false - } + data: Partial constructor(id:ODValidId,data:Partial){ this.id = new ODId(id) @@ -1060,21 +1034,7 @@ export class ODQuickEmbed { /**The id of this embed. */ id: ODId /**The current data of this embed */ - data: Partial = { - title:null, - color:null, - url:null, - description:null, - authorText:null, - authorImage:null, - authorUrl:null, - footerText:null, - footerImage:null, - image:null, - thumbnail:null, - fields:[], - timestamp:null - } + data: Partial constructor(id:ODValidId,data:Partial){ this.id = new ODId(id) @@ -1186,13 +1146,10 @@ export class ODMessageInstance { content:null, poll:null, ephemeral:false, - embeds:[], components:[], files:[], - - additionalOptions:{ - } + additionalOptions:{} } /**Set the content of this message */ @@ -1354,6 +1311,81 @@ export class ODMessage extends ODBuilderImplementa } } +/**## ODQuickMessage `class` + * This is an Open Ticket quick message builder. + * + * With this class, you can quickly create a message to send in a discord channel. + * This quick message can be used by Open Ticket plugins instead of the normal builders to speed up the process! + * + * Because of the quick functionality, these messages are less customisable by other plugins. + */ +export class ODQuickMessage { + /**The id of this message. */ + id: ODId + /**The current data of this message. */ + data: Partial + + constructor(id:ODValidId,data:Partial){ + this.id = new ODId(id) + this.data = data + } + + /**Build this message & compile it for discord.js */ + async build(): Promise { + //create the discord.js message + const componentArray: discord.ActionRowBuilder[] = [] + let currentRow: discord.ActionRowBuilder = new discord.ActionRowBuilder() + this.data.components?.forEach((c) => { + //return when component crashed + if (c.component == null) return + else if (c.component == "\n"){ + //create new current row when required + if (currentRow.components.length > 0){ + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + } + }else if (c.component instanceof discord.BaseSelectMenuBuilder){ + //push current row when not empty + if (currentRow.components.length > 0){ + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + } + currentRow.addComponents(c.component) + //create new current row after dropdown + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + }else{ + //push button to current row + currentRow.addComponents(c.component) + } + + //create new row when 5 rows in length + if (currentRow.components.length == 5){ + componentArray.push(currentRow) + currentRow = new discord.ActionRowBuilder() + } + }) + //push final row to array + if (currentRow.components.length > 0) componentArray.push(currentRow) + + const filteredEmbeds = (this.data.embeds?.map((e) => e.embed).filter((e) => e instanceof discord.EmbedBuilder) as discord.EmbedBuilder[]) ?? [] + const filteredFiles = (this.data.files?.map((f) => f.file).filter((f) => f instanceof discord.AttachmentBuilder) as discord.AttachmentBuilder[]) ?? [] + + const message : discord.MessageCreateOptions = { + content:this.data.content ?? "", + poll:this.data.poll ?? undefined, + embeds:filteredEmbeds, + components:componentArray, + files:filteredFiles + } + + let result = {id:this.id,message,ephemeral:this.data.ephemeral ?? false} + + Object.assign(result.message,this.data.additionalOptions) + return result + } +} + /**## ODModalManager `class` * This is an Open Ticket modal manager. * From 51c4ae0229e2e3181c4c5850e1193275aae3fb49 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:10:09 +0200 Subject: [PATCH 36/78] Finished Interactive Update CLI Questions Added all remaining questions including panel configuration, ticket message layout, ticket limits, emoji style & more. Also fixed some smaller bugs. --- src/core/cli/quickSetup.ts | 429 +++++++++++++++++++++++++++++-------- 1 file changed, 345 insertions(+), 84 deletions(-) diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 5ebc9d9..3b21d14 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -26,56 +26,65 @@ interface ODQuickSetupVariables { }|null)[], autocloseHours?:number|null, cooldownMinutes?:number|null, - removeParticipantsOnClose?:boolean + globalUserLimit?:number|null, + removeParticipantsOnClose?:boolean, + ticketMessageLayout?:"embed"|"text"|null, + emojiStyle?:api.ODJsonConfig_DefaultSystem["emojiStyle"], + panelName?:string, + panelDescription?:string, + panelDropdown?:boolean, + panelLayout?:"embed"|"text", + panelDescribeOptions?:"simple"|"normal"|"detailed"|null, + panelMaxTicketsWarning?:boolean, } -const stepCount = (count:number) => "(Step "+count+"/20) " +const stepCount = (count:number) => "(Step "+count+"/24) " const quickSetupStorage: ODQuickSetupVariables = {ticketOptions:[]} const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, selectedStyle:terminal.bgBlue.white } -const presetColors = new Map([ - ["dark red",discord.Colors.DarkRed], - ["red",0xff0000], - ["light red",0xf06c6c], - ["dark orange",0xed510e], - ["orange",0xed6f0e], - ["light orange",0xf0b06c], - ["openticket",0xf8ba00], - ["dark yellow",0xdeb100], - ["yellow",0xffff00], - ["light yellow",0xffff8c], - ["banana",0xffe896], - ["lime",0xa8e312], - ["dark green",0x009600], - ["green",0x00ff00], - ["light green",0x76f266], - ["dark cyan",0x00abab], - ["cyan",0x00ffff], - ["light cyan",0x63ffff], - ["aquamarine",0x7fffd4], - ["dark skyblue",0x006bc9], - ["skyblue",0x0095ff], - ["light skyblue",0x40bfff], - ["dark blue",0x00006e], - ["blue",0x0000ff], - ["light blue",0x5353fc], - ["blurple",0x5865F2], - ["dark purple",0x3f009e], - ["purple",0x8000ff], - ["light purple",0x9257eb], - ["dark pink",0xb82ab0], - ["pink",0xff6bf8], - ["light pink",0xff9cfa], - ["magenta",0xff00ff], - ["black",0x000000], - ["brown",0x806050], - ["dark gray",0x4f4f4f], - ["gray",0x808080], - ["light gray",0xb3b3b3], - ["white",0xffffff], - ["invisible",0x393A41] +const presetColors = new Map([ + ["dark red","#992d22"], + ["red","#ff0000"], + ["light red","#f06c6c"], + ["dark orange","#ed510e"], + ["orange","#ed6f0e"], + ["light orange","#f0b06c"], + ["openticket","#f8ba00"], + ["dark yellow","#deb100"], + ["yellow","#ffff00"], + ["light yellow","#ffff8c"], + ["banana","#ffe896"], + ["lime","#a8e312"], + ["dark green","#009600"], + ["green","#00ff00"], + ["light green","#76f266"], + ["dark cyan","#00abab"], + ["cyan","#00ffff"], + ["light cyan","#63ffff"], + ["aquamarine","#7fffd4"], + ["dark skyblue","#006bc9"], + ["skyblue","#0095ff"], + ["light skyblue","#40bfff"], + ["dark blue","#00006e"], + ["blue","#0000ff"], + ["light blue","#5353fc"], + ["blurple","#5865F2"], + ["dark purple","#3f009e"], + ["purple","#8000ff"], + ["light purple","#9257eb"], + ["dark pink","#b82ab0"], + ["pink","#ff6bf8"], + ["light pink","#ff9cfa"], + ["magenta","#ff00ff"], + ["black","#000000"], + ["brown","#806050"], + ["dark gray","#4f4f4f"], + ["gray","#808080"], + ["light gray","#b3b3b3"], + ["white","#ffffff"], + ["invisible","#393A41"] ]) export async function renderQuickSetup(backFn:() => api.ODPromiseVoid){ @@ -157,7 +166,7 @@ async function renderQuickSetupDevPortal(backFn:() => api.ODPromiseVoid){ "👶 I've never seen a Discord bot before.", ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -347,7 +356,7 @@ async function renderQuickSetupColorPicker(backFn:() => api.ODPromiseVoid){ let color: discord.ColorResolvable if (Array.from(presetColors.keys()).includes(answer)){ - color = presetColors.get(answer) as number + color = presetColors.get(answer) as `#${string}` }else{ color = answer as `#${string}` } @@ -396,7 +405,7 @@ async function renderQuickSetupCommandTypes(backFn:() => api.ODPromiseVoid){ "Use Both Slash & Text Commands", ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -431,7 +440,7 @@ async function renderQuickSetupStatusType(backFn:() => api.ODPromiseVoid){ "Playing ..." ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -559,7 +568,7 @@ async function renderQuickSetupTicketCount(backFn:() => api.ODPromiseVoid){ "5️⃣ 5 Ticket Options", ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -580,7 +589,7 @@ async function renderQuickSetupTicketCount(backFn:() => api.ODPromiseVoid){ async function renderQuickSetupCreateTicketName(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") - terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the name of this ticket option\n") + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the name of this ticket option.\n") terminal.gray("Recommendation: Clean, short, obvious name, not more than ±30 characters.\n\n> ") const answer = await terminal.inputField({ @@ -614,7 +623,7 @@ async function renderQuickSetupCreateTicketName(ticketIndex:number,requiredTicke async function renderQuickSetupCreateTicketDescription(ticketIndex:number,requiredTickets:number,backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Ticket Configuration (Ticket "+(ticketIndex+1)+"/"+requiredTickets+")") - terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the description of this ticket option\n") + terminal.bold.blue("("+utilities.ordinalNumber(ticketIndex+1)+" Ticket) Please insert the description of this ticket option.\n") terminal.gray("Recommendation: Use '\\n' (backslash-n) for a newline.\n\n> ") const answer = await terminal.inputField({ @@ -643,7 +652,7 @@ async function renderQuickSetupCreateTicketButtonType(ticketIndex:number,require "Emoji Only" ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -703,7 +712,7 @@ async function renderQuickSetupCreateTicketButtonColor(ticketIndex:number,requir "Green" ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -749,15 +758,15 @@ async function renderQuickSetupCreateTicketChannelSuffix(ticketIndex:number,requ terminal.gray("The suffix is appended after the prefix and will be generated on ticket creation.\n\n") const answer = await terminal.singleColumnMenu([ - "Username (e.g. #ticket-DJj123dj, #question-wumpus)", - "User Id (e.g. #ticket-123456789, #question-01020304)", - "Random Number (e.g. #ticket-1234, #question-1411)", - "Random Hex (e.g. #ticket-f8ba, #question-01f3)", - "Dynamic Counter (e.g. #ticket-1, #question-23)", - "Fixed Counter (e.g. #ticket-0001, #question-0023)" + "Username "+ansis.gray("(e.g. #ticket-DJj123dj, #question-wumpus)"), + "User Id "+ansis.gray("(e.g. #ticket-123456789, #question-01020304)"), + "Random Number "+ansis.gray("(e.g. #ticket-1234, #question-1411)"), + "Random Hex "+ansis.gray("(e.g. #ticket-f8ba, #question-01f3)"), + "Dynamic Counter "+ansis.gray("(e.g. #ticket-1, #question-23)"), + "Fixed Counter "+ansis.gray("(e.g. #ticket-0001, #question-0023)") ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -800,7 +809,7 @@ async function renderQuickSetupAutoclose(backFn:() => api.ODPromiseVoid){ "3 Days Inactivity", ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -834,7 +843,7 @@ async function renderQuickSetupCooldown(backFn:() => api.ODPromiseVoid){ "3 Hours Cooldown", ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -845,14 +854,45 @@ async function renderQuickSetupCooldown(backFn:() => api.ODPromiseVoid){ else{ if (answer.selectedIndex == 0) quickSetupStorage.cooldownMinutes = null else quickSetupStorage.cooldownMinutes = [1,2,5,10,15,30,60,120,180][answer.selectedIndex-1] - return await renderQuickSetupCloseParticipants(async () => {await renderQuickSetupCooldown(backFn)}) + return await renderQuickSetupLimits(async () => {await renderQuickSetupCooldown(backFn)}) + } +} + +async function renderQuickSetupLimits(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Limits") + + terminal.bold.blue(stepCount(14)+"Would you like to enable user ticket creation limits?\n") + terminal.gray("Applies to all created tickets. You can always change/disable limits globally or per ticket-option in the config afterwards.\n\n") + + const answer = await terminal.singleColumnMenu([ + ansis.red("❌ "), + "Max 1 Ticket/Person", + "Max 2 Tickets/Person", + "Max 3 Tickets/Person", + "Max 5 Tickets/Person", + "Max 10 Tickets/Person", + "Max 20 Tickets/Person" + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + if (answer.selectedIndex == 0) quickSetupStorage.globalUserLimit = null + else quickSetupStorage.globalUserLimit = [1,2,3,5,10,20][answer.selectedIndex-1] + return await renderQuickSetupCloseParticipants(async () => {await renderQuickSetupLimits(backFn)}) } } async function renderQuickSetupCloseParticipants(backFn:() => api.ODPromiseVoid){ renderHeader("⏱️ Open Ticket Quick Setup: Ticket Close Configuration") - terminal.bold.blue(stepCount(14)+"Would you like to remove all ticket participants when closing the ticket?\n") + terminal.bold.blue(stepCount(15)+"Would you like to remove all ticket participants when closing the ticket?\n") terminal.gray("When a ticket is closed, only admins can read/write in the ticket. Reopen ticket to restore read/write perms.\n\n") const answer = await terminal.singleColumnMenu([ @@ -860,7 +900,7 @@ async function renderQuickSetupCloseParticipants(backFn:() => api.ODPromiseVoid) "✅ Yes, remove ticket participants on close", ],{ leftPadding:"> ", - style:terminal.gray, + style:terminal.cyan, selectedStyle:terminal.bgDefaultColor.bold, submittedStyle:terminal.bgBlue, extraLines:2, @@ -870,35 +910,256 @@ async function renderQuickSetupCloseParticipants(backFn:() => api.ODPromiseVoid) if (answer.canceled) return await backFn() else{ quickSetupStorage.removeParticipantsOnClose = (answer.selectedIndex == 1) - return await renderQuickSetupLOREMIPSUM(async () => {await renderQuickSetupCloseParticipants(backFn)}) + return await renderQuickSetupTicketMessageLayout(async () => {await renderQuickSetupCloseParticipants(backFn)}) } } -async function renderQuickSetupLOREMIPSUM(backFn:() => api.ODPromiseVoid){ - renderHeader("⏱️ Open Ticket Quick Setup: LOREMIPSUM") +async function renderQuickSetupTicketMessageLayout(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Message Configuration") - console.log("todo",quickSetupStorage) + terminal.bold.blue(stepCount(16)+"How would you like the (initial) ticket message to be displayed?\n") + terminal.gray("This message is sent by the bot when creating a ticket and contains buttons like closing, claiming & deleting.\n\n") + + const answer = await terminal.singleColumnMenu([ + "📋 Embed Message "+ansis.gray("(Default)"), + "💬 Raw Text Message", + ansis.red("❌ No Message (Not Recommended)") + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + quickSetupStorage.ticketMessageLayout = (answer.selectedIndex == 0) ? "embed" : (answer.selectedIndex == 1) ? "text" : null + return await renderQuickSetupEmojiStyle(async () => {await renderQuickSetupTicketMessageLayout(backFn)}) + } +} + +async function renderQuickSetupEmojiStyle(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Emoji Style") + + terminal.bold.blue(stepCount(17)+"How would you like emojis to be displayed in messages?\n") + terminal.gray("This will affect emojis in all messages of the bot, but does not apply to buttons & dropdowns.\n\n") + + const answer = await terminal.singleColumnMenu([ + "✅ Before ❌ (Default) "+ansis.gray("(e.g. 🎫 Ticket Created)"), + "❌ After ✅ "+ansis.gray("(e.g. Ticket Created 🎫)"), + "✅ Double ✅ "+ansis.gray("(e.g. 🎫 Ticket Created 🎫)"), + "❌ Disabled ❌ "+ansis.gray("(e.g. Ticket Created)"), + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + quickSetupStorage.emojiStyle = (answer.selectedIndex == 0) ? "before" : (answer.selectedIndex == 1) ? "after" : (answer.selectedIndex == 2) ? "double" : "disabled" + return await renderQuickSetupPanelName(async () => {await renderQuickSetupEmojiStyle(backFn)}) + } +} + +async function renderQuickSetupPanelName(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Panel Name") + + terminal.bold.blue(stepCount(18)+"Please insert the name of the ticket panel.\n") + terminal.gray("This will be shown as the title of the panel message where all tickets are located.\n\n> ") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true + }).promise + + if (typeof answer != "string") return await backFn() + else if (answer.length == 0){ + terminal.red.bold("\n\n❌ Please insert a valid panel name.\n") + await utilities.timer(2000) + return await renderQuickSetupPanelName(backFn) + }else{ + quickSetupStorage.panelName = answer + return await renderQuickSetupPanelDescription(async () => {await renderQuickSetupPanelName(backFn)}) + } +} + +async function renderQuickSetupPanelDescription(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Panel Description") + + terminal.bold.blue(stepCount(19)+"Please insert the description of the ticket panel.\n") + terminal.gray("Shown below the title. Can be used to explain some info/rules about the ticket system.\n\n> ") + + const answer = await terminal.inputField({ + style:terminal.white, + hintStyle:terminal.gray, + cancelable:true + }).promise + + if (typeof answer != "string") return await backFn() + else{ + quickSetupStorage.panelDescription = answer + return await renderQuickSetupPanelDropdown(async () => {await renderQuickSetupPanelDescription(backFn)}) + } +} + +async function renderQuickSetupPanelDropdown(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Panel Mode") + + terminal.bold.blue(stepCount(20)+"Do you want to show the tickets as buttons or a dropdown?\n") + terminal.gray("Dropdown doesn't support colors and cannot contain option types other than 'tickets' (e.g. website/url or reaction roles).\n\n") + + const answer = await terminal.singleColumnMenu([ + "Use Buttons "+ansis.gray("(Recommended with 2 or less ticket options)"), + "Use Dropdown "+ansis.gray("(Recommended with 3 or more ticket options)"), + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + quickSetupStorage.panelDropdown = (answer.selectedIndex == 1) + return await renderQuickSetupPanelLayout(async () => {await renderQuickSetupPanelDropdown(backFn)}) + } +} + +async function renderQuickSetupPanelLayout(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Panel Layout") + + terminal.bold.blue(stepCount(21)+"How would you like the panel message to be displayed?\n") + terminal.gray("Most of the time embeds are used. But for a simpler solution, you can choose the text layout.\n\n") + + const answer = await terminal.singleColumnMenu([ + "📋 Embed Message "+ansis.gray("(Default)"), + "💬 Raw Text Message", + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + quickSetupStorage.panelLayout = (answer.selectedIndex == 0) ? "embed" : "text" + return await renderQuickSetupPanelDescribeOptions(async () => {await renderQuickSetupPanelLayout(backFn)}) + } +} + +async function renderQuickSetupPanelDescribeOptions(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Panel Option Descriptions") + + terminal.bold.blue(stepCount(22)+"Would you like the panel to have auto-generated (ticket-)option descriptions?\n") + terminal.gray("It will use the 'name' & 'description' of each ticket option and displays it below the panel description.\n\n") + + const answer = await terminal.singleColumnMenu([ + ansis.red("❌ "), + "🟢 Use Simple Option Descriptions", + "🟠 Use Normal Option Descriptions "+ansis.gray("(Default)"), + "🔴 Use Detailed Option Descriptions", + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + quickSetupStorage.panelDescribeOptions = (answer.selectedIndex == 0) ? null : (answer.selectedIndex == 1) ? "simple" : (answer.selectedIndex == 2) ? "normal" : "detailed" + return await renderQuickSetupPanelMaxTicketsWarning(async () => {await renderQuickSetupPanelDescribeOptions(backFn)}) + } +} + +async function renderQuickSetupPanelMaxTicketsWarning(backFn:() => api.ODPromiseVoid){ + renderHeader("⏱️ Open Ticket Quick Setup: Ticket Close Configuration") + + terminal.bold.blue(stepCount(23)+"Would you like to show the maximum amount of tickets a user can create in the panel?\n") + terminal.gray("This will show the amount of tickets a user can create at the same time when limits are enabled.\n\n") + + const answer = await terminal.singleColumnMenu([ + "❌ No, don't show the max tickets warning in the panel", + "✅ Yes, show the max tickets warning in the panel", + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + else{ + quickSetupStorage.panelMaxTicketsWarning = (answer.selectedIndex == 1) + return await renderQuickSetupReady(async () => {await renderQuickSetupPanelMaxTicketsWarning(backFn)}) + } +} + +async function renderQuickSetupReady(backFn:() => api.ODPromiseVoid){ + renderHeader("😎 Open Ticket Quick Setup: Overview") + + terminal.bold.blue(stepCount(24)+"This is the overview of your ticket bot configuration!\n") + terminal.gray("Press 'Enter' to save the result to the config.\n\n") + + const commands = ((quickSetupStorage.slashCommands && quickSetupStorage.textCommands) ? "Slash & Text" : (quickSetupStorage.slashCommands) ? "Slash Only" : "Text Only") + const statusText = (quickSetupStorage.status?.type == "listening" ? "Listening To " : (quickSetupStorage.status?.type == "playing" ? "Playing " : (quickSetupStorage.status?.type == "watching" ? "Watching " : ""))) + quickSetupStorage.status?.text + const status = (quickSetupStorage.status?.enabled) ? statusText : "Disabled" + + + terminal([ + ansis.bold.hex("#f8ba00")("Client: ")+(quickSetupStorage.client?.client.user.displayName ?? "?"), + ansis.bold.hex("#f8ba00")("Status: ")+status, + ansis.bold.hex("#f8ba00")("Server: ")+(quickSetupStorage.guild?.name ?? "?"), + ansis.bold.hex("#f8ba00")("Admins: ")+(quickSetupStorage.globalAdmins?.length ?? "?")+" Admins", + ansis.bold.hex("#f8ba00")("Color: ")+(quickSetupStorage.mainColor ?? "?"), + ansis.bold.hex("#f8ba00")("Language: ")+(quickSetupStorage.language ?? "?"), + ansis.bold.hex("#f8ba00")("Commands: ")+commands, + ansis.bold.hex("#f8ba00")("Options: ")+quickSetupStorage.ticketOptions.length+" Tickets", + ansis.bold.hex("#f8ba00")("Autoclose: ")+(quickSetupStorage.autocloseHours ? quickSetupStorage.autocloseHours+" Hours" : "Disabled"), + ansis.bold.hex("#f8ba00")("Cooldown: ")+(quickSetupStorage.cooldownMinutes ? quickSetupStorage.cooldownMinutes+" Minutes" : "Disabled"), + ansis.bold.hex("#f8ba00")("Limits: ")+(quickSetupStorage.globalUserLimit ? quickSetupStorage.globalUserLimit+" Tickets/Person" : "Disabled"), + ansis.bold.gray("+ 13 More Settings ..."), + ].join("\n")+"\n\n") + + const answer = await terminal.singleColumnMenu([ + ansis.green("Press 'Enter' to save configuration!") + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + //save configuration + if (answer.canceled) return await backFn() + else if (answer.selectedIndex == 0) return await saveQuickSetupConfig() +} + +async function saveQuickSetupConfig(){ + console.log(quickSetupStorage) } /** Steps Todo - * - S15: Enable Reply on ticket creation - * - S16: Panel Name - * - S17: Panel Description - * - S18: Panel Mode => (dropdown/buttons) - * - S19: Panel Auto Describe Options => (dropdown: disabled, in text, in embed fields, in embed description) - * - S20: Panel Max Tickets Warning - * - * - * - S20: TODO!! => extra's in general.json "system" => e.g. removeParticipantsOnClose, reply Ticket creation, ... - * * ALREADY FINISHED: * - Ticket configuration => per-ticket configuration - * - ticket name - * - ticket description - * - button type => (dropdown: label+emoji, label-only or emoji-only) - * - button emoji - * - ticket prefix - * - ticket suffix (dropdown) * - (option ID autogenerated from name BE AWARE OF TICKETS WITH SAME NAME!! + remove unicode, spaces & special chars from id) * - (embed autofilled with name+desc+color, thumbnail will automatically be set to the server icon) * - (ping will be @here) From 160f89c7cf10f4f7204777acc4f238e1f8556231 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:11:00 +0200 Subject: [PATCH 37/78] Improved inline contributor guides Improved the inline contributor guides for registering new languages, config variables & commands. --- src/core/api/defaults/client.ts | 10 ++++ src/core/api/defaults/config.ts | 20 +++++++ src/core/api/defaults/helpmenu.ts | 10 ++++ src/core/api/defaults/language.ts | 83 +++++++++++++++------------- src/data/framework/checkerLoader.ts | 20 +++++++ src/data/framework/commandLoader.ts | 10 ++++ src/data/framework/configLoader.ts | 30 ++++++---- src/data/framework/helpMenuLoader.ts | 10 ++++ src/data/framework/languageLoader.ts | 16 +++--- 9 files changed, 155 insertions(+), 54 deletions(-) diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts index d451afa..f9b8e50 100644 --- a/src/core/api/defaults/client.ts +++ b/src/core/api/defaults/client.ts @@ -4,6 +4,16 @@ import { ODValidId } from "../modules/base" import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, ODTextCommandManager, ODSlashCommandInteractionCallback, ODTextCommandInteractionCallback } from "../modules/client" +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW COMMANDS? + * - Register the command in loadAllSlashCommands() & loadAllTextCommands() in (./src/data/framework/commandLoader.ts) + * - Add autocomplete for the command in OD(Slash/Text)CommandManagerIds_Default in (./src/core/api/defaults/client.ts) + * - Add the command to the help menu in (./src/data/framework/helpMenuLoader.ts) + * - If required, new config variables should be added (incl. logs, dm-logs & permissions). + * - Update the Open Ticket Documentation. + * - If the command contains complex logic or can be executed from a button/dropdown, it should be placed inside an `ODAction`. + * - Check all files, test the bot carefully & try a lot of different scenario's with different settings. + */ + /**## ODClientManager_Default `default_class` * This is a special class that adds type definitions & typescript to the ODClientManager class. * It doesn't add any extra features! diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index afdd836..af308a6 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -7,6 +7,26 @@ import { ODConfigManager, ODConfig, ODJsonConfig } from "../modules/config" import { ODClientActivityStatus, ODClientActivityType } from "../modules/client" import { ODRoleUpdateMode } from "../openticket/role" +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW CONFIG VARIABLES? + * - Make the change to the config file in (./config/) and be aware of the following things: + * - The variable has a clear name and its function is obvious. + * - The variable is in the correct position/category of the config. + * - The variable contains a default placeholder to suggest the contents. + * - If there's a (./devconfig/), also modify this file. + * - Register the config in loadAllConfigs() in (./src/data/framework/configLoader.ts) + * - The variable should be added to the "formatters" in the correct position. + * - Add autocomplete for the variable in ODJsonConfig_Default... in (./src/core/api/defaults/config.ts) + * - Add the variable to the config checker in (./src/data/framework/checkerLoader.ts) + * - Make sure the variable is compatible with the Interactive Setup CLI. + * - The variable should be added by the migration manager (./src/core/startup/migration.ts) when missing. + * - Update the Open Ticket Documentation. + * + * IF VARIABLE IS FROM questions.json, options.json OR panels.json: + * - Check (./src/data/openticket/...) for loading/unloading of data. + * - Check (./src/actions/createTicket.ts) and related files. + * - Check (./src/builders), (./src/actions), (./src/data) & (./src/commands) in general in the areas that were changed. + */ + /**## ODConfigManagerIds_Default `interface` * This interface is a list of ids available in the `ODConfigManager_Default` class. * It's used to generate typescript declarations for this class. diff --git a/src/core/api/defaults/helpmenu.ts b/src/core/api/defaults/helpmenu.ts index a8a986d..d8a3e84 100644 --- a/src/core/api/defaults/helpmenu.ts +++ b/src/core/api/defaults/helpmenu.ts @@ -4,6 +4,16 @@ import { ODValidId } from "../modules/base" import { ODHelpMenuCategory, ODHelpMenuCommandComponent, ODHelpMenuComponent, ODHelpMenuManager } from "../modules/helpmenu" +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW COMMANDS? + * - Register the command in loadAllSlashCommands() & loadAllTextCommands() in (./src/data/framework/commandLoader.ts) + * - Add autocomplete for the command in OD(Slash/Text)CommandManagerIds_Default in (./src/core/api/defaults/client.ts) + * - Add the command to the help menu in (./src/data/framework/helpMenuLoader.ts) + * - If required, new config variables should be added (incl. logs, dm-logs & permissions). + * - Update the Open Ticket Documentation. + * - If the command contains complex logic or can be executed from a button/dropdown, it should be placed inside an `ODAction`. + * - Check all files, test the bot carefully & try a lot of different scenario's with different settings. + */ + /**## ODHelpMenuManagerIds_Default `interface` * This interface is a list of ids available in the `ODHelpMenuManager_Default` class. * It's used to generate typescript declarations for this class. diff --git a/src/core/api/defaults/language.ts b/src/core/api/defaults/language.ts index 0ec7b7e..b8c93d0 100644 --- a/src/core/api/defaults/language.ts +++ b/src/core/api/defaults/language.ts @@ -4,6 +4,52 @@ import { ODValidId } from "../modules/base" import { ODLanguageManager, ODLanguage } from "../modules/language" +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW LANGUAGES? + * - Add the file to (./languages/) and make sure the metadata is valid. + * - Register the language in loadAllLanguages() in (./src/data/framework/languageLoader.ts). + * - Add autocomplete for the language in ODLanguageManagerIds_Default in (./src/core/api/defaults/language.ts). + * - Update the language list in the README.md translator list. + * - Update the 2 language counters in the README.md features list. + * - Update the Open Ticket Documentation. + */ + +/**## ODLanguageManagerIds_Default `interface` + * This interface is a list of ids available in the `ODLanguageManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODLanguageManagerIds_Default { + "opendiscord:custom":ODLanguage, + "opendiscord:english":ODLanguage, + "opendiscord:dutch":ODLanguage, + "opendiscord:portuguese":ODLanguage, + "opendiscord:czech":ODLanguage, + "opendiscord:german":ODLanguage, + "opendiscord:catalan":ODLanguage, + "opendiscord:hungarian":ODLanguage, + "opendiscord:spanish":ODLanguage, + "opendiscord:romanian":ODLanguage, + "opendiscord:ukrainian":ODLanguage, + "opendiscord:indonesian":ODLanguage, + "opendiscord:italian":ODLanguage, + "opendiscord:estonian":ODLanguage, + "opendiscord:finnish":ODLanguage, + "opendiscord:danish":ODLanguage, + "opendiscord:thai":ODLanguage, + "opendiscord:turkish":ODLanguage, + "opendiscord:french":ODLanguage, + "opendiscord:arabic":ODLanguage, + "opendiscord:hindi":ODLanguage, + "opendiscord:lithuanian":ODLanguage, + "opendiscord:polish":ODLanguage, + "opendiscord:latvian":ODLanguage, + "opendiscord:norwegian":ODLanguage, + "opendiscord:russian":ODLanguage, + "opendiscord:swedish":ODLanguage, + "opendiscord:vietnamese":ODLanguage, + "opendiscord:persian":ODLanguage, + //ADD NEW LANGUAGES HERE!!! +} + /**## ODLanguageManagerTranslations_Default `type` * This interface is a list of ids available in the `ODLanguageManager_Default` class. * It's used to generate typescript declarations for this class. @@ -439,43 +485,6 @@ export type ODLanguageManagerTranslations_Default = ( "stats.properties.transcriptsCreated" ) -/**## ODLanguageManagerIds_Default `interface` - * This interface is a list of ids available in the `ODLanguageManager_Default` class. - * It's used to generate typescript declarations for this class. - */ -export interface ODLanguageManagerIds_Default { - "opendiscord:custom":ODLanguage, - "opendiscord:english":ODLanguage, - "opendiscord:dutch":ODLanguage, - "opendiscord:portuguese":ODLanguage, - "opendiscord:czech":ODLanguage, - "opendiscord:german":ODLanguage, - "opendiscord:catalan":ODLanguage, - "opendiscord:hungarian":ODLanguage, - "opendiscord:spanish":ODLanguage, - "opendiscord:romanian":ODLanguage, - "opendiscord:ukrainian":ODLanguage, - "opendiscord:indonesian":ODLanguage, - "opendiscord:italian":ODLanguage, - "opendiscord:estonian":ODLanguage, - "opendiscord:finnish":ODLanguage, - "opendiscord:danish":ODLanguage, - "opendiscord:thai":ODLanguage, - "opendiscord:turkish":ODLanguage, - "opendiscord:french":ODLanguage, - "opendiscord:arabic":ODLanguage, - "opendiscord:hindi":ODLanguage, - "opendiscord:lithuanian":ODLanguage, - "opendiscord:polish":ODLanguage, - "opendiscord:latvian":ODLanguage, - "opendiscord:norwegian":ODLanguage, - "opendiscord:russian":ODLanguage, - "opendiscord:swedish":ODLanguage, - "opendiscord:vietnamese":ODLanguage, - "opendiscord:persian":ODLanguage, - //ADD NEW LANGUAGES HERE!!! -} - /**## ODLanguageManager_Default `default_class` * This is a special class that adds type definitions & typescript to the ODLanguageManager class. * It doesn't add any extra features! diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 6547293..1698b00 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -2,6 +2,26 @@ import {opendiscord, api, utilities} from "../../index" const generalConfig = opendiscord.configs.get("opendiscord:general") +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW CONFIG VARIABLES? + * - Make the change to the config file in (./config/) and be aware of the following things: + * - The variable has a clear name and its function is obvious. + * - The variable is in the correct position/category of the config. + * - The variable contains a default placeholder to suggest the contents. + * - If there's a (./devconfig/), also modify this file. + * - Register the config in loadAllConfigs() in (./src/data/framework/configLoader.ts) + * - The variable should be added to the "formatters" in the correct position. + * - Add autocomplete for the variable in ODJsonConfig_Default... in (./src/core/api/defaults/config.ts) + * - Add the variable to the config checker in (./src/data/framework/checkerLoader.ts) + * - Make sure the variable is compatible with the Interactive Setup CLI. + * - The variable should be added by the migration manager (./src/core/startup/migration.ts) when missing. + * - Update the Open Ticket Documentation. + * + * IF VARIABLE IS FROM questions.json, options.json OR panels.json: + * - Check (./src/data/openticket/...) for loading/unloading of data. + * - Check (./src/actions/createTicket.ts) and related files. + * - Check (./src/builders), (./src/actions), (./src/data) & (./src/commands) in general in the areas that were changed. + */ + export const loadAllConfigCheckers = async () => { opendiscord.checkers.add(new api.ODChecker("opendiscord:general",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:general"),defaultGeneralStructure,{cliDisplayName:"General Config",cliDisplayDescription:"Configure the bot token, status, colors, permissions & more."})) opendiscord.checkers.add(new api.ODChecker("opendiscord:questions",opendiscord.checkers.storage,2,opendiscord.configs.get("opendiscord:questions"),defaultQuestionsStructure,{cliDisplayName:"Questions Config",cliDisplayDescription:"Create, modify & delete questions which are used in options."})) diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index c62ccf3..a854dbf 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -3,6 +3,16 @@ import * as discord from "discord.js" const lang = opendiscord.languages +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW COMMANDS? + * - Register the command in loadAllSlashCommands() & loadAllTextCommands() in (./src/data/framework/commandLoader.ts) + * - Add autocomplete for the command in OD(Slash/Text)CommandManagerIds_Default in (./src/core/api/defaults/client.ts) + * - Add the command to the help menu in (./src/data/framework/helpMenuLoader.ts) + * - If required, new config variables should be added (incl. logs, dm-logs & permissions). + * - Update the Open Ticket Documentation. + * - If the command contains complex logic or can be executed from a button/dropdown, it should be placed inside an `ODAction`. + * - Check all files, test the bot carefully & try a lot of different scenario's with different settings. + */ + export const loadAllSlashCommands = async () => { const commands = opendiscord.client.slashCommands const generalConfig = opendiscord.configs.get("opendiscord:general") diff --git a/src/data/framework/configLoader.ts b/src/data/framework/configLoader.ts index db85d51..54a0a63 100644 --- a/src/data/framework/configLoader.ts +++ b/src/data/framework/configLoader.ts @@ -1,19 +1,29 @@ import {opendiscord, api, utilities} from "../../index" import * as fjs from "formatted-json-stringify" +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW CONFIG VARIABLES? + * - Make the change to the config file in (./config/) and be aware of the following things: + * - The variable has a clear name and its function is obvious. + * - The variable is in the correct position/category of the config. + * - The variable contains a default placeholder to suggest the contents. + * - If there's a (./devconfig/), also modify this file. + * - Register the config in loadAllConfigs() in (./src/data/framework/configLoader.ts) + * - The variable should be added to the "formatters" in the correct position. + * - Add autocomplete for the variable in ODJsonConfig_Default... in (./src/core/api/defaults/config.ts) + * - Add the variable to the config checker in (./src/data/framework/checkerLoader.ts) + * - Make sure the variable is compatible with the Interactive Setup CLI. + * - The variable should be added by the migration manager (./src/core/startup/migration.ts) when missing. + * - Update the Open Ticket Documentation. + * + * IF VARIABLE IS FROM questions.json, options.json OR panels.json: + * - Check (./src/data/openticket/...) for loading/unloading of data. + * - Check (./src/actions/createTicket.ts) and related files. + * - Check (./src/builders), (./src/actions), (./src/data) & (./src/commands) in general in the areas that were changed. + */ + export const loadAllConfigs = async () => { const devconfigFlag = opendiscord.flags.get("opendiscord:dev-config") const isDevconfig = devconfigFlag ? devconfigFlag.value : false - - /** How to add more config variables? - * - Add the variable to the config files in `./config/` & `./devconfig/`. - * - Add the variable to the config in ./src/core/api/defaults/config.ts (interfaces + types) - * - Add the variable to the config checker in ./src/data/framework/checkerLoader.ts - * - Make sure it's compatible with the Interactive Setup CLI. - * - Make sure the Migration Manager automatically adds the variable when missing. - * - Add the variable to the formatters in this file. - * - Update the documentation reference. - */ opendiscord.configs.add(new api.ODJsonConfig("opendiscord:general","general.json",(isDevconfig) ? "./devconfig/" : "./config/",defaultGeneralFormatter)) opendiscord.configs.add(new api.ODJsonConfig("opendiscord:questions","questions.json",(isDevconfig) ? "./devconfig/" : "./config/",defaultQuestionsFormatter)) diff --git a/src/data/framework/helpMenuLoader.ts b/src/data/framework/helpMenuLoader.ts index 914d1ef..0c23297 100644 --- a/src/data/framework/helpMenuLoader.ts +++ b/src/data/framework/helpMenuLoader.ts @@ -2,6 +2,16 @@ import {opendiscord, api, utilities} from "../../index" const lang = opendiscord.languages +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW COMMANDS? + * - Register the command in loadAllSlashCommands() & loadAllTextCommands() in (./src/data/framework/commandLoader.ts) + * - Add autocomplete for the command in OD(Slash/Text)CommandManagerIds_Default in (./src/core/api/defaults/client.ts) + * - Add the command to the help menu in (./src/data/framework/helpMenuLoader.ts) + * - If required, new config variables should be added (incl. logs, dm-logs & permissions). + * - Update the Open Ticket Documentation. + * - If the command contains complex logic or can be executed from a button/dropdown, it should be placed inside an `ODAction`. + * - Check all files, test the bot carefully & try a lot of different scenario's with different settings. + */ + export const loadAllHelpMenuCategories = async () => { const helpmenu = opendiscord.helpmenu diff --git a/src/data/framework/languageLoader.ts b/src/data/framework/languageLoader.ts index 42b5dd2..64b1cce 100644 --- a/src/data/framework/languageLoader.ts +++ b/src/data/framework/languageLoader.ts @@ -1,5 +1,14 @@ import {opendiscord, api, utilities} from "../../index" +/** (CONTRIBUTOR GUIDE) HOW TO ADD NEW LANGUAGES? + * - Add the file to (./languages/) and make sure the metadata is valid. + * - Register the language in loadAllLanguages() in (./src/data/framework/languageLoader.ts). + * - Add autocomplete for the language in ODLanguageManagerIds_Default in (./src/core/api/defaults/language.ts). + * - Update the language list in the README.md translator list. + * - Update the 2 language counters in the README.md features list. + * - Update the Open Ticket Documentation. + */ + export const loadAllLanguages = async () => { //register languages opendiscord.languages.add(new api.ODJsonLanguage("opendiscord:custom","custom.json")) @@ -31,11 +40,4 @@ export const loadAllLanguages = async () => { opendiscord.languages.add(new api.ODJsonLanguage("opendiscord:swedish","swedish.json")) opendiscord.languages.add(new api.ODJsonLanguage("opendiscord:vietnamese","vietnamese.json")) opendiscord.languages.add(new api.ODJsonLanguage("opendiscord:persian","persian.json")) - - /** How to add more languages? - * - Register the language to the manager (see above) - * - Add the language to the list in the "ODLanguageManagerIds_Default" interface (./src/core/api/defaults/language.ts) - * - Update the language list in the README.md translator list - * - Update the language counter in the README.md features list - */ } \ No newline at end of file From c2e6f781b9a9ed5f71aaf8f1f84acd19e25ebd9e Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 19 Jun 2025 22:43:38 +0200 Subject: [PATCH 38/78] Finished Quick Setup CLI --- src/core/api/defaults/config.ts | 114 +++++----- src/core/api/modules/builder.ts | 6 +- src/core/cli/quickSetup.ts | 378 +++++++++++++++++++++++++++++++- 3 files changed, 441 insertions(+), 57 deletions(-) diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index af308a6..008ab00 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -33,9 +33,9 @@ import { ODRoleUpdateMode } from "../openticket/role" */ export interface ODConfigManagerIds_Default { "opendiscord:general":ODJsonConfig_DefaultGeneral, + "opendiscord:questions":ODJsonConfig_DefaultQuestions, "opendiscord:options":ODJsonConfig_DefaultOptions, "opendiscord:panels":ODJsonConfig_DefaultPanels, - "opendiscord:questions":ODJsonConfig_DefaultQuestions, "opendiscord:transcripts":ODJsonConfig_DefaultTranscripts } @@ -239,7 +239,7 @@ export interface ODJsonConfig_DefaultGeneralData { tokenFromENV:boolean, /**The main (hex) color used in almost every embed in the bot. */ - mainColor:discord.ColorResolvable, + mainColor:discord.ColorResolvable|string, /**The language to use. Can be the id of the language or the id without the prefix when using `opendiscord:...`. */ language:string, /**The prefix used in all text-commands. */ @@ -315,7 +315,7 @@ export interface ODJsonConfig_DefaultOptionEmbedSettingsType { /**The description of this embed. */ description:string, /**A custom color for this embed. (The default bot color is used when empty) */ - customColor:discord.ColorResolvable, + customColor:discord.ColorResolvable|string, /**A URL to an image displayed in the embed. */ image:string, @@ -471,6 +471,11 @@ export interface ODJsonConfig_DefaultOptionRoleType extends ODJsonConfig_Default addOnMemberJoin:boolean } +/**## ODJsonConfig_DefaultOptionsData `type` + * All contents of the `options.json` config file. + */ +export type ODJsonConfig_DefaultOptionsData = (ODJsonConfig_DefaultOptionTicketType|ODJsonConfig_DefaultOptionWebsiteType|ODJsonConfig_DefaultOptionRoleType)[] + /**## ODJsonConfig_DefaultOptions `default_class` * This is a special class that adds type definitions & typescript to the ODJsonConfig class. * It doesn't add any extra features! @@ -478,11 +483,7 @@ export interface ODJsonConfig_DefaultOptionRoleType extends ODJsonConfig_Default * This default class is made for the `options.json` config! */ export class ODJsonConfig_DefaultOptions extends ODJsonConfig { - declare data: ( - ODJsonConfig_DefaultOptionTicketType| - ODJsonConfig_DefaultOptionWebsiteType| - ODJsonConfig_DefaultOptionRoleType - )[] + declare data: ODJsonConfig_DefaultOptionsData } /**## ODJsonConfig_DefaultPanelEmbedSettingsType `interface` @@ -497,7 +498,7 @@ export interface ODJsonConfig_DefaultPanelEmbedSettingsType { description:string, /**A custom color for this embed. (The default bot color is used when empty) */ - customColor:discord.ColorResolvable, + customColor:discord.ColorResolvable|string, /**An optional URL used in the title of the embed. */ url:string, @@ -565,6 +566,11 @@ export interface ODJsonConfig_DefaultPanelType { settings:ODJsonConfig_DefaultPanelSettingsType } +/**## ODJsonConfig_DefaultPanelsData `type` + * All contents of the `panels.json` config file. + */ +export type ODJsonConfig_DefaultPanelsData = ODJsonConfig_DefaultPanelType[] + /**## ODJsonConfig_DefaultPanels `default_class` * This is a special class that adds type definitions & typescript to the ODJsonConfig class. * It doesn't add any extra features! @@ -572,7 +578,7 @@ export interface ODJsonConfig_DefaultPanelType { * This default class is made for the `panels.json` config! */ export class ODJsonConfig_DefaultPanels extends ODJsonConfig { - declare data: ODJsonConfig_DefaultPanelType[] + declare data: ODJsonConfig_DefaultPanelsData } /**## ODJSonConfig_DefaultQuestionLengthSettings `interface` @@ -625,6 +631,11 @@ export interface ODJsonConfig_DefaultParagraphQuestionType { length:ODJSonConfig_DefaultQuestionLengthSettings } +/**## ODJsonConfig_DefaultQuestionsData `type` + * All contents of the `questions.json` config file. + */ +export type ODJsonConfig_DefaultQuestionsData = (ODJsonConfig_DefaultShortQuestionType|ODJsonConfig_DefaultParagraphQuestionType)[] + /**## ODJsonConfig_DefaultQuestions `default_class` * This is a special class that adds type definitions & typescript to the ODJsonConfig class. * It doesn't add any extra features! @@ -632,10 +643,7 @@ export interface ODJsonConfig_DefaultParagraphQuestionType { * This default class is made for the `questions.json` config! */ export class ODJsonConfig_DefaultQuestions extends ODJsonConfig { - declare data: ( - ODJsonConfig_DefaultShortQuestionType| - ODJsonConfig_DefaultParagraphQuestionType - )[] + declare data: ODJsonConfig_DefaultQuestionsData } /**## ODJsonConfig_DefaultTranscriptsTextLayout `interface` @@ -709,6 +717,47 @@ export interface ODJsonConfig_DefaultTranscriptsHtmlLayout { } } +/**## ODJsonConfig_DefaultTranscriptsData `interface` + * All contents of the `transcripts.json` config file. + */ +export interface ODJsonConfig_DefaultTranscriptsData { + /**All general settings related to transcripts. */ + general:{ + /**Are transcripts enabled? */ + enabled:boolean, + + /**Enable sending the generated transcript in a channel. */ + enableChannel:boolean, + /**Enable sending the generated transcript to the DM of the ticket creator. */ + enableCreatorDM:boolean, + /**Enable sending the generated transcript to the DM of the participants. */ + enableParticipantDM:boolean, + /**Enable sending the generated transcript to the DM of all admins which were active in the ticket. */ + enableActiveAdminDM:boolean, + /**Enable sending the generated transcript to the DM of all admins which were assigned to the ticket. */ + enableEveryAdminDM:boolean, + + /**A discord channel id for the `"enableChannel"` setting. */ + channel:string, + /**Want to use text or HTML transcripts? */ + mode:"html"|"text" + }, + /**All settings related to the embed from the transcripts. (UNIMPLEMENTED!!) */ + embedSettings:{ + /**Unimplemented feature */ + customColor:discord.ColorResolvable|string, + /**Unimplemented feature */ + listAllParticipants:boolean, + /**Unimplemented feature */ + includeTicketStats:boolean + }, + /**The layout of the text transcripts. */ + textTranscriptStyle:ODJsonConfig_DefaultTranscriptsTextLayout, + /**The layout of the HTML transcripts. */ + htmlTranscriptStyle:ODJsonConfig_DefaultTranscriptsHtmlLayout +} + + /**## ODJsonConfig_DefaultTranscripts `default_class` * This is a special class that adds type definitions & typescript to the ODJsonConfig class. * It doesn't add any extra features! @@ -716,40 +765,5 @@ export interface ODJsonConfig_DefaultTranscriptsHtmlLayout { * This default class is made for the `transcripts.json` config! */ export class ODJsonConfig_DefaultTranscripts extends ODJsonConfig { - declare data: { - /**All general settings related to transcripts. */ - general:{ - /**Are transcripts enabled? */ - enabled:boolean, - - /**Enable sending the generated transcript in a channel. */ - enableChannel:boolean, - /**Enable sending the generated transcript to the DM of the ticket creator. */ - enableCreatorDM:boolean, - /**Enable sending the generated transcript to the DM of the participants. */ - enableParticipantDM:boolean, - /**Enable sending the generated transcript to the DM of all admins which were active in the ticket. */ - enableActiveAdminDM:boolean, - /**Enable sending the generated transcript to the DM of all admins which were assigned to the ticket. */ - enableEveryAdminDM:boolean, - - /**A discord channel id for the `"enableChannel"` setting. */ - channel:string, - /**Want to use text or HTML transcripts? */ - mode:"html"|"text" - }, - /**All settings related to the embed from the transcripts. (UNIMPLEMENTED!!) */ - embedSettings:{ - /**Unimplemented feature */ - customColor:discord.ColorResolvable, - /**Unimplemented feature */ - listAllParticipants:boolean, - /**Unimplemented feature */ - includeTicketStats:boolean - }, - /**The layout of the text transcripts. */ - textTranscriptStyle:ODJsonConfig_DefaultTranscriptsTextLayout, - /**The layout of the HTML transcripts. */ - htmlTranscriptStyle:ODJsonConfig_DefaultTranscriptsHtmlLayout - } + declare data: ODJsonConfig_DefaultTranscriptsData } \ No newline at end of file diff --git a/src/core/api/modules/builder.ts b/src/core/api/modules/builder.ts index dfb05ea..f864f2a 100644 --- a/src/core/api/modules/builder.ts +++ b/src/core/api/modules/builder.ts @@ -836,7 +836,7 @@ export interface ODEmbedData { /**The title of the embed */ title:string|null, /**The color of the embed */ - color:discord.ColorResolvable|null, + color:discord.ColorResolvable|string|null, /**The url of the embed */ url:string|null, /**The description of the embed */ @@ -994,7 +994,7 @@ export class ODEmbed extends ODBuilderImplementati //create the discord.js embed const embed = new discord.EmbedBuilder() if (instance.data.title) embed.setTitle(instance.data.title) - if (instance.data.color) embed.setColor(instance.data.color) + if (instance.data.color) embed.setColor(instance.data.color as discord.ColorResolvable) if (instance.data.url) embed.setURL(instance.data.url) if (instance.data.description) embed.setDescription(instance.data.description) if (instance.data.authorText) embed.setAuthor({ @@ -1047,7 +1047,7 @@ export class ODQuickEmbed { //create the discord.js embed const embed = new discord.EmbedBuilder() if (this.data.title) embed.setTitle(this.data.title) - if (this.data.color) embed.setColor(this.data.color) + if (this.data.color) embed.setColor(this.data.color as discord.ColorResolvable) if (this.data.url) embed.setURL(this.data.url) if (this.data.description) embed.setDescription(this.data.description) if (this.data.authorText) embed.setAuthor({ diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 3b21d14..d791b67 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -2,7 +2,15 @@ import {opendiscord, api, utilities} from "../../index" import {Terminal, terminal} from "terminal-kit" import ansis from "ansis" import * as discord from "discord.js" -import {renderHeader} from "./cli" +import crypto from "crypto" +import {renderHeader, terminate} from "./cli" + +function generateUniqueIdFromName(name:string){ + //id only allows a-z, 0-9 & dash characters (& replace spaces with dashes) + const filteredChars = name.replaceAll(" ","-").split("").filter((ch) => /^[a-zA-Z0-9-]{1}$/.test(ch)) + const randomSuffix = "-"+crypto.randomBytes(4).toString("hex") + return filteredChars.join("")+randomSuffix +} interface ODQuickSetupVariables { client?:api.ODClientManager, @@ -24,6 +32,7 @@ interface ODQuickSetupVariables { channelPrefix:string, channelSuffix:api.ODJsonConfig_DefaultOptionTicketChannelType["suffix"] }|null)[], + optionIdStorage:string[], autocloseHours?:number|null, cooldownMinutes?:number|null, globalUserLimit?:number|null, @@ -39,7 +48,7 @@ interface ODQuickSetupVariables { } const stepCount = (count:number) => "(Step "+count+"/24) " -const quickSetupStorage: ODQuickSetupVariables = {ticketOptions:[]} +const quickSetupStorage: ODQuickSetupVariables = {ticketOptions:[],optionIdStorage:[]} const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { style:terminal.white, selectedStyle:terminal.bgBlue.white @@ -1150,11 +1159,372 @@ async function renderQuickSetupReady(backFn:() => api.ODPromiseVoid){ //save configuration if (answer.canceled) return await backFn() - else if (answer.selectedIndex == 0) return await saveQuickSetupConfig() + else if (answer.selectedIndex == 0){ + await saveQuickSetupConfig() + return await renderQuickSetupFinished() + } +} + +async function renderQuickSetupFinished(){ + renderHeader("✅ Open Ticket Quick Setup: Ready") + + terminal.bold.green("The config has been saved succesfully and the bot is now ready for usage!\n") + terminal.gray("Press 'Enter' to exit the Quick Setup CLI.\n\n") + + terminal(ansis.gray([ + "Start the bot using the following command:", + ansis.blue("------------------------------"), + ansis.magenta.bold("> npm start"), + ansis.blue("------------------------------"), + "", + "Edit the existing config in the CLI or directly in the (./config/) directory:", + ansis.blue("------------------------------"), + ansis.magenta.bold("> npm run setup"), + ansis.blue("------------------------------"), + "", + ansis.yellow.bold("⭐ Don't forget to star our Github repository when you enjoy using the bot! ⭐"), + ansis.gray("Join our discord server "+ansis.hex("#5865F2").bold.underline("https://discord.dj-dj.be")+" when you need help with troubleshooting."), + ].join("\n"))+"\n\n") + + await terminal.singleColumnMenu([ + ansis.green("Press 'Enter' to exit Quick Setup CLI!") + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + //stop CLI + return await terminate() } async function saveQuickSetupConfig(){ - console.log(quickSetupStorage) + //GENERAL CONFIG + const generalConfig = opendiscord.configs.get("opendiscord:general") + const generalConfigData: api.ODJsonConfig_DefaultGeneralData = { + _INFO:{ + support:"https://otdocs.dj-dj.be", + discord:"https://discord.dj-dj.be", + version:"open-ticket-v4.0.6" + }, + + token:quickSetupStorage.client?.token ?? "", + tokenFromENV:false, + + mainColor:quickSetupStorage.mainColor ?? "#f8ba00", + language:quickSetupStorage.language ?? "english", + prefix:"!ticket ", + serverId:quickSetupStorage.guild?.id ?? "", + globalAdmins:quickSetupStorage.globalAdmins ?? [], + + slashCommands:quickSetupStorage.slashCommands ?? false, + textCommands:quickSetupStorage.textCommands ?? false, + + status:quickSetupStorage.status ?? {enabled:false,status:"online",text:"",type:"custom"}, + + system:{ + removeParticipantsOnClose:quickSetupStorage.removeParticipantsOnClose ?? false, + replyOnTicketCreation:false, + replyOnReactionRole:true, + useTranslatedConfigChecker:true, + preferSlashOverText:quickSetupStorage.slashCommands ?? false, + sendErrorOnUnknownCommand:true, + questionFieldsInCodeBlock:true, + disableVerifyBars:false, + useRedErrorEmbeds:true, + emojiStyle:quickSetupStorage.emojiStyle ?? "before", + + enableTicketClaimButtons:true, + enableTicketCloseButtons:true, + enableTicketPinButtons:true, + enableTicketDeleteButtons:true, + enableTicketActionWithReason:true, + enableDeleteWithoutTranscript:true, + + logs:{ + enabled:(typeof quickSetupStorage.logChannel == "string"), + channel:quickSetupStorage.logChannel ?? "" + }, + + limits:{ + enabled:(typeof quickSetupStorage.globalUserLimit == "number"), + globalMaximum:100, + userMaximum:quickSetupStorage.globalUserLimit ?? 3 + }, + + permissions:{ + help:"everyone", + panel:"admin", + ticket:"everyone", + close:"admin", + delete:"admin", + reopen:"admin", + claim:"admin", + unclaim:"admin", + pin:"admin", + unpin:"admin", + move:"admin", + rename:"admin", + add:"admin", + remove:"admin", + blacklist:"admin", + stats:"everyone", + clear:"admin", + autoclose:"admin", + autodelete:"admin" + }, + + messages:{ + creation:{dm:true,logs:true}, + closing:{dm:true,logs:true}, + deleting:{dm:true,logs:true}, + reopening:{dm:false,logs:true}, + claiming:{dm:false,logs:true}, + pinning:{dm:false,logs:true}, + adding:{dm:false,logs:true}, + removing:{dm:false,logs:true}, + renaming:{dm:false,logs:true}, + moving:{dm:true,logs:true}, + blacklisting:{dm:true,logs:true}, + roleAdding:{dm:false,logs:true}, + roleRemoving:{dm:false,logs:true} + } + } + } + generalConfig.data = generalConfigData + await generalConfig.save() + + //QUESTIONS CONFIG => no configuration needed (coming soonTM) + const questionsConfig = opendiscord.configs.get("opendiscord:questions") + const questionsConfigData: api.ODJsonConfig_DefaultQuestionsData = [ + { + id:"example-question-1", + name:"Example Question 1", + type:"short", + + required:true, + placeholder:"Insert your short answer here!", + length:{ + enabled:false, + min:0, + max:1000 + } + }, + { + id:"example-question-2", + name:"Example Question 2", + type:"paragraph", + + required:false, + placeholder:"Insert your long answer here!", + length:{ + enabled:false, + min:0, + max:1000 + } + } + ] + questionsConfig.data = questionsConfigData + await questionsConfig.save() + + //OPTIONS CONFIG + const optionsConfig = opendiscord.configs.get("opendiscord:options") + const optionsConfigData: api.ODJsonConfig_DefaultOptionsData = quickSetupStorage.ticketOptions.filter((ticket) => ticket !== null).map((ticket) => { + const id = generateUniqueIdFromName(ticket.name) + quickSetupStorage.optionIdStorage.push(id) + + return { + id:id, + name:ticket.name, + description:ticket.description, + type:"ticket", + + button:{ + emoji:(ticket.buttonType == "label") ? "" : (ticket.buttonEmoji ?? ""), + label:(ticket.buttonType == "emoji") ? "" : ticket.name, + color:ticket.buttonColor ?? "gray" + }, + + ticketAdmins:[], + readonlyAdmins:[], + allowCreationByBlacklistedUsers:false, + questions:[], + + channel:{ + prefix:ticket.channelPrefix, + suffix:ticket.channelSuffix, + category:quickSetupStorage.ticketCategory ?? "", + closedCategory:"", + backupCategory:"", + claimedCategory:[], + description:ticket.description + }, + + dmMessage:{ + enabled:false, + text:"", + embed:{ + enabled:false, + title:"", + description:"", + customColor:"", + + image:"", + thumbnail:"", + fields:[], + timestamp:false + } + }, + ticketMessage:{ + enabled:true, + text:"", + embed:{ + enabled:true, + title:ticket.name, + description:ticket.description, + customColor:"", + + image:"", + thumbnail:"", + fields:[], + timestamp:true + }, + ping:{ + "@here":true, + "@everyone":false, + custom:[] + } + }, + autoclose:{ + enableInactiveHours:(typeof quickSetupStorage.autocloseHours == "number"), + inactiveHours:quickSetupStorage.autocloseHours ?? 24, + enableUserLeave:true, + disableOnClaim:false + }, + autodelete:{ + enableInactiveDays:false, + inactiveDays:7, + enableUserLeave:false, + disableOnClaim:false + }, + cooldown:{ + enabled:(typeof quickSetupStorage.cooldownMinutes == "number"), + cooldownMinutes:quickSetupStorage.cooldownMinutes ?? 10 + }, + limits:{ + enabled:false, + globalMaximum:20, + userMaximum:3 + } + } + }) + optionsConfig.data = optionsConfigData + await optionsConfig.save() + + //PANELS CONFIG + const panelsConfig = opendiscord.configs.get("opendiscord:panels") + const panelsConfigData: api.ODJsonConfig_DefaultPanelsData = [ + { + id:generateUniqueIdFromName(quickSetupStorage.panelName ?? "ticket-panel"), + name:quickSetupStorage.panelName ?? "Ticket Panel", + dropdown:quickSetupStorage.panelDropdown ?? false, + options:quickSetupStorage.optionIdStorage, + + text:(quickSetupStorage.panelLayout == "text") ? (quickSetupStorage.panelDescription ?? "") : "", + embed:{ + enabled:true, + title:quickSetupStorage.panelName ?? "Ticket Panel", + description:(quickSetupStorage.panelLayout == "embed") ? (quickSetupStorage.panelDescription ?? "") : "", + + customColor:"", + url:"", + + image:"", + thumbnail:"", + + footer:quickSetupStorage.guild?.name ?? "", + fields:[], + timestamp:false + }, + settings:{ + dropdownPlaceholder:"Open a ticket", + + enableMaxTicketsWarningInText:(quickSetupStorage.panelLayout == "text" && (quickSetupStorage.panelMaxTicketsWarning ?? false)), + enableMaxTicketsWarningInEmbed:(quickSetupStorage.panelLayout == "embed" && (quickSetupStorage.panelMaxTicketsWarning ?? false)), + + describeOptionsLayout:quickSetupStorage.panelDescribeOptions ?? "normal", + describeOptionsCustomTitle:"", + describeOptionsInText:(quickSetupStorage.panelLayout == "text" && typeof quickSetupStorage.panelDescribeOptions == "string"), + describeOptionsInEmbedFields:(quickSetupStorage.panelLayout == "embed" && typeof quickSetupStorage.panelDescribeOptions == "string"), + describeOptionsInEmbedDescription:false + } + } + ] + panelsConfig.data = panelsConfigData + await panelsConfig.save() + + //TRANSCRIPTS CONFIG => no configuration needed (coming soonTM) + const transcriptsConfig = opendiscord.configs.get("opendiscord:transcripts") + const transcriptsConfigData: api.ODJsonConfig_DefaultTranscriptsData = { + general:{ + enabled:(typeof quickSetupStorage.logChannel == "string"), + + enableChannel:true, + enableCreatorDM:true, + enableParticipantDM:false, + enableActiveAdminDM:false, + enableEveryAdminDM:false, + + channel:quickSetupStorage.logChannel ?? "", + mode:"html" + }, + embedSettings:{ + customColor:"", + listAllParticipants:false, + includeTicketStats:false + }, + textTranscriptStyle:{ + layout:"normal", + includeStats:true, + includeIds:false, + includeEmbeds:true, + includeFiles:true, + includeBotMessages:true, + + fileMode:"channel-name", + customFileName:"transcript" + }, + htmlTranscriptStyle:{ + background:{ + enableCustomBackground:false, + backgroundColor:"", + backgroundImage:"" + }, + header:{ + enableCustomHeader:false, + backgroundColor:"#202225", + decoColor:"#f8ba00", + textColor:"#ffffff" + }, + stats:{ + enableCustomStats:false, + backgroundColor:"#202225", + keyTextColor:"#737373", + valueTextColor:"#ffffff", + hideBackgroundColor:"#40444a", + hideTextColor:"#ffffff" + }, + favicon:{ + enableCustomFavicon:false, + imageUrl:"https://t.dj-dj.be/favicon.png" + } + } + } + transcriptsConfig.data = transcriptsConfigData + await transcriptsConfig.save() } /** Steps Todo From b2f2ef340544c8b965c54adafebd1bf775f7caba Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:59:52 +0200 Subject: [PATCH 39/78] Update quickSetup.ts --- src/core/cli/quickSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index d791b67..7378dad 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -7,7 +7,7 @@ import {renderHeader, terminate} from "./cli" function generateUniqueIdFromName(name:string){ //id only allows a-z, 0-9 & dash characters (& replace spaces with dashes) - const filteredChars = name.replaceAll(" ","-").split("").filter((ch) => /^[a-zA-Z0-9-]{1}$/.test(ch)) + const filteredChars = name.toLowerCase().replaceAll(" ","-").split("").filter((ch) => /^[a-zA-Z0-9-]{1}$/.test(ch)) const randomSuffix = "-"+crypto.randomBytes(4).toString("hex") return filteredChars.join("")+randomSuffix } From 2ea38cd7a52d4c4affe2ad63d09b61af5af75111 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:20:27 +0200 Subject: [PATCH 40/78] (Open Discord) Added support for context menu's --- src/core/api/defaults/client.ts | 47 ++- src/core/api/defaults/event.ts | 8 +- src/core/api/defaults/progressbar.ts | 3 + src/core/api/modules/client.ts | 391 +++++++++++++++++++++++- src/core/api/modules/defaults.ts | 12 + src/data/framework/commandLoader.ts | 9 + src/data/framework/eventLoader.ts | 6 + src/data/framework/progressBarLoader.ts | 9 + src/index.ts | 32 ++ 9 files changed, 513 insertions(+), 4 deletions(-) diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts index f9b8e50..620e4ef 100644 --- a/src/core/api/defaults/client.ts +++ b/src/core/api/defaults/client.ts @@ -2,7 +2,7 @@ //DEFAULT CLIENT MODULE /////////////////////////////////////// import { ODValidId } from "../modules/base" -import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, ODTextCommandManager, ODSlashCommandInteractionCallback, ODTextCommandInteractionCallback } from "../modules/client" +import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, ODTextCommandManager, ODSlashCommandInteractionCallback, ODTextCommandInteractionCallback, ODContextMenu, ODContextMenuManager, ODContextMenuInteractionCallback } from "../modules/client" /** (CONTRIBUTOR GUIDE) HOW TO ADD NEW COMMANDS? * - Register the command in loadAllSlashCommands() & loadAllTextCommands() in (./src/data/framework/commandLoader.ts) @@ -23,6 +23,7 @@ import { ODClientManager, ODSlashCommand, ODTextCommand, ODSlashCommandManager, export class ODClientManager_Default extends ODClientManager { declare slashCommands: ODSlashCommandManager_Default declare textCommands: ODTextCommandManager_Default + declare contextMenus: ODContextMenuManager_Default } /**## ODSlashCommandManagerIds_Default `interface` @@ -152,4 +153,48 @@ export class ODTextCommandManager_Default extends ODTextCommandManager { onInteraction(commandPrefix:string, commandName:string|RegExp, callback:ODTextCommandInteractionCallback): void { return super.onInteraction(commandPrefix,commandName,callback) } +} + +/**## ODContextMenuManagerIds_Default `interface` + * This interface is a list of ids available in the `ODContextMenuManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODContextMenuManagerIds_Default { + "opendiscord:test-menu":ODContextMenu +} + +/**## ODContextMenuManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODContextMenuManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `opendiscord.client.contextMenus`! + */ +export class ODContextMenuManager_Default extends ODContextMenuManager { + get(id:ContextMenuId): ODContextMenuManagerIds_Default[ContextMenuId] + get(id:ODValidId): ODContextMenu|null + + get(id:ODValidId): ODContextMenu|null { + return super.get(id) + } + + remove(id:ContextMenuId): ODContextMenuManagerIds_Default[ContextMenuId] + remove(id:ODValidId): ODContextMenu|null + + remove(id:ODValidId): ODContextMenu|null { + return super.remove(id) + } + + exists(id:keyof ODContextMenuManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + onInteraction(menuName:keyof ODContextMenuManagerIds_Default, callback:ODContextMenuInteractionCallback): void + onInteraction(menuName:string|RegExp, callback:ODContextMenuInteractionCallback): void + + onInteraction(menuName:string|RegExp, callback:ODContextMenuInteractionCallback): void { + return super.onInteraction(menuName,callback) + } } \ No newline at end of file diff --git a/src/core/api/defaults/event.ts b/src/core/api/defaults/event.ts index 1833156..1ea6607 100644 --- a/src/core/api/defaults/event.ts +++ b/src/core/api/defaults/event.ts @@ -19,7 +19,7 @@ import { ODFlagManager_Default } from "./flag" import { ODSessionManager_Default } from "./session" import { ODLanguageManager_Default } from "./language" import { ODCheckerFunctionManager_Default, ODCheckerManager_Default, ODCheckerRenderer_Default, ODCheckerTranslationRegister_Default } from "./checker" -import { ODClientManager_Default, ODSlashCommandManager_Default, ODTextCommandManager_Default } from "./client" +import { ODClientManager_Default, ODContextMenuManager_Default, ODSlashCommandManager_Default, ODTextCommandManager_Default } from "./client" import { ODBuilderManager_Default, ODButtonManager_Default, ODDropdownManager_Default, ODEmbedManager_Default, ODFileManager_Default, ODMessageManager_Default, ODModalManager_Default } from "./builder" import { ODButtonResponderManager_Default, ODCommandResponderManager_Default, ODDropdownResponderManager_Default, ODModalResponderManager_Default, ODResponderManager_Default } from "./responder" import { ODActionManager_Default } from "./action" @@ -128,6 +128,12 @@ export interface ODEventIds_Default { "onSlashCommandRegister": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> "afterSlashCommandsRegistered": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + //client context menus + "onContextMenuLoad": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "afterContextMenusLoaded": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "onContextMenuRegister": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + "afterContextMenusRegistered": ODEvent_Default<(menu:ODContextMenuManager_Default, client:ODClientManager_Default) => ODPromiseVoid> + //client text commands "onTextCommandLoad": ODEvent_Default<(text:ODTextCommandManager_Default, client:ODClientManager_Default,) => ODPromiseVoid> "afterTextCommandsLoaded": ODEvent_Default<(text:ODTextCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> diff --git a/src/core/api/defaults/progressbar.ts b/src/core/api/defaults/progressbar.ts index 2d1b3f1..f353472 100644 --- a/src/core/api/defaults/progressbar.ts +++ b/src/core/api/defaults/progressbar.ts @@ -134,6 +134,9 @@ export interface ODProgressBarManagerIds_Default { "opendiscord:slash-command-remove":ODManualProgressBar, "opendiscord:slash-command-create":ODManualProgressBar, "opendiscord:slash-command-update":ODManualProgressBar, + "opendiscord:context-menu-remove":ODManualProgressBar, + "opendiscord:context-menu-create":ODManualProgressBar, + "opendiscord:context-menu-update":ODManualProgressBar, } /**## ODProgressBarManager_Default `default_class` diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index 9d0ea07..456248a 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -76,12 +76,15 @@ export class ODClientManager { slashCommands: ODSlashCommandManager /**The text command manager is responsible for all text commands & their events inside the bot. */ textCommands: ODTextCommandManager + /**The context menu manager is responsible for all context menus & their events inside the bot. */ + contextMenus: ODContextMenuManager constructor(debug:ODDebugger){ this.#debug = debug this.activity = new ODClientActivityManager(this.#debug,this) this.slashCommands = new ODSlashCommandManager(this.#debug,this) this.textCommands = new ODTextCommandManager(this.#debug,this) + this.contextMenus = new ODContextMenuManager(this.#debug,this) } /**Initiate the `client` variable & add the intents & partials to the bot. */ @@ -145,9 +148,10 @@ export class ODClientManager { this.client.once("ready",async () => { this.ready = true - //set slashCommandManager to client applicationCommandManager - if (!this.client.application) throw new ODSystemError("Couldn't get client application! Unable to register slash commands!") + //set slashCommandManager & contextMenuManager to client applicationCommandManager + if (!this.client.application) throw new ODSystemError("Couldn't get client application for slashCommand & contextMenu managers!") this.slashCommands.commandManager = this.client.application.commands + this.contextMenus.commandManager = this.client.application.commands if (this.readyListener) await this.readyListener() resolve(true) @@ -522,6 +526,9 @@ export interface ODSlashCommandBuilder extends discord.ChatInputApplicationComma contexts:discord.InteractionContextType[] } +/**## ODSlashCommandComparator `class` + * A utility class to compare existing slash commands with newly registered ones. + */ export class ODSlashCommandComparator { /**Convert a `discord.ApplicationCommandOptionChoiceData` to a universal Open Ticket slash command option choice object for comparison. */ #convertOptionChoice(choice:discord.ApplicationCommandOptionChoiceData): ODSlashCommandUniversalOptionChoice { @@ -1788,4 +1795,384 @@ export class ODTextCommandManager extends ODManager { else if (!checkResult.valid && checkResult.reason == "allowspaces_not_last") throw new ODSystemError("Invalid text command '"+data.id.value+"' => string option with 'allowSpaces' is only allowed at the end of a command!") else return super.add(data,overwrite) } +} + +/**## ODContextMenuUniversalMenu `interface` + * A universal template for a context menu. + * + * Why universal? Both **existing context menus** & **unregistered templates** can be converted to this type. + */ +export interface ODContextMenuUniversalMenu { + /**The type of this context menu. (required => `Message`|`User`) */ + type:discord.ApplicationCommandType.Message|discord.ApplicationCommandType.User, + /**The name of this context menu. */ + name:string, + /**All localized names of this context menu. */ + nameLocalizations:readonly ODSlashCommandUniversalTranslation[], + /**The id of the guild this context menu is registered in. */ + guildId:string|null, + /**Is this context menu for 18+ users only? */ + nsfw:boolean, + /**A bitfield of the user permissions required to use this context menu. */ + defaultMemberPermissions:bigint, + /**Is this context menu available in DM? */ + dmPermission:boolean, + /**A list of contexts where you can install this context menu. */ + integrationTypes:readonly discord.ApplicationIntegrationType[], + /**A list of contexts where you can use this context menu. */ + contexts:readonly discord.InteractionContextType[] +} + +/**## ODContextMenuBuilderMessage `interface` + * The builder for message context menus. + */ +export interface ODContextMenuBuilderMessage extends discord.MessageApplicationCommandData { + /**This field is required in Open Ticket for future compatibility. */ + integrationTypes:discord.ApplicationIntegrationType[], + /**This field is required in Open Ticket for future compatibility. */ + contexts:discord.InteractionContextType[] +} + +/**## ODContextMenuBuilderUser `interface` + * The builder for user context menus. + */ +export interface ODContextMenuBuilderUser extends discord.UserApplicationCommandData { + /**This field is required in Open Ticket for future compatibility. */ + integrationTypes:discord.ApplicationIntegrationType[], + /**This field is required in Open Ticket for future compatibility. */ + contexts:discord.InteractionContextType[] +} + +/**## ODContextMenuBuilderUser `interface` + * The builder for context menus. + */ +export type ODContextMenuBuilder = (ODContextMenuBuilderMessage|ODContextMenuBuilderUser) + +/**## ODContextMenuComparator `class` + * A utility class to compare existing context menu's with newly registered ones. + */ +export class ODContextMenuComparator { + /**Convert a `ODContextMenuBuilder` to a universal Open Ticket context menu object for comparison. */ + convertBuilder(builder:ODContextMenuBuilder,guildId:string|null): ODContextMenuUniversalMenu|null { + if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User) return null + const nameLoc = builder.nameLocalizations ?? {} + + return { + type:builder.type, + name:builder.name, + nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}), + guildId:guildId, + nsfw:builder.nsfw ?? false, + defaultMemberPermissions:discord.PermissionsBitField.resolve(builder.defaultMemberPermissions ?? ["ViewChannel"]), + dmPermission:(builder.contexts && builder.contexts.includes(discord.InteractionContextType.BotDM)) ?? false, + integrationTypes:builder.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall], + contexts:builder.contexts ?? [] + } + } + /**Convert a `discord.ApplicationCommand` to a universal Open Ticket context menu object for comparison. */ + convertMenu(cmd:discord.ApplicationCommand): ODContextMenuUniversalMenu|null { + if (cmd.type != discord.ApplicationCommandType.Message && cmd.type != discord.ApplicationCommandType.User) return null + const nameLoc = cmd.nameLocalizations ?? {} + + return { + type:cmd.type, + name:cmd.name, + nameLocalizations:Object.keys(nameLoc).map((key) => {return {language:key as `${discord.Locale}`,value:nameLoc[key]}}), + guildId:cmd.guildId, + nsfw:cmd.nsfw, + defaultMemberPermissions:discord.PermissionsBitField.resolve(cmd.defaultMemberPermissions ?? ["ViewChannel"]), + dmPermission:(cmd.contexts && cmd.contexts.includes(discord.InteractionContextType.BotDM)) ? true : false, + integrationTypes:cmd.integrationTypes ?? [discord.ApplicationIntegrationType.GuildInstall], + contexts:cmd.contexts ?? [] + } + } + /**Returns `true` when the 2 context menus are the same. */ + compare(ctxA:ODContextMenuUniversalMenu,ctxB:ODContextMenuUniversalMenu): boolean { + if (ctxA.name != ctxB.name) return false + if (ctxA.type != ctxB.type) return false + if (ctxA.nsfw != ctxB.nsfw) return false + if (ctxA.guildId != ctxB.guildId) return false + if (ctxA.dmPermission != ctxB.dmPermission) return false + if (ctxA.defaultMemberPermissions != ctxB.defaultMemberPermissions) return false + + //nameLocalizations + if (ctxA.nameLocalizations.length != ctxB.nameLocalizations.length) return false + if (!ctxA.nameLocalizations.every((nameA) => { + const nameB = ctxB.nameLocalizations.find((nameB) => nameB.language == nameA.language) + if (!nameB || nameA.value != nameB.value) return false + else return true + })) return false + + //contexts + if (ctxA.contexts.length != ctxB.contexts.length) return false + if (!ctxA.contexts.every((contextA) => { + return ctxB.contexts.includes(contextA) + })) return false + + //integrationTypes + if (ctxA.integrationTypes.length != ctxB.integrationTypes.length) return false + if (!ctxA.integrationTypes.every((integrationA) => { + return ctxB.integrationTypes.includes(integrationA) + })) return false + + return true + } +} + +/**## ODContextMenuInteractionCallback `type` + * Callback for the message context menu interaction listener. + */ +export type ODContextMenuInteractionCallback = (interaction:discord.ContextMenuCommandInteraction,cmd:ODContextMenu) => void + +/**## ODContextMenuRegisteredResult `type` + * The result which will be returned when getting all (un)registered user context menu's from the manager. + */ +export type ODContextMenuRegisteredResult = { + /**A list of all registered message context menus. */ + registered:{ + /**The instance (`ODContextMenu`) from this message context menu. */ + instance:ODContextMenu, + /**The universal object/template/builder of this message context menu. */ + menu:ODContextMenuUniversalMenu, + /**Does this message context menu require an update? */ + requiresUpdate:boolean + }[], + /**A list of all unregistered message context menus. */ + unregistered:{ + /**The instance (`ODContextMenu`) from this message context menu. */ + instance:ODContextMenu, + /**The universal object/template/builder of this message context menu. */ + menu:null, + /**Does this message context menu require an update? */ + requiresUpdate:true + }[], + /**A list of all unused message context menus (not found in `ODContextMenuManager`). */ + unused:{ + /**The instance (`ODContextMenu`) from this message context menu. */ + instance:null, + /**The universal object/template/builder of this message context menu. */ + menu:ODContextMenuUniversalMenu, + /**Does this context menu require an update? */ + requiresUpdate:false + }[] +} + +/**## ODContextMenuManager `class` + * This is an Open Ticket client message context menu manager. + * + * It's responsible for managing all the message context interactions from the client. + * + * Here, you can add & remove message context interactions & the bot will do the (de)registering. + */ +export class ODContextMenuManager extends ODManager { + /**Alias to Open Ticket debugger. */ + #debug: ODDebugger + + /**Refrerence to discord.js client. */ + manager: ODClientManager + /**Discord.js application commands manager. */ + commandManager: discord.ApplicationCommandManager|null + /**Collection of all interaction listeners. */ + #interactionListeners: {name:string|RegExp, callback:ODContextMenuInteractionCallback}[] = [] + /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */ + listenerLimit: number = 100 + /**A utility class used to compare 2 context menus with each other. */ + comparator: ODContextMenuComparator = new ODContextMenuComparator() + + constructor(debug:ODDebugger, manager:ODClientManager){ + super(debug,"context menu") + this.#debug = debug + this.manager = manager + this.commandManager = (manager.client.application) ? manager.client.application.commands : null + } + + /**Get all registered & unregistered message context menu commands. */ + async getAllRegisteredMenus(guildId?:string): Promise { + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register message context menus!") + + const menus = (await this.commandManager.fetch({guildId})).toJSON() + const registered: {instance:ODContextMenu, menu:ODContextMenuUniversalMenu, requiresUpdate:boolean}[] = [] + const unregistered: {instance:ODContextMenu, menu:null, requiresUpdate:true}[] = [] + const unused: {instance:null, menu:ODContextMenuUniversalMenu, requiresUpdate:false}[] = [] + + await this.loopAll((instance) => { + if (guildId && instance.guildId != guildId) return + + const index = menus.findIndex((menu) => menu.name == instance.name) + const menu = menus[index] + menus.splice(index,1) + if (menu){ + //menu is registered (and may need to be updated) + const universalBuilder = this.comparator.convertBuilder(instance.builder,instance.guildId) + const universalMenu = this.comparator.convertMenu(menu) + + //menu is not of the type 'message'|'user' + if (!universalBuilder || !universalMenu) return + + const didChange = !this.comparator.compare(universalBuilder,universalMenu) + const requiresUpdate = didChange || (instance.requiresUpdate ? instance.requiresUpdate(universalMenu) : false) + registered.push({instance,menu:universalMenu,requiresUpdate}) + + //menu is not registered + }else unregistered.push({instance,menu:null,requiresUpdate:true}) + }) + + menus.forEach((menu) => { + //menu does not exist in the manager (only append to unused when type == 'message'|'user') + const universalCmd = this.comparator.convertMenu(menu) + if (!universalCmd) return + unused.push({instance:null,menu:universalCmd,requiresUpdate:false}) + }) + + return {registered,unregistered,unused} + } + /**Create all context menus that are not registered yet.*/ + async createNewMenus(instances:ODContextMenu[],progress?:ODManualProgressBar){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!") + if (instances.length > 0 && progress){ + progress.max = instances.length + progress.start() + } + + for (const instance of instances){ + await this.createMenu(instance) + this.#debug.debug("Created new context menu",[ + {key:"id",value:instance.id.value}, + {key:"name",value:instance.name}, + {key:"type",value:(instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"} + ]) + if (progress) progress.increase(1) + } + } + /**Update all context menus that are already registered. */ + async updateExistingMenus(instances:ODContextMenu[],progress?:ODManualProgressBar){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!") + if (instances.length > 0 && progress){ + progress.max = instances.length + progress.start() + } + + for (const instance of instances){ + await this.createMenu(instance) + this.#debug.debug("Updated existing context menu",[ + {key:"id",value:instance.id.value}, + {key:"name",value:instance.name}, + {key:"type",value:(instance.builder.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"} + ]) + if (progress) progress.increase(1) + } + } + /**Remove all context menus that are registered but unused by Open Ticket. */ + async removeUnusedMenus(instances:ODContextMenuUniversalMenu[],guildId?:string,progress?:ODManualProgressBar){ + if (!this.manager.ready) throw new ODSystemError("Client isn't ready yet! Unable to register context menus!") + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register context menus!") + if (instances.length > 0 && progress){ + progress.max = instances.length + progress.start() + } + + const menus = await this.commandManager.fetch({guildId}) + + for (const instance of instances){ + const menu = menus.find((menu) => menu.name == instance.name) + if (menu){ + try { + await menu.delete() + this.#debug.debug("Removed existing context menu",[ + {key:"name",value:menu.name}, + {key:"guildId",value:guildId ?? "/"}, + {key:"type",value:(instance.type == discord.ApplicationCommandType.Message) ? "message-context" : "user-context"} + ]) + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("Failed to delete context menu '"+menu.name+"'!") + } + } + if (progress) progress.increase(1) + } + } + /**Create a context menu. **(SYSTEM ONLY)** => Use `ODContextMenuManager` for registering context menu's the default way! */ + async createMenu(menu:ODContextMenu){ + if (!this.commandManager) throw new ODSystemError("Couldn't get client application to register context menu's!") + try { + await this.commandManager.create(menu.builder,(menu.guildId ?? undefined)) + }catch(err){ + process.emit("uncaughtException",err) + throw new ODSystemError("Failed to register context menu '"+menu.name+"'!") + } + } + /**Start listening to the discord.js client `interactionCreate` event. */ + startListeningToInteractions(){ + this.manager.client.on("interactionCreate",(interaction) => { + //return when not in main server or DM + if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id)) return + + if (!interaction.isContextMenuCommand()) return + const menu = this.getFiltered((menu) => menu.name == interaction.commandName)[0] + if (!menu) return + + this.#interactionListeners.forEach((listener) => { + if (typeof listener.name == "string" && (interaction.commandName != listener.name)) return + else if (listener.name instanceof RegExp && !listener.name.test(interaction.commandName)) return + + //this is a valid listener + listener.callback(interaction,menu) + }) + }) + } + /**Callback on interaction from one or multiple context menu's. */ + onInteraction(menuName:string|RegExp, callback:ODContextMenuInteractionCallback){ + this.#interactionListeners.push({ + name:menuName, + callback + }) + + if (this.#interactionListeners.length > this.listenerLimit){ + this.#debug.console.log("Possible context menu interaction memory leak detected!","warning",[ + {key:"listeners",value:this.#interactionListeners.length.toString()} + ]) + } + } +} + +/**## ODContextMenuUpdateFunction `type` + * The function responsible for updating context menu's when they already exist. + */ +export type ODContextMenuUpdateFunction = (menu:ODContextMenuUniversalMenu) => boolean + +/**## ODContextMenu `class` + * This is an Open Ticket context menu. + * + * When registered, you can listen for this context menu using the `ODContextResponder`. The advantages of using this class for creating a context menu are: + * - automatic registration in discord.js + * - error reporting to the user when the bot fails to respond + * - plugins can extend this context menu + * - the bot won't re-register the context menu when it already exists (except when requested)! + * + * And more! + */ +export class ODContextMenu extends ODManagerData { + /**The discord.js builder for this context menu. */ + builder: ODContextMenuBuilder + /**The id of the guild this context menu is for. `null` when not set. */ + guildId: string|null + /**Function to check if the context menu requires to be updated (when it already exists). */ + requiresUpdate: ODContextMenuUpdateFunction|null = null + + constructor(id:ODValidId, builder:ODContextMenuBuilder, requiresUpdate?:ODContextMenuUpdateFunction, guildId?:string){ + super(id) + if (builder.type != discord.ApplicationCommandType.Message && builder.type != discord.ApplicationCommandType.User) throw new ODSystemError("ApplicationCommandData is required to be the 'Message'|'User' type!") + + this.builder = builder + this.guildId = guildId ?? null + this.requiresUpdate = requiresUpdate ?? null + } + + /**The name of this context menu. */ + get name(): string { + return this.builder.name + } + set name(name:string){ + this.builder.name = name + } } \ No newline at end of file diff --git a/src/core/api/modules/defaults.ts b/src/core/api/modules/defaults.ts index 63a9d68..02029fa 100644 --- a/src/core/api/modules/defaults.ts +++ b/src/core/api/modules/defaults.ts @@ -91,6 +91,14 @@ export interface ODDefaults { forceSlashCommandRegistration:boolean, /**When enabled, the bot is allowed to unregister all slash commands which aren't used in Open Ticket. Disable this if you don't want to use the Open Ticket `ODSlashCommand` classes. */ allowSlashCommandRemoval:boolean, + /**Load the default Open Ticket context menus. */ + contextMenuLoading:boolean, + /**Load the default Open Ticket context menu registerer (register menus in discord). */ + contextMenuRegistering:boolean, + /**When enabled, the bot is forced to re-register all context menus in the server. This can be used in case of a auto-update malfunction. */ + forceContextMenuRegistration:boolean, + /**When enabled, the bot is allowed to unregister all context menus which aren't used in Open Ticket. Disable this if you don't want to use the Open Ticket `ODContextMenu` classes. */ + allowContextMenuRemoval:boolean, /**Load the default Open Ticket text commands. */ textCommandLoading:boolean, @@ -277,6 +285,10 @@ export class ODDefaultsManager { slashCommandRegistering:true, forceSlashCommandRegistration:false, allowSlashCommandRemoval:true, + contextMenuLoading:true, + contextMenuRegistering:true, + forceContextMenuRegistration:false, + allowContextMenuRemoval:true, textCommandLoading:true, questionLoading:true, diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index a854dbf..b24d09b 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -1094,4 +1094,13 @@ export const loadAllTextCommands = async () => { } ] })) +} + +export const loadAllContextMenus = async () => { + const menus = opendiscord.client.contextMenus + const generalConfig = opendiscord.configs.get("opendiscord:general") + if (!generalConfig) return + + const act = discord.ApplicationCommandType + if (!generalConfig.data.slashCommands) return } \ No newline at end of file diff --git a/src/data/framework/eventLoader.ts b/src/data/framework/eventLoader.ts index 2124a1e..6aa4111 100644 --- a/src/data/framework/eventLoader.ts +++ b/src/data/framework/eventLoader.ts @@ -82,6 +82,12 @@ export const loadAllEvents = () => { "onSlashCommandRegister", "afterSlashCommandsRegistered", + //client context menus + "onContextMenuLoad", + "afterContextMenusLoaded", + "onContextMenuRegister", + "afterContextMenusRegistered", + //client text commands "onTextCommandLoad", "afterTextCommandsLoaded", diff --git a/src/data/framework/progressBarLoader.ts b/src/data/framework/progressBarLoader.ts index 27795f4..63eacb1 100644 --- a/src/data/framework/progressBarLoader.ts +++ b/src/data/framework/progressBarLoader.ts @@ -64,4 +64,13 @@ export const loadAllProgressBars = async () => { //SLASH COMMAND UPDATE (doesn't have correct amount yet) opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:slash-command-update",fractRenderer.withAdditionalSettings({filledBarColor:"openticket"}),0,"max",null,"Commands Updated")) + + //CONTEXT MENU REMOVE (doesn't have correct amount yet) + opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:context-menu-remove",fractRenderer.withAdditionalSettings({filledBarColor:"red"}),0,"max",null,"Context Menus Removed")) + + //CONTEXT MENU CREATE (doesn't have correct amount yet) + opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:context-menu-create",fractRenderer.withAdditionalSettings({filledBarColor:"green"}),0,"max",null,"Context Menus Created")) + + //CONTEXT MENU UPDATE (doesn't have correct amount yet) + opendiscord.progressbars.add(new api.ODManualProgressBar("opendiscord:context-menu-update",fractRenderer.withAdditionalSettings({filledBarColor:"openticket"}),0,"max",null,"Context Menus Updated")) } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7f368f0..dbd42eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -456,6 +456,38 @@ const main = async () => { await opendiscord.events.get("afterSlashCommandsRegistered").emit([opendiscord.client.slashCommands,opendiscord.client]) } + //load context menus + opendiscord.log("Loading context menus...","system") + if (opendiscord.defaults.getDefault("contextMenuLoading")){ + await (await import("./data/framework/commandLoader.js")).loadAllContextMenus() + } + await opendiscord.events.get("onContextMenuLoad").emit([opendiscord.client.contextMenus,opendiscord.client]) + await opendiscord.events.get("afterContextMenusLoaded").emit([opendiscord.client.contextMenus,opendiscord.client]) + + //register context menus (create, update & remove) + if (opendiscord.defaults.getDefault("forceContextMenuRegistration")) opendiscord.log("Forcing all context menus to be re-registered...","system") + opendiscord.log("Registering context menus... (this can take up to a minute)","system") + await opendiscord.events.get("onContextMenuRegister").emit([opendiscord.client.contextMenus,opendiscord.client]) + if (opendiscord.defaults.getDefault("contextMenuRegistering")){ + //get all context menus that are already registered in the bot + const menus = await opendiscord.client.contextMenus.getAllRegisteredMenus() + const removableMenus = menus.unused.map((menu) => menu.menu) + const newMenus = menus.unregistered.map((menu) => menu.instance) + const updatableMenus = menus.registered.filter((menu) => menu.requiresUpdate || opendiscord.defaults.getDefault("forceContextMenuRegistration")).map((menu) => menu.instance) + + //init progress bars + const removeProgress = opendiscord.progressbars.get("opendiscord:context-menu-remove") + const createProgress = opendiscord.progressbars.get("opendiscord:context-menu-create") + const updateProgress = opendiscord.progressbars.get("opendiscord:context-menu-update") + + //remove unused menus, create new menus & update existing menus + if (opendiscord.defaults.getDefault("allowContextMenuRemoval")) await opendiscord.client.contextMenus.removeUnusedMenus(removableMenus,undefined,removeProgress) + await opendiscord.client.contextMenus.createNewMenus(newMenus,createProgress) + await opendiscord.client.contextMenus.updateExistingMenus(updatableMenus,updateProgress) + + await opendiscord.events.get("afterContextMenusRegistered").emit([opendiscord.client.contextMenus,opendiscord.client]) + } + //load text commands opendiscord.log("Loading text commands...","system") if (opendiscord.defaults.getDefault("allowDumpCommand")){ From e987c176b72539ca76abe93d3474b13800c26ebb Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sat, 21 Jun 2025 22:16:50 +0200 Subject: [PATCH 41/78] Add autocomplete manager + context menu bugfixes --- src/core/api/modules/client.ts | 96 ++++++++++++++++++++++++----- src/core/startup/manageMigration.ts | 5 +- src/data/framework/codeLoader.ts | 2 + src/index.ts | 6 +- 4 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index 456248a..7425a7d 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -78,6 +78,8 @@ export class ODClientManager { textCommands: ODTextCommandManager /**The context menu manager is responsible for all context menus & their events inside the bot. */ contextMenus: ODContextMenuManager + /**The autocomplete manager is responsible for all autocomplete events inside the bot. */ + autocompletes: ODAutocompleteManager constructor(debug:ODDebugger){ this.#debug = debug @@ -85,6 +87,7 @@ export class ODClientManager { this.slashCommands = new ODSlashCommandManager(this.#debug,this) this.textCommands = new ODTextCommandManager(this.#debug,this) this.contextMenus = new ODContextMenuManager(this.#debug,this) + this.autocompletes = new ODAutocompleteManager(this.#debug,this) } /**Initiate the `client` variable & add the intents & partials to the bot. */ @@ -152,6 +155,7 @@ export class ODClientManager { if (!this.client.application) throw new ODSystemError("Couldn't get client application for slashCommand & contextMenu managers!") this.slashCommands.commandManager = this.client.application.commands this.contextMenus.commandManager = this.client.application.commands + this.autocompletes.commandManager = this.client.application.commands if (this.readyListener) await this.readyListener() resolve(true) @@ -1920,7 +1924,7 @@ export class ODContextMenuComparator { } /**## ODContextMenuInteractionCallback `type` - * Callback for the message context menu interaction listener. + * Callback for the context menu interaction listener. */ export type ODContextMenuInteractionCallback = (interaction:discord.ContextMenuCommandInteraction,cmd:ODContextMenu) => void @@ -1928,29 +1932,29 @@ export type ODContextMenuInteractionCallback = (interaction:discord.ContextMenuC * The result which will be returned when getting all (un)registered user context menu's from the manager. */ export type ODContextMenuRegisteredResult = { - /**A list of all registered message context menus. */ + /**A list of all registered context menus. */ registered:{ - /**The instance (`ODContextMenu`) from this message context menu. */ + /**The instance (`ODContextMenu`) from this context menu. */ instance:ODContextMenu, - /**The universal object/template/builder of this message context menu. */ + /**The universal object/template/builder of this context menu. */ menu:ODContextMenuUniversalMenu, - /**Does this message context menu require an update? */ + /**Does this context menu require an update? */ requiresUpdate:boolean }[], - /**A list of all unregistered message context menus. */ + /**A list of all unregistered context menus. */ unregistered:{ - /**The instance (`ODContextMenu`) from this message context menu. */ + /**The instance (`ODContextMenu`) from this context menu. */ instance:ODContextMenu, - /**The universal object/template/builder of this message context menu. */ + /**The universal object/template/builder of this context menu. */ menu:null, - /**Does this message context menu require an update? */ + /**Does this context menu require an update? */ requiresUpdate:true }[], - /**A list of all unused message context menus (not found in `ODContextMenuManager`). */ + /**A list of all unused context menus (not found in `ODContextMenuManager`). */ unused:{ - /**The instance (`ODContextMenu`) from this message context menu. */ + /**The instance (`ODContextMenu`) from this context menu. */ instance:null, - /**The universal object/template/builder of this message context menu. */ + /**The universal object/template/builder of this context menu. */ menu:ODContextMenuUniversalMenu, /**Does this context menu require an update? */ requiresUpdate:false @@ -1958,11 +1962,11 @@ export type ODContextMenuRegisteredResult = { } /**## ODContextMenuManager `class` - * This is an Open Ticket client message context menu manager. + * This is an Open Ticket client context menu manager. * - * It's responsible for managing all the message context interactions from the client. + * It's responsible for managing all the context interactions from the client. * - * Here, you can add & remove message context interactions & the bot will do the (de)registering. + * Here, you can add & remove context interactions & the bot will do the (de)registering. */ export class ODContextMenuManager extends ODManager { /**Alias to Open Ticket debugger. */ @@ -2175,4 +2179,66 @@ export class ODContextMenu extends ODManagerData { set name(name:string){ this.builder.name = name } +} + +/**## ODAutocompleteInteractionCallback `type` + * Callback for the autocomplete interaction listener. + */ +export type ODAutocompleteInteractionCallback = (interaction:discord.AutocompleteInteraction) => void + +/**## ODAutocompleteManager `class` + * This is an Open Ticket client autocomplete interaction manager. + * + * It's responsible for managing all the autocomplete interactions from the client. + */ +export class ODAutocompleteManager { + /**Alias to Open Ticket debugger. */ + #debug: ODDebugger + + /**Refrerence to discord.js client. */ + manager: ODClientManager + /**Discord.js application commands manager. */ + commandManager: discord.ApplicationCommandManager|null + /**Collection of all interaction listeners. */ + #interactionListeners: {cmdName:string|RegExp, optName:string|RegExp, callback:ODAutocompleteInteractionCallback}[] = [] + /**Set the soft limit for maximum amount of listeners. A warning will be shown when there are more listeners than this limit. */ + listenerLimit: number = 100 + + constructor(debug:ODDebugger, manager:ODClientManager){ + this.#debug = debug + this.manager = manager + this.commandManager = (manager.client.application) ? manager.client.application.commands : null + } + + /**Start listening to the discord.js client `interactionCreate` event. */ + startListeningToInteractions(){ + this.manager.client.on("interactionCreate",(interaction) => { + //return when not in main server or DM + if (!this.manager.mainServer || (interaction.guild && interaction.guild.id != this.manager.mainServer.id)) return + + if (!interaction.isAutocomplete()) return + this.#interactionListeners.forEach((listener) => { + + if (typeof listener.cmdName == "string" && (interaction.commandName != listener.cmdName)) return + else if (listener.cmdName instanceof RegExp && !listener.cmdName.test(interaction.commandName)) return + if (typeof listener.optName == "string" && (interaction.options.getFocused(true).name != listener.optName)) return + else if (listener.optName instanceof RegExp && !listener.optName.test(interaction.options.getFocused(true).name)) return + + //this is a valid listener + listener.callback(interaction) + }) + }) + } + /**Callback on interaction from one or multiple autocompletes. */ + onInteraction(cmdName:string|RegExp,optName:string|RegExp,callback:ODAutocompleteInteractionCallback){ + this.#interactionListeners.push({ + cmdName,optName,callback + }) + + if (this.#interactionListeners.length > this.listenerLimit){ + this.#debug.console.log("Possible autocomplete interaction memory leak detected!","warning",[ + {key:"listeners",value:this.#interactionListeners.length.toString()} + ]) + } + } } \ No newline at end of file diff --git a/src/core/startup/manageMigration.ts b/src/core/startup/manageMigration.ts index 8aeae49..82499c1 100644 --- a/src/core/startup/manageMigration.ts +++ b/src/core/startup/manageMigration.ts @@ -47,7 +47,10 @@ export const loadVersionMigrationSystem = async () => { if (opendiscord.flags.exists("opendiscord:no-plugins") && opendiscord.flags.get("opendiscord:no-plugins").value) opendiscord.defaults.setDefault("pluginLoading",false) if (opendiscord.flags.exists("opendiscord:soft-plugins") && opendiscord.flags.get("opendiscord:soft-plugins").value) opendiscord.defaults.setDefault("softPluginLoading",true) if (opendiscord.flags.exists("opendiscord:crash") && opendiscord.flags.get("opendiscord:crash").value) opendiscord.defaults.setDefault("crashOnError",true) - if (opendiscord.flags.exists("opendiscord:force-slash-update") && opendiscord.flags.get("opendiscord:force-slash-update").value) opendiscord.defaults.setDefault("forceSlashCommandRegistration",true) + if (opendiscord.flags.exists("opendiscord:force-slash-update") && opendiscord.flags.get("opendiscord:force-slash-update").value){ + opendiscord.defaults.setDefault("forceSlashCommandRegistration",true) + opendiscord.defaults.setDefault("forceContextMenuRegistration",true) + } if (opendiscord.flags.exists("opendiscord:silent") && opendiscord.flags.get("opendiscord:silent").value) opendiscord.console.silent = true diff --git a/src/data/framework/codeLoader.ts b/src/data/framework/codeLoader.ts index 4d7051e..da79601 100644 --- a/src/data/framework/codeLoader.ts +++ b/src/data/framework/codeLoader.ts @@ -62,6 +62,8 @@ export const loadStartListeningInteractionsCode = async () => { opendiscord.code.add(new api.ODCode("opendiscord:start-listening-interactions",13,() => { opendiscord.client.slashCommands.startListeningToInteractions() opendiscord.client.textCommands.startListeningToInteractions() + opendiscord.client.contextMenus.startListeningToInteractions() + opendiscord.client.autocompletes.startListeningToInteractions() })) } diff --git a/src/index.ts b/src/index.ts index dbd42eb..be21530 100644 --- a/src/index.ts +++ b/src/index.ts @@ -384,7 +384,7 @@ const main = async () => { if (!mainServer || !client.checkBotInGuild(mainServer)){ console.log("\n") opendiscord.log("The bot isn't a member of the server provided in the config!","error") - opendiscord.log("Please invite your bot to the server!","info") + opendiscord.log("Please invite your bot to this server!","info") console.log("\n") process.exit(1) } @@ -399,8 +399,8 @@ const main = async () => { if (opendiscord.defaults.getDefault("clientMultiGuildWarning")){ //warn if bot is in multiple servers if (botServers.length > 1){ - opendiscord.log("This bot is part of multiple servers, but Open Ticket doesn't have support for it!","warning") - opendiscord.log("It may result in the bot crashing & glitching when used in these servers!","info") + opendiscord.log("This bot is part of multiple servers, but Open Ticket doesn't provide support for this!","warning") + opendiscord.log("As a result, the bot may crash & glitch when used in the additional servers!","info") } botServers.forEach((server) => { //warn if bot doesn't have permissions in multiple servers From 8116d4d8dde893631d175f4095264bcf5be6257a Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sat, 21 Jun 2025 22:17:31 +0200 Subject: [PATCH 42/78] Startscreen plugin list rendering improvements --- src/core/api/modules/startscreen.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/api/modules/startscreen.ts b/src/core/api/modules/startscreen.ts index 7990c19..9aab04b 100644 --- a/src/core/api/modules/startscreen.ts +++ b/src/core/api/modules/startscreen.ts @@ -269,13 +269,19 @@ export class ODStartScreenPluginsCategoryComponent extends ODStartScreenCategory constructor(id:ODValidId, priority:number, plugins:ODPlugin[], unknownCrashedPlugins:ODUnknownCrashedPlugin[]){ super(id,priority,"plugins",() => { - const renderedPlugins = this.plugins.sort((a,b) => b.priority-a.priority).map((plugin) => { - if (plugin.enabled && plugin.executed) return ansis.green("✅ ["+plugin.name+"] "+plugin.details.shortDescription) - else if (plugin.enabled && plugin.crashed) return ansis.red("❌ ["+plugin.name+"] "+plugin.details.shortDescription) - else return ansis.gray("💤 ["+plugin.name+"] "+plugin.details.shortDescription) - }) + const disabledPlugins = this.plugins.filter((plugin) => !plugin.enabled) + + const renderedActivePlugins = this.plugins.filter((plugin) => plugin.enabled && plugin.executed).sort((a,b) => b.priority-a.priority).map((plugin) => ansis.green("✅ ["+plugin.name+"] "+plugin.details.shortDescription)) + const renderedCrashedPlugins = this.plugins.filter((plugin) => plugin.enabled && plugin.crashed).sort((a,b) => b.priority-a.priority).map((plugin) => ansis.red("❌ ["+plugin.name+"] "+plugin.details.shortDescription)) + const renderedDisabledPlugins = (disabledPlugins.length > 4) ? [ansis.gray("💤 (+"+disabledPlugins.length+" disabled plugins)")] : disabledPlugins.sort((a,b) => b.priority-a.priority).map((plugin) => ansis.gray("💤 ["+plugin.name+"] "+plugin.details.shortDescription)) const renderedUnknownPlugins = unknownCrashedPlugins.map((plugin) => ansis.red("❌ ["+plugin.name+"] "+plugin.description)) - return [...renderedPlugins,...renderedUnknownPlugins].join("\n") + + return [ + ...renderedActivePlugins, + ...renderedDisabledPlugins, + ...renderedCrashedPlugins, + ...renderedUnknownPlugins + ].join("\n") },false) this.plugins = plugins this.unknownCrashedPlugins = unknownCrashedPlugins From 6df3498cb5c5c2a20681623526c3b1e00460ac80 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 24 Jun 2025 08:43:57 +0200 Subject: [PATCH 43/78] Finished autocomplete & context menu interactions Autocomplete & context menu interactions have been fully implemented in the Open Discord framework. They have not yet been implemented in the Open Ticket bot code though. --- src/core/api/defaults/client.ts | 2 +- src/core/api/defaults/event.ts | 6 +- src/core/api/defaults/responder.ts | 98 ++++++++- src/core/api/modules/defaults.ts | 6 + src/core/api/modules/responder.ts | 305 ++++++++++++++++++++++++++++- src/data/framework/eventLoader.ts | 4 + src/index.ts | 18 +- 7 files changed, 432 insertions(+), 7 deletions(-) diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts index 620e4ef..0859c14 100644 --- a/src/core/api/defaults/client.ts +++ b/src/core/api/defaults/client.ts @@ -160,7 +160,7 @@ export class ODTextCommandManager_Default extends ODTextCommandManager { * It's used to generate typescript declarations for this class. */ export interface ODContextMenuManagerIds_Default { - "opendiscord:test-menu":ODContextMenu + //"opendiscord:test-menu":ODContextMenu } /**## ODContextMenuManager_Default `default_class` diff --git a/src/core/api/defaults/event.ts b/src/core/api/defaults/event.ts index 1ea6607..f4f6d05 100644 --- a/src/core/api/defaults/event.ts +++ b/src/core/api/defaults/event.ts @@ -21,7 +21,7 @@ import { ODLanguageManager_Default } from "./language" import { ODCheckerFunctionManager_Default, ODCheckerManager_Default, ODCheckerRenderer_Default, ODCheckerTranslationRegister_Default } from "./checker" import { ODClientManager_Default, ODContextMenuManager_Default, ODSlashCommandManager_Default, ODTextCommandManager_Default } from "./client" import { ODBuilderManager_Default, ODButtonManager_Default, ODDropdownManager_Default, ODEmbedManager_Default, ODFileManager_Default, ODMessageManager_Default, ODModalManager_Default } from "./builder" -import { ODButtonResponderManager_Default, ODCommandResponderManager_Default, ODDropdownResponderManager_Default, ODModalResponderManager_Default, ODResponderManager_Default } from "./responder" +import { ODAutocompleteResponderManager_Default, ODButtonResponderManager_Default, ODCommandResponderManager_Default, ODContextMenuResponderManager_Default, ODDropdownResponderManager_Default, ODModalResponderManager_Default, ODResponderManager_Default } from "./responder" import { ODActionManager_Default } from "./action" import { ODPermissionManager_Default } from "./permission" import { ODHelpMenuManager_Default } from "./helpmenu" @@ -255,6 +255,10 @@ export interface ODEventIds_Default { "afterDropdownRespondersLoaded": ODEvent_Default<(dropdowns:ODDropdownResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> "onModalResponderLoad": ODEvent_Default<(modals:ODModalResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> "afterModalRespondersLoaded": ODEvent_Default<(modals:ODModalResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onContextMenuResponderLoad": ODEvent_Default<(menus:ODContextMenuResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterContextMenuRespondersLoaded": ODEvent_Default<(menus:ODContextMenuResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "onAutocompleteResponderLoad": ODEvent_Default<(autocomplete:ODAutocompleteResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> + "afterAutocompleteRespondersLoaded": ODEvent_Default<(autocomplete:ODAutocompleteResponderManager_Default, responders:ODResponderManager_Default, actions:ODActionManager_Default) => ODPromiseVoid> //plugin loading before finalizations "onPluginBeforeFinalizationLoad": ODEvent_Default<() => ODPromiseVoid>, diff --git a/src/core/api/defaults/responder.ts b/src/core/api/defaults/responder.ts index 1d80ea9..95534bb 100644 --- a/src/core/api/defaults/responder.ts +++ b/src/core/api/defaults/responder.ts @@ -2,7 +2,7 @@ //DEFAULT RESPONDER MODULE /////////////////////////////////////// import { ODValidId } from "../modules/base" -import { ODButtonResponder, ODButtonResponderInstance, ODButtonResponderManager, ODCommandResponder, ODCommandResponderInstance, ODCommandResponderManager, ODDropdownResponder, ODDropdownResponderInstance, ODDropdownResponderManager, ODModalResponder, ODModalResponderInstance, ODModalResponderManager, ODResponderManager } from "../modules/responder" +import { ODAutocompleteResponder, ODAutocompleteResponderInstance, ODAutocompleteResponderManager, ODButtonResponder, ODButtonResponderInstance, ODButtonResponderManager, ODCommandResponder, ODCommandResponderInstance, ODCommandResponderManager, ODContextMenuResponder, ODContextMenuResponderInstance, ODContextMenuResponderManager, ODDropdownResponder, ODDropdownResponderInstance, ODDropdownResponderManager, ODModalResponder, ODModalResponderInstance, ODModalResponderManager, ODResponderManager } from "../modules/responder" import { ODWorkerManager_Default } from "./worker" /**## ODResponderManager_Default `default_class` @@ -16,6 +16,8 @@ export class ODResponderManager_Default extends ODResponderManager { declare buttons: ODButtonResponderManager_Default declare dropdowns: ODDropdownResponderManager_Default declare modals: ODModalResponderManager_Default + declare contextMenus: ODContextMenuResponderManager_Default + declare autocomplete: ODAutocompleteResponderManager_Default } /**## ODCommandResponderManagerIds_Default `interface` @@ -252,4 +254,98 @@ export class ODModalResponderManager_Default extends ODModalResponderManager { */ export class ODModalResponder_Default extends ODModalResponder { declare workers: ODWorkerManager_Default +} + +/**## ODContextMenuResponderManagerIds_Default `interface` + * This interface is a list of ids available in the `ODContextMenuResponderManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODContextMenuResponderManagerIds_Default { + //"opendiscord:example":{source:"context-menu",params:{},workers:"opendiscord:example"}, +} + +/**## ODContextMenuResponderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODContextMenuResponderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `opendiscord.responders.contextMenus`! + */ +export class ODContextMenuResponderManager_Default extends ODContextMenuResponderManager { + get(id:ModalResponderId): ODContextMenuResponder_Default + get(id:ODValidId): ODContextMenuResponder<"context-menu",any>|null + + get(id:ODValidId): ODContextMenuResponder<"context-menu",any>|null { + return super.get(id) + } + + remove(id:ModalResponderId): ODContextMenuResponder_Default + remove(id:ODValidId): ODContextMenuResponder<"context-menu",any>|null + + remove(id:ODValidId): ODContextMenuResponder<"context-menu",any>|null { + return super.remove(id) + } + + exists(id:keyof ODContextMenuResponderManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODContextMenuResponder_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODContextMenuResponder class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODContextMenuResponder`'s! + */ +export class ODContextMenuResponder_Default extends ODContextMenuResponder { + declare workers: ODWorkerManager_Default +} + +/**## ODAutocompleteResponderManagerIds_Default `interface` + * This interface is a list of ids available in the `ODAutocompleteResponderManager_Default` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODAutocompleteResponderManagerIds_Default { + //"opendiscord:example":{source:"autocomplete",params:{},workers:"opendiscord:example"}, +} + +/**## ODAutocompleteResponderManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODAutocompleteResponderManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `opendiscord.responders.autocomplete`! + */ +export class ODAutocompleteResponderManager_Default extends ODAutocompleteResponderManager { + get(id:ModalResponderId): ODAutocompleteResponder_Default + get(id:ODValidId): ODAutocompleteResponder<"autocomplete",any>|null + + get(id:ODValidId): ODAutocompleteResponder<"autocomplete",any>|null { + return super.get(id) + } + + remove(id:ModalResponderId): ODAutocompleteResponder_Default + remove(id:ODValidId): ODAutocompleteResponder<"autocomplete",any>|null + + remove(id:ODValidId): ODAutocompleteResponder<"autocomplete",any>|null { + return super.remove(id) + } + + exists(id:keyof ODAutocompleteResponderManagerIds_Default): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODAutocompleteResponder_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODAutocompleteResponder class. + * It doesn't add any extra features! + * + * This default class is made for the default `ODAutocompleteResponder`'s! + */ +export class ODAutocompleteResponder_Default extends ODAutocompleteResponder { + declare workers: ODWorkerManager_Default } \ No newline at end of file diff --git a/src/core/api/modules/defaults.ts b/src/core/api/modules/defaults.ts index 02029fa..4b366ff 100644 --- a/src/core/api/modules/defaults.ts +++ b/src/core/api/modules/defaults.ts @@ -140,6 +140,10 @@ export interface ODDefaults { dropdownRespondersLoading:boolean, /**Load the default Open Ticket modal responders. */ modalRespondersLoading:boolean, + /**Load the default Open Ticket context menu responders. */ + contextMenuRespondersLoading:boolean, + /**Load the default Open Ticket autocomplete responders. */ + autocompleteRespondersLoading:boolean, /**Set the time (in ms) before Open Ticket sends an error message when no reply is sent in a responder. */ responderTimeoutMs:number, @@ -311,6 +315,8 @@ export class ODDefaultsManager { buttonRespondersLoading:true, dropdownRespondersLoading:true, modalRespondersLoading:true, + contextMenuRespondersLoading:true, + autocompleteRespondersLoading:true, responderTimeoutMs:2500, actionsLoading:true, diff --git a/src/core/api/modules/responder.ts b/src/core/api/modules/responder.ts index a732d2a..ab995dd 100644 --- a/src/core/api/modules/responder.ts +++ b/src/core/api/modules/responder.ts @@ -5,7 +5,7 @@ import { ODId, ODManager, ODValidId, ODSystemError, ODManagerData } from "./base import * as discord from "discord.js" import { ODWorkerManager, ODWorkerCallback, ODWorker } from "./worker" import { ODDebugger } from "./console" -import { ODClientManager, ODSlashCommand, ODTextCommand, ODTextCommandInteractionOption } from "./client" +import { ODClientManager, ODContextMenu, ODSlashCommand, ODTextCommand, ODTextCommandInteractionOption } from "./client" import { ODDropdownData, ODMessageBuildResult, ODMessageBuildSentResult, ODModalBuildResult } from "./builder" /**## ODResponderImplementation `class` @@ -36,7 +36,7 @@ export class ODResponderImplementation ex /**## ODResponderTimeoutErrorCallback `type` * This is the callback for the responder timeout function. It will be executed when something went wrong or the action takes too much time. */ -export type ODResponderTimeoutErrorCallback = (instance:Instance, source:Source) => void|Promise +export type ODResponderTimeoutErrorCallback = (instance:Instance, source:Source) => void|Promise /**## ODResponderManager `class` * This is an Open Ticket responder manager. @@ -61,12 +61,18 @@ export class ODResponderManager { dropdowns: ODDropdownResponderManager /**A manager for all modal responders. */ modals: ODModalResponderManager + /**A manager for all context menu responders. */ + contextMenus: ODContextMenuResponderManager + /**A manager for all autocomplete responders. */ + autocomplete: ODAutocompleteResponderManager constructor(debug:ODDebugger, client:ODClientManager){ this.commands = new ODCommandResponderManager(debug,"command responder",client) this.buttons = new ODButtonResponderManager(debug,"button responder",client) this.dropdowns = new ODDropdownResponderManager(debug,"dropdown responder",client) this.modals = new ODModalResponderManager(debug,"modal responder",client) + this.contextMenus = new ODContextMenuResponderManager(debug,"context menu responder",client) + this.autocomplete = new ODAutocompleteResponderManager(debug,"autocomplete responder",client) } } @@ -1083,7 +1089,7 @@ export class ODModalResponderInstance { const sent = await this.interaction.editReply(Object.assign(msg.message,{flags:msgFlags})) this.didReply = true return {success:true,message:await sent.fetch()} - }else throw new ODSystemError() + }else throw new ODSystemError("Unable to update modal interaction!") }catch{ return {success:false,message:null} } @@ -1113,4 +1119,297 @@ export class ODModalResponder extends ODResponderI //wait for workers to finish await this.workers.executeWorkers(instance,source,params) } +} + +/**## ODContextMenuResponderManager `class` + * This is an Open Ticket context menu responder manager. + * + * It contains all Open Ticket context menu responders. These can respond to user/message context menu interactions. + * + * Using the Open Ticket responder system has a few advantages compared to vanilla discord.js: + * - plugins can extend/edit replies + * - automatically reply on error + * - independent workers (with priority) + * - fail-safe design using try-catch + * - know where the request came from! + * - And so much more! + */ +export class ODContextMenuResponderManager extends ODManager> { + /**An alias to the Open Ticket client manager. */ + #client: ODClientManager + /**The callback executed when the default workers take too much time to reply. */ + #timeoutErrorCallback: ODResponderTimeoutErrorCallback|null = null + /**The amount of milliseconds before the timeout error callback is executed. */ + #timeoutMs: number|null = null + + constructor(debug:ODDebugger, debugname:string, client:ODClientManager){ + super(debug,debugname) + this.#client = client + } + + /**Set the message to send when the response times out! */ + setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback|null, ms:number|null){ + this.#timeoutErrorCallback = callback + this.#timeoutMs = ms + } + + add(data:ODContextMenuResponder<"context-menu",any>, overwrite?:boolean){ + const res = super.add(data,overwrite) + + this.#client.contextMenus.onInteraction(data.match,(interaction,cmd) => { + const newData = this.get(data.id) + if (!newData) return + newData.respond(new ODContextMenuResponderInstance(interaction,cmd,this.#timeoutErrorCallback,this.#timeoutMs),"context-menu",{}) + }) + + return res + } +} + +/**## ODContextMenuResponderInstance `class` + * This is an Open Ticket context menu responder instance. + * + * An instance is an active context menu interaction. You can reply to the context menu using `reply()`. + */ +export class ODContextMenuResponderInstance { + /**The interaction which is the source of this instance. */ + interaction: discord.ContextMenuCommandInteraction + /**Did a worker already reply to this instance/interaction? */ + didReply: boolean = false + /**The context menu wich is the source of this instance. */ + menu:ODContextMenu + /**The user who triggered this context menu. */ + user: discord.User + /**The guild member who triggered this context menu. */ + member: discord.GuildMember|null + /**The guild where this context menu was triggered. */ + guild: discord.Guild|null + /**The channel where this context menu was triggered. */ + channel: discord.TextBasedChannel + /**The target of this context menu (user or message). */ + target: discord.Message|discord.User + + constructor(interaction:discord.ContextMenuCommandInteraction, menu:ODContextMenu, errorCallback:ODResponderTimeoutErrorCallback|null, timeoutMs:number|null){ + if (!interaction.channel) throw new ODSystemError("ODContextMenuResponderInstance: Unable to find interaction channel!") + this.interaction = interaction + this.menu = menu + this.user = interaction.user + this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null + this.guild = interaction.guild + this.channel = interaction.channel + if (interaction.isMessageContextMenuCommand()) this.target = interaction.targetMessage + else if (interaction.isUserContextMenuCommand()) this.target = interaction.targetUser + else throw new ODSystemError("ODContextMenuResponderInstance: Invalid context menu type. Should be of the type User/Message!") + + setTimeout(async () => { + if (!this.didReply){ + try { + if (!errorCallback){ + this.reply({id:new ODId("looks-like-we-got-an-error-here"), ephemeral:true, message:{ + content:":x: **Something went wrong while replying to this context menu!**" + }}) + }else{ + await errorCallback(this,"context-menu") + } + + }catch(err){ + process.emit("uncaughtException",err) + } + } + },timeoutMs ?? 2500) + } + + /**Reply to this context menu. */ + async reply(msg:ODMessageBuildResult): Promise> { + try{ + const msgFlags: number[] = msg.ephemeral ? [discord.MessageFlags.Ephemeral] : [] + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{flags:msgFlags})) + this.didReply = true + return {success:true,message:sent} + }else{ + const sent = await this.interaction.reply(Object.assign(msg.message,{flags:msgFlags})) + this.didReply = true + return {success:true,message:await sent.fetch()} + } + }catch{ + return {success:false,message:null} + } + } + /**Update the message of this context menu. */ + async update(msg:ODMessageBuildResult): Promise> { + try{ + const msgFlags: number[] = msg.ephemeral ? [discord.MessageFlags.Ephemeral] : [] + if (this.interaction.replied || this.interaction.deferred){ + const sent = await this.interaction.editReply(Object.assign(msg.message,{flags:msgFlags})) + this.didReply = true + return {success:true,message:await sent.fetch()} + }else throw new ODSystemError("Unable to update context menu interaction!") + }catch{ + return {success:false,message:null} + } + } + /**Defer this context menu. */ + async defer(type:"reply", ephemeral:boolean){ + if (this.interaction.deferred) return false + if (type == "reply"){ + const msgFlags: number[] = ephemeral ? [discord.MessageFlags.Ephemeral] : [] + await this.interaction.deferReply({flags:msgFlags}) + } + this.didReply = true + return true + } + /**Show a modal as reply to this context menu. */ + async modal(modal:ODModalBuildResult){ + this.interaction.showModal(modal.modal) + this.didReply = true + return true + } +} + +/**## ODContextMenuResponder `class` + * This is an Open Ticket context menu responder. + * + * This class manages all workers which are executed when the related context menu is triggered. + */ +export class ODContextMenuResponder extends ODResponderImplementation { + /**Respond to this button */ + async respond(instance:ODContextMenuResponderInstance, source:Source, params:Params){ + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + } +} + +/**## ODAutocompleteResponderManager `class` + * This is an Open Ticket autocomplete responder manager. + * + * It contains all Open Ticket autocomplete responders. These can respond to autocomplete interactions. + * + * Using the Open Ticket responder system has a few advantages compared to vanilla discord.js: + * - plugins can extend/edit replies + * - automatically reply on error + * - independent workers (with priority) + * - fail-safe design using try-catch + * - know where the request came from! + * - And so much more! + */ +export class ODAutocompleteResponderManager extends ODManager> { + /**An alias to the Open Ticket client manager. */ + #client: ODClientManager + /**The callback executed when the default workers take too much time to reply. */ + #timeoutErrorCallback: ODResponderTimeoutErrorCallback|null = null + /**The amount of milliseconds before the timeout error callback is executed. */ + #timeoutMs: number|null = null + + constructor(debug:ODDebugger, debugname:string, client:ODClientManager){ + super(debug,debugname) + this.#client = client + } + + /**Set the message to send when the response times out! */ + setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback|null, ms:number|null){ + this.#timeoutErrorCallback = callback + this.#timeoutMs = ms + } + + add(data:ODAutocompleteResponder<"autocomplete",any>, overwrite?:boolean){ + const res = super.add(data,overwrite) + + this.#client.autocompletes.onInteraction(data.cmdMatch,data.match,(interaction) => { + const newData = this.get(data.id) + if (!newData) return + newData.respond(new ODAutocompleteResponderInstance(interaction,this.#timeoutErrorCallback,this.#timeoutMs),"autocomplete",{}) + }) + + return res + } +} + +/**## ODAutocompleteResponderInstance `class` + * This is an Open Ticket autocomplete responder instance. + * + * An instance is an active autocomplete interaction. You can reply to the autocomplete using `reply()`. + */ +export class ODAutocompleteResponderInstance { + /**The interaction which is the source of this instance. */ + interaction: discord.AutocompleteInteraction + /**Did a worker already reply to this instance/interaction? */ + didReply: boolean = false + /**The user who triggered this autocomplete. */ + user: discord.User + /**The guild member who triggered this autocomplete. */ + member: discord.GuildMember|null + /**The guild where this autocomplete was triggered. */ + guild: discord.Guild|null + /**The channel where this autocomplete was triggered. */ + channel: discord.TextBasedChannel + /**The target slash command option of this autocomplete. */ + target: discord.AutocompleteFocusedOption + + constructor(interaction:discord.AutocompleteInteraction, errorCallback:ODResponderTimeoutErrorCallback|null, timeoutMs:number|null){ + if (!interaction.channel) throw new ODSystemError("ODAutocompleteResponderInstance: Unable to find interaction channel!") + this.interaction = interaction + this.user = interaction.user + this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null + this.guild = interaction.guild + this.channel = interaction.channel + this.target = interaction.options.getFocused(true) + + setTimeout(async () => { + if (!this.didReply){ + process.emit("uncaughtException",new ODSystemError("Autocomplete responder instance failed to respond widthin 2.5sec!")) + } + },timeoutMs ?? 2500) + } + + /**Reply to this autocomplete. */ + async autocomplete(choices:(string|discord.ApplicationCommandOptionChoiceData)[]): Promise<{success:boolean}> { + const newChoices: (discord.ApplicationCommandOptionChoiceData)[] = choices.map((raw) => { + if (typeof raw == "string") return {name:raw,value:raw} + else return raw + }) + + try{ + if (this.interaction.responded){ + return {success:false} + }else{ + await this.interaction.respond(newChoices) + return {success:true} + } + }catch(err){ + process.emit("uncaughtException",err) + return {success:false} + } + } + /**Reply to this autocomplete, but filter choices based on the input of the user. */ + async filteredAutocomplete(choices:(string|discord.ApplicationCommandOptionChoiceData)[]): Promise<{success:boolean}> { + const newChoices: (discord.ApplicationCommandOptionChoiceData)[] = choices.map((raw) => { + if (typeof raw == "string") return {name:raw,value:raw} + else return raw + }) + + const filteredChoices = newChoices.filter((choice) => choice.name.startsWith(this.target.value) || choice.value.toString().startsWith(this.target.value)) + return await this.autocomplete(filteredChoices) + } +} + +/**## ODAutocompleteResponder `class` + * This is an Open Ticket autocomplete responder. + * + * This class manages all workers which are executed when the related autocomplete is triggered. + */ +export class ODAutocompleteResponder extends ODResponderImplementation { + /**The slash command of the autocomplete should match the following regex. */ + cmdMatch: string|RegExp + + constructor(id:ODValidId,cmdMatch:string|RegExp,match:string|RegExp,callback?:ODWorkerCallback,priority?:number,callbackId?:ODValidId){ + super(id,match,callback,priority,callbackId) + this.cmdMatch = cmdMatch + } + + /**Respond to this autocomplete interaction. */ + async respond(instance:ODAutocompleteResponderInstance, source:Source, params:Params){ + //wait for workers to finish + await this.workers.executeWorkers(instance,source,params) + } } \ No newline at end of file diff --git a/src/data/framework/eventLoader.ts b/src/data/framework/eventLoader.ts index 6aa4111..aeda57d 100644 --- a/src/data/framework/eventLoader.ts +++ b/src/data/framework/eventLoader.ts @@ -209,6 +209,10 @@ export const loadAllEvents = () => { "afterDropdownRespondersLoaded", "onModalResponderLoad", "afterModalRespondersLoaded", + "onContextMenuResponderLoad", + "afterContextMenuRespondersLoaded", + "onAutocompleteResponderLoad", + "afterAutocompleteRespondersLoaded", //plugin loading before finalizations "onPluginBeforeFinalizationLoad", diff --git a/src/index.ts b/src/index.ts index be21530..c6e0c8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -710,6 +710,22 @@ const main = async () => { await opendiscord.events.get("onModalResponderLoad").emit([opendiscord.responders.modals,opendiscord.responders,opendiscord.actions]) await opendiscord.events.get("afterModalRespondersLoaded").emit([opendiscord.responders.modals,opendiscord.responders,opendiscord.actions]) + //load context menu responders + opendiscord.log("Loading context menu responders...","system") + if (opendiscord.defaults.getDefault("contextMenuRespondersLoading")){ + //TODO!! + } + await opendiscord.events.get("onContextMenuResponderLoad").emit([opendiscord.responders.contextMenus,opendiscord.responders,opendiscord.actions]) + await opendiscord.events.get("afterContextMenuRespondersLoaded").emit([opendiscord.responders.contextMenus,opendiscord.responders,opendiscord.actions]) + + //load autocomplete responders + opendiscord.log("Loading autocomplete responders...","system") + if (opendiscord.defaults.getDefault("autocompleteRespondersLoading")){ + //TODO!! + } + await opendiscord.events.get("onAutocompleteResponderLoad").emit([opendiscord.responders.autocomplete,opendiscord.responders,opendiscord.actions]) + await opendiscord.events.get("afterAutocompleteRespondersLoaded").emit([opendiscord.responders.autocomplete,opendiscord.responders,opendiscord.actions]) + //plugin loading before finalizations await opendiscord.events.get("onPluginBeforeFinalizationLoad").emit([]) await opendiscord.events.get("afterPluginBeforeFinalizationLoaded").emit([]) @@ -871,7 +887,7 @@ const main = async () => { await opendiscord.startscreen.renderAllComponents() if (opendiscord.languages.getLanguageMetadata(false)?.automated){ console.log("===================") - opendiscord.log("You are currently using a language which has been translated by Google Translate!","warning") + opendiscord.log("You are using a language which has been translated using Google Translate or AI!","warning") opendiscord.log("Please help us improve the translation by contributing to our project!","warning") console.log("===================") } From 549c161830dbcc0b7b1026fbcb8421af9f9ac250 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:42:15 +0200 Subject: [PATCH 44/78] Added all the new config variables for v4.1.0! --- config/general.json | 71 +++++++++++++------ config/options.json | 6 +- index.js | 2 +- src/actions/createTicket.ts | 14 +++- src/actions/moveTicket.ts | 4 +- src/actions/reactionRole.ts | 4 +- src/core/api/defaults/config.ts | 102 +++++++++++++++++++++----- src/core/api/openticket/option.ts | 5 +- src/core/api/openticket/ticket.ts | 8 +++ src/core/cli/quickSetup.ts | 62 ++++++++++++---- src/core/startup/init.ts | 6 +- src/core/startup/manageMigration.ts | 29 ++++++++ src/core/startup/migration.ts | 106 +++++++++++++++++++++++++++- src/data/framework/checkerLoader.ts | 57 ++++++++++++--- src/data/framework/configLoader.ts | 51 ++++++++++--- src/data/openticket/optionLoader.ts | 7 +- src/data/openticket/ticketLoader.ts | 5 +- src/index.ts | 2 +- 18 files changed, 455 insertions(+), 86 deletions(-) diff --git a/config/general.json b/config/general.json index ec55506..10106b8 100644 --- a/config/general.json +++ b/config/general.json @@ -5,7 +5,7 @@ "version":"open-ticket-v4.0.7" }, - "token":"your bot token here! (or leave empty when using 'tokenFromENV')", + "token":"insert your bot token here! (or leave empty when using 'tokenFromENV')", "tokenFromENV":false, "mainColor":"#f8ba00", @@ -20,21 +20,35 @@ "status":{ "enabled":true, "type":"listening OR watching OR playing OR custom", + "mode":"online OR invisible OR idle OR dnd", "text":"/help", - "status":"online OR invisible OR idle OR dnd" + "state":"(additional text or leave empty)" }, "system":{ - "removeParticipantsOnClose":false, - "replyOnTicketCreation":true, - "replyOnReactionRole":true, - "useTranslatedConfigChecker":true, "preferSlashOverText":true, "sendErrorOnUnknownCommand":true, "questionFieldsInCodeBlock":true, + "displayFieldsWithQuestions":false, + "showGlobalAdminsInPanelRoles":false, "disableVerifyBars":false, "useRedErrorEmbeds":true, + "alwaysShowReason":false, "emojiStyle":"before (OR after OR double OR disabled)", + "pinEmoji":"📌", + + "replyOnTicketCreation":true, + "replyOnReactionRole":true, + "showPreAutocloseWarning":false, + "askPriorityOnTicketCreation":false, + "removeParticipantsOnClose":false, + "disableAutocloseAfterReopen":true, + "autodeleteRequiresClosedTicket":true, + "adminOnlyDeleteWithoutTranscript":true, + "allowCloseBeforeMessage":false, + "allowCloseBeforeAdminMessage":true, + "useTranslatedConfigChecker":true, + "pinFirstTicketMessage":false, "enableTicketClaimButtons":true, "enableTicketCloseButtons":true, @@ -54,6 +68,18 @@ "userMaximum":3 }, + "channelTopic":{ + "showOptionName":true, + "showOptionDescription":false, + "showOptionTopic":true, + "showClosed":true, + "showClaimed":false, + "showPinned":false, + "showPriority":false, + "showCreator":false, + "showParticipants":false + }, + "permissions":{ "help":"everyone (OR admin OR none OR role id)", "panel":"admin (OR everyone OR none OR role id)", @@ -73,23 +99,28 @@ "stats":"everyone (OR admin OR none OR role id)", "clear":"admin (OR everyone OR none OR role id)", "autoclose":"admin (OR everyone OR none OR role id)", - "autodelete":"admin (OR everyone OR none OR role id)" + "autodelete":"admin (OR everyone OR none OR role id)", + "transfer":"admin (OR everyone OR none OR role id)", + "topic":"admin (OR everyone OR none OR role id)", + "priority":"admin (OR everyone OR none OR role id)" }, "messages":{ - "creation":{"dm":false, "logs":true}, - "closing":{"dm":false, "logs":true}, - "deleting":{"dm":false, "logs":true}, - "reopening":{"dm":false, "logs":true}, - "claiming":{"dm":false, "logs":true}, - "pinning":{"dm":false, "logs":true}, - "adding":{"dm":false, "logs":true}, - "removing":{"dm":false, "logs":true}, - "renaming":{"dm":false, "logs":true}, - "moving":{"dm":false, "logs":true}, - "blacklisting":{"dm":false, "logs":true}, - "roleAdding":{"dm":false, "logs":true}, - "roleRemoving":{"dm":false, "logs":true} + "creation":{"dm":false,"logs":true}, + "closing":{"dm":false,"logs":true}, + "deleting":{"dm":false,"logs":true}, + "reopening":{"dm":false,"logs":true}, + "claiming":{"dm":false,"logs":true}, + "pinning":{"dm":false,"logs":true}, + "adding":{"dm":false,"logs":true}, + "removing":{"dm":false,"logs":true}, + "renaming":{"dm":false,"logs":true}, + "moving":{"dm":false,"logs":true}, + "blacklisting":{"dm":false,"logs":true}, + "transferring":{"dm":false,"logs":true}, + "topicChange":{"dm":false,"logs":true}, + "priorityChange":{"dm":false,"logs":true}, + "reactionRole":{"dm":false,"logs":true} } } } \ No newline at end of file diff --git a/config/options.json b/config/options.json index 277a5ff..981a1bc 100644 --- a/config/options.json +++ b/config/options.json @@ -25,7 +25,7 @@ "claimedCategory":[ {"user":"user id","category":"category id"} ], - "description":"This is a question ticket (or leave empty)" + "topic":"This is the topic of this ticket channel and is visible to everyone! (or leave empty)" }, "dmMessage":{ @@ -87,6 +87,10 @@ "enabled":false, "globalMaximum":20, "userMaximum":3 + }, + "slowMode":{ + "enabled":false, + "slowModeSeconds":20 } }, { diff --git a/index.js b/index.js index 8d77b92..c0995a6 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ process.argv.push(...flags) ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ - v4.0.7 - Made by DJj123dj & Contributors + v4.1.0 - Made by DJj123dj & Contributors Discord: https://discord.dj-dj.be Docs: https://otdocs.dj-dj.be diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index 0974e02..9bd1467 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -19,7 +19,7 @@ export const registerActions = async () => { const channelPrefix = option.get("opendiscord:channel-prefix").value const channelCategory = option.get("opendiscord:channel-category").value const channelBackupCategory = option.get("opendiscord:channel-category-backup").value - const channelDescription = option.get("opendiscord:channel-description").value + const channelTopic = option.get("opendiscord:channel-topic").value const channelSuffix = await opendiscord.options.suffix.getSuffixFromOption(option,user) const channelName = channelPrefix+channelSuffix @@ -108,7 +108,7 @@ export const registerActions = async () => { type:discord.ChannelType.GuildText, name:channelName, nsfw:false, - topic:channelDescription, + topic:channelTopic, parent:category, reason:"Ticket Created By "+user.displayName, permissionOverwrites:permissions @@ -131,6 +131,7 @@ export const registerActions = async () => { new api.ODTicketData("opendiscord:ticket-message",null), new api.ODTicketData("opendiscord:participants",participants), new api.ODTicketData("opendiscord:channel-suffix",channelSuffix), + new api.ODTicketData("opendiscord:previous-creators",[]), new api.ODTicketData("opendiscord:open",true), new api.ODTicketData("opendiscord:opened-by",user.id), @@ -138,6 +139,9 @@ export const registerActions = async () => { new api.ODTicketData("opendiscord:closed",false), new api.ODTicketData("opendiscord:closed-by",null), new api.ODTicketData("opendiscord:closed-on",null), + new api.ODTicketData("opendiscord:reopened",false), + new api.ODTicketData("opendiscord:reopened-by",null), + new api.ODTicketData("opendiscord:reopened-on",null), new api.ODTicketData("opendiscord:claimed",false), new api.ODTicketData("opendiscord:claimed-by",null), new api.ODTicketData("opendiscord:claimed-on",null), @@ -155,7 +159,11 @@ export const registerActions = async () => { new api.ODTicketData("opendiscord:autodelete-enabled",option.get("opendiscord:autodelete-enable-days").value), new api.ODTicketData("opendiscord:autodelete-days",(option.get("opendiscord:autodelete-enable-days").value ? option.get("opendiscord:autodelete-days").value : 0)), - new api.ODTicketData("opendiscord:answers",answers) + new api.ODTicketData("opendiscord:answers",answers), + new api.ODTicketData("opendiscord:priority",-1), + new api.ODTicketData("opendiscord:topic",option.get("opendiscord:channel-topic").value), + new api.ODTicketData("opendiscord:message-sent",false), + new api.ODTicketData("opendiscord:admin-message-sent",false), ]) //manage stats diff --git a/src/actions/moveTicket.ts b/src/actions/moveTicket.ts index 11c21e3..5a9ef35 100644 --- a/src/actions/moveTicket.ts +++ b/src/actions/moveTicket.ts @@ -28,7 +28,7 @@ export const registerActions = async () => { const rawClaimCategory = ticket.option.get("opendiscord:channel-categories-claimed").value.find((c) => c.user == user.id) const claimCategory = (rawClaimCategory) ? rawClaimCategory.category : null const closeCategory = ticket.option.get("opendiscord:channel-category-closed").value - const channelDescription = ticket.option.get("opendiscord:channel-description").value + const channelTopic = ticket.option.get("opendiscord:channel-topic").value const channelName = channelPrefix+channelSuffix //handle category @@ -164,7 +164,7 @@ export const registerActions = async () => { await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:error-channel-rename").build("ticket-move",{guild,channel,user,originalName,newName:channelName})).message) } try{ - if (channel.type == discord.ChannelType.GuildText) channel.setTopic(channelDescription) + if (channel.type == discord.ChannelType.GuildText) channel.setTopic(channelTopic) }catch{} //update ticket message diff --git a/src/actions/reactionRole.ts b/src/actions/reactionRole.ts index c9cff4b..84760e1 100644 --- a/src/actions/reactionRole.ts +++ b/src/actions/reactionRole.ts @@ -88,13 +88,13 @@ export const registerActions = async () => { if (!instance.role || !instance.result) return //to logs - if (generalConfig.data.system.logs.enabled && (generalConfig.data.system.messages.roleAdding.logs || generalConfig.data.system.messages.roleRemoving.logs)){ + if (generalConfig.data.system.logs.enabled && (generalConfig.data.system.messages.reactionRole.logs)){ const logChannel = opendiscord.posts.get("opendiscord:logs") if (logChannel) logChannel.send(await opendiscord.builders.messages.getSafe("opendiscord:reaction-role-logs").build(source,{guild,user,role:instance.role,result:instance.result})) } //to dm - if (generalConfig.data.system.messages.roleAdding.dm || generalConfig.data.system.messages.roleRemoving.dm) await opendiscord.client.sendUserDm(user,await opendiscord.builders.messages.getSafe("opendiscord:reaction-role-dm").build(source,{guild,user,role:instance.role,result:instance.result})) + if (generalConfig.data.system.messages.reactionRole.dm) await opendiscord.client.sendUserDm(user,await opendiscord.builders.messages.getSafe("opendiscord:reaction-role-dm").build(source,{guild,user,role:instance.role,result:instance.result})) }), new api.ODWorker("opendiscord:logs",0,(instance,params,source,cancel) => { const {guild,user,option} = params diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index 008ab00..7a50dad 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -76,10 +76,12 @@ export interface ODJsonConfig_DefaultStatusType { enabled:boolean, /**The type of status (e.g. playing, listening, custom, ...) */ type:Exclude, + /**The mode/status of the bot (e.g. online, invisible, idle, do not disturb) */ + mode:ODClientActivityStatus /**The text for the status. */ text:string, - /**The status of the bot (e.g. online, invisible, idle, do not disturb) */ - status:ODClientActivityStatus + /**Additional text for the status. (visible below 'text') */ + state:string, } /**## ODJsonConfig_DefaultMessageSettingsType `interface` @@ -131,6 +133,30 @@ export interface ODJsonConfig_DefaultSystemLimits { userMaximum:number } +/**## ODJsonConfig_DefaultSystemChannelTopic `interface` + * All global channel topic settings. + */ +export interface ODJsonConfig_DefaultSystemChannelTopic { + /**Show the option name in the channel topic. */ + showOptionName:boolean, + /**Show the option description in the channel topic. */ + showOptionDescription:boolean, + /**Show the option topic text in the channel topic (configured in the options config). */ + showOptionTopic:boolean, + /**Show the current close/reopen status in the channel topic (auto-updated). */ + showClosed:boolean, + /**Show the current claim status in the channel topic (auto-updated). */ + showClaimed:boolean, + /**Show the current pin status in the channel topic (auto-updated). */ + showPinned:boolean, + /**Show the current priority in the channel topic (auto-updated). */ + showPriority:boolean, + /**Show the creator of the ticket in the channel topic (auto-updated on transfer). */ + showCreator:boolean, + /**Show the first 5 participants of the ticket in the channel topic (auto-updated). */ + showParticipants:boolean +} + /**## ODJsonConfig_DefaultSystemPermissions `interface` * Configure permissions for all Open Ticket commands & actions. */ @@ -153,7 +179,10 @@ export interface ODJsonConfig_DefaultSystemPermissions { stats:ODJsonConfig_DefaultCmdPermissionSettingsType, clear:ODJsonConfig_DefaultCmdPermissionSettingsType, autoclose:ODJsonConfig_DefaultCmdPermissionSettingsType, - autodelete:ODJsonConfig_DefaultCmdPermissionSettingsType + autodelete:ODJsonConfig_DefaultCmdPermissionSettingsType, + transfer:ODJsonConfig_DefaultCmdPermissionSettingsType, + topic:ODJsonConfig_DefaultCmdPermissionSettingsType, + priority:ODJsonConfig_DefaultCmdPermissionSettingsType, } /**## ODJsonConfig_DefaultSystemMessages `interface` @@ -171,35 +200,62 @@ export interface ODJsonConfig_DefaultSystemMessages { renaming:ODJsonConfig_DefaultMessageSettingsType, moving:ODJsonConfig_DefaultMessageSettingsType, blacklisting:ODJsonConfig_DefaultMessageSettingsType, - roleAdding:ODJsonConfig_DefaultMessageSettingsType, - roleRemoving:ODJsonConfig_DefaultMessageSettingsType + transferring:ODJsonConfig_DefaultMessageSettingsType, + topicChange:ODJsonConfig_DefaultMessageSettingsType, + priorityChange:ODJsonConfig_DefaultMessageSettingsType, + reactionRole:ODJsonConfig_DefaultMessageSettingsType, } /**## ODJsonConfig_DefaultSystem `interface` * All settings related to the ticket system. */ export interface ODJsonConfig_DefaultSystem { - /**Remove all participants (except admins) from the ticket when it's closed. */ - removeParticipantsOnClose:boolean, - /**Reply with an ephemeral message when a ticket is created. */ - replyOnTicketCreation:boolean, - /**Reply with an ephemeral message when reaction roles are changed. */ - replyOnReactionRole:boolean, - /**Use a translated config checker in the console. */ - useTranslatedConfigChecker:boolean, /**Prefer slash-commands over text-commands when displaying them in menu's and messages. */ preferSlashOverText:boolean, /**Reply with "unknown command" when the prefix is used without a valid command. */ sendErrorOnUnknownCommand:boolean, /**Display the question fields (in a ticket message) in code blocks. */ questionFieldsInCodeBlock:boolean, + /**Display embed fields together with question fields (in a ticket message). */ + displayFieldsWithQuestions:boolean, + /**Show global admins roles together with ticket admins in panel embeds. */ + showGlobalAdminsInPanelRoles:boolean, /**Disable the (✅❌) buttons and directly run the action. */ disableVerifyBars:boolean, /**Display error embeds/messages with red instead of the default bot color. */ useRedErrorEmbeds:boolean, + /**Always show the reason field in embeds, even when there is no reason provided. */ + alwaysShowReason:boolean, /**The emoji style used in the bot. This will affect all embeds, titles & messages in the bot. */ emojiStyle:"before"|"after"|"double"|"disabled", - + /**The emoji used when pinning tickets. This is '📌' by default. */ + pinEmoji:string, + + /**Reply with an ephemeral message when a ticket is created. */ + replyOnTicketCreation:boolean, + /**Reply with an ephemeral message when reaction roles are changed. */ + replyOnReactionRole:boolean, + /**Show a warning message before the ticket gets autoclosed. This will happen when only 1/4th of the autoclose time remains. */ + showPreAutocloseWarning:boolean, + /**Ask for the priority of this ticket on ticket creation. This will happen in a dropdown in the ticket message. */ + askPriorityOnTicketCreation:boolean, + /**Remove all participants (except admins) from the ticket when it's closed. */ + removeParticipantsOnClose:boolean, + /**Disable autoclose for a ticket when it has been closed and re-opened. */ + disableAutocloseAfterReopen:boolean, + /**Only allow autodelete when the ticket is already closed. */ + autodeleteRequiresClosedTicket:boolean, + /**When enabled, only global admins are able to delete a ticket without transcript. */ + adminOnlyDeleteWithoutTranscript:boolean, + /**Only allow ticket closing when at least 1 message has been sent by the creator. (admins are able to bypass) */ + allowCloseBeforeMessage:boolean, + /**Only allow ticket closing when at least 1 message has been sent by a global or ticket admin. (admins are able to bypass) */ + allowCloseBeforeAdminMessage:boolean, + /**Use a translated config checker in the console. */ + useTranslatedConfigChecker:boolean, + /**Pin the (first) ticket message in the channel. This simulates old behaviour like Open Ticket v1, v2 & v3. */ + pinFirstTicketMessage:boolean, + /**Enable/disable the ticket claim & unclaim button in the ticket message. */ enableTicketClaimButtons:boolean, /**Enable/disable the ticket close & re-open button in the ticket message. */ @@ -219,6 +275,9 @@ export interface ODJsonConfig_DefaultSystem { /**All settings related to global ticket limits. */ limits:ODJsonConfig_DefaultSystemLimits, + /**All global channel topic settings. */ + channelTopic:ODJsonConfig_DefaultSystemChannelTopic, + /**Configure permissions for all Open Ticket commands & actions. */ permissions:ODJsonConfig_DefaultSystemPermissions, @@ -366,8 +425,8 @@ export interface ODJsonConfig_DefaultOptionTicketChannelType { /**The category to move the ticket to when claimed by this user. */ category:string }[], - /**The channel description/topic shown at the top of the channel in discord. */ - description:string + /**The channel topic shown at the top of the channel in discord. */ + topic:string } /**## ODJsonConfig_DefaultOptionTicketType `interface` @@ -428,14 +487,14 @@ export interface ODJsonConfig_DefaultOptionTicketType extends ODJsonConfig_Defau /**Disable autodeleting when the ticket is claimed by someone. */ disableOnClaim:boolean }, - /**All settings related to the cooldown of this ticket type */ + /**All settings related to the cooldown of this ticket type. */ cooldown:{ /**Enable cooldown (per user) */ enabled:boolean, /**The amount of minutes a user needs to wait before being able to create a ticket again. */ cooldownMinutes:number }, - /**All settings related to the limits of this ticket type */ + /**All settings related to the limits of this ticket type. */ limits:{ /**Enable option ticket limits. */ enabled:boolean, @@ -443,6 +502,13 @@ export interface ODJsonConfig_DefaultOptionTicketType extends ODJsonConfig_Defau globalMaximum:number, /**The maximum amount of tickets of this type that a user is allowed to create at the same time. */ userMaximum:number + }, + /**All settings related to the slow mode of this ticket type. */ + slowMode:{ + /**Enable channel slow mode. */ + enabled:boolean, + /**The amount of seconds users need to wait between sending messages. */ + slowModeSeconds:number } } diff --git a/src/core/api/openticket/option.ts b/src/core/api/openticket/option.ts index 75e741a..6741616 100644 --- a/src/core/api/openticket/option.ts +++ b/src/core/api/openticket/option.ts @@ -156,7 +156,7 @@ export interface ODTicketOptionIds { "opendiscord:channel-category-closed":ODOptionData, "opendiscord:channel-category-backup":ODOptionData, "opendiscord:channel-categories-claimed":ODOptionData<{user:string,category:string}[]>, - "opendiscord:channel-description":ODOptionData, + "opendiscord:channel-topic":ODOptionData, "opendiscord:dm-message-enabled":ODOptionData, "opendiscord:dm-message-text":ODOptionData, @@ -183,6 +183,9 @@ export interface ODTicketOptionIds { "opendiscord:limits-enabled":ODOptionData, "opendiscord:limits-maximum-global":ODOptionData, "opendiscord:limits-maximum-user":ODOptionData + + "opendiscord:slowmode-enabled":ODOptionData, + "opendiscord:slowmode-seconds":ODOptionData, } /**## ODTicketOption `class` diff --git a/src/core/api/openticket/ticket.ts b/src/core/api/openticket/ticket.ts index 7b03243..2d8203a 100644 --- a/src/core/api/openticket/ticket.ts +++ b/src/core/api/openticket/ticket.ts @@ -158,6 +158,7 @@ export interface ODTicketIds { "opendiscord:ticket-message":ODTicketData, "opendiscord:participants":ODTicketData<{type:"role"|"user",id:string}[]>, "opendiscord:channel-suffix":ODTicketData, + "opendiscord:previous-creators":ODTicketData, "opendiscord:open":ODTicketData, "opendiscord:opened-by":ODTicketData, @@ -165,6 +166,9 @@ export interface ODTicketIds { "opendiscord:closed":ODTicketData, "opendiscord:closed-by":ODTicketData, "opendiscord:closed-on":ODTicketData, + "opendiscord:reopened":ODTicketData, + "opendiscord:reopened-by":ODTicketData, + "opendiscord:reopened-on":ODTicketData, "opendiscord:claimed":ODTicketData, "opendiscord:claimed-by":ODTicketData, "opendiscord:claimed-on":ODTicketData, @@ -183,6 +187,10 @@ export interface ODTicketIds { "opendiscord:autodelete-days":ODTicketData, "opendiscord:answers":ODTicketData<{id:string,name:string,type:"short"|"paragraph",value:string|null}[]>, + "opendiscord:priority":ODTicketData, + "opendiscord:topic":ODTicketData, + "opendiscord:message-sent":ODTicketData, + "opendiscord:admin-message-sent":ODTicketData, } /**## ODTicket `class` diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 7378dad..8dc6312 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -457,11 +457,11 @@ async function renderQuickSetupStatusType(backFn:() => api.ODPromiseVoid){ }).promise if (answer.canceled) return await backFn() - else if (answer.selectedIndex == 0) quickSetupStorage.status = {enabled:false,status:"online",type:"custom",text:""} - else if (answer.selectedIndex == 1) quickSetupStorage.status = {enabled:true,status:"online",type:"custom",text:""} - else if (answer.selectedIndex == 2) quickSetupStorage.status = {enabled:true,status:"online",type:"listening",text:""} - else if (answer.selectedIndex == 3) quickSetupStorage.status = {enabled:true,status:"online",type:"watching",text:""} - else if (answer.selectedIndex == 4) quickSetupStorage.status = {enabled:true,status:"online",type:"playing",text:""} + else if (answer.selectedIndex == 0) quickSetupStorage.status = {enabled:false,mode:"online",type:"custom",text:"",state:""} + else if (answer.selectedIndex == 1) quickSetupStorage.status = {enabled:true,mode:"online",type:"custom",text:"",state:""} + else if (answer.selectedIndex == 2) quickSetupStorage.status = {enabled:true,mode:"online",type:"listening",text:"",state:""} + else if (answer.selectedIndex == 3) quickSetupStorage.status = {enabled:true,mode:"online",type:"watching",text:"",state:""} + else if (answer.selectedIndex == 4) quickSetupStorage.status = {enabled:true,mode:"online",type:"playing",text:"",state:""} if (answer.selectedIndex == 0) return await renderQuickSetupLogs(async () => {await renderQuickSetupStatusType(backFn)}) else return await renderQuickSetupStatusText(async () => {await renderQuickSetupStatusType(backFn)}) @@ -1223,19 +1223,32 @@ async function saveQuickSetupConfig(){ slashCommands:quickSetupStorage.slashCommands ?? false, textCommands:quickSetupStorage.textCommands ?? false, - status:quickSetupStorage.status ?? {enabled:false,status:"online",text:"",type:"custom"}, + status:quickSetupStorage.status ?? {enabled:false,mode:"online",type:"custom",text:"",state:""}, system:{ - removeParticipantsOnClose:quickSetupStorage.removeParticipantsOnClose ?? false, - replyOnTicketCreation:false, - replyOnReactionRole:true, - useTranslatedConfigChecker:true, preferSlashOverText:quickSetupStorage.slashCommands ?? false, sendErrorOnUnknownCommand:true, questionFieldsInCodeBlock:true, + displayFieldsWithQuestions:false, + showGlobalAdminsInPanelRoles:false, disableVerifyBars:false, useRedErrorEmbeds:true, + alwaysShowReason:false, emojiStyle:quickSetupStorage.emojiStyle ?? "before", + pinEmoji:"📌", + + replyOnTicketCreation:false, + replyOnReactionRole:true, + showPreAutocloseWarning:false, + askPriorityOnTicketCreation:false, + removeParticipantsOnClose:quickSetupStorage.removeParticipantsOnClose ?? false, + disableAutocloseAfterReopen:true, + autodeleteRequiresClosedTicket:true, + adminOnlyDeleteWithoutTranscript:true, + allowCloseBeforeMessage:false, + allowCloseBeforeAdminMessage:true, + useTranslatedConfigChecker:true, + pinFirstTicketMessage:false, enableTicketClaimButtons:true, enableTicketCloseButtons:true, @@ -1254,6 +1267,18 @@ async function saveQuickSetupConfig(){ globalMaximum:100, userMaximum:quickSetupStorage.globalUserLimit ?? 3 }, + + channelTopic:{ + showOptionName:true, + showOptionDescription:false, + showOptionTopic:true, + showClosed:true, + showClaimed:false, + showPinned:false, + showPriority:false, + showCreator:false, + showParticipants:false + }, permissions:{ help:"everyone", @@ -1274,7 +1299,10 @@ async function saveQuickSetupConfig(){ stats:"everyone", clear:"admin", autoclose:"admin", - autodelete:"admin" + autodelete:"admin", + transfer:"admin", + topic:"admin", + priority:"admin", }, messages:{ @@ -1289,8 +1317,10 @@ async function saveQuickSetupConfig(){ renaming:{dm:false,logs:true}, moving:{dm:true,logs:true}, blacklisting:{dm:true,logs:true}, - roleAdding:{dm:false,logs:true}, - roleRemoving:{dm:false,logs:true} + transferring:{dm:true,logs:true}, + topicChange:{dm:false,logs:true}, + priorityChange:{dm:false,logs:true}, + reactionRole:{dm:false,logs:true} } } } @@ -1360,7 +1390,7 @@ async function saveQuickSetupConfig(){ closedCategory:"", backupCategory:"", claimedCategory:[], - description:ticket.description + topic:ticket.description }, dmMessage:{ @@ -1418,6 +1448,10 @@ async function saveQuickSetupConfig(){ enabled:false, globalMaximum:20, userMaximum:3 + }, + slowMode:{ + enabled:false, + slowModeSeconds:20 } } }) diff --git a/src/core/startup/init.ts b/src/core/startup/init.ts index a243ebe..6d1478d 100644 --- a/src/core/startup/init.ts +++ b/src/core/startup/init.ts @@ -147,7 +147,8 @@ export class ODVersionMigration { try{ await this.#func() return true - }catch{ + }catch(err){ + process.emit("uncaughtException",err) return false } } @@ -156,7 +157,8 @@ export class ODVersionMigration { try{ await this.#afterInitFunc() return true - }catch{ + }catch(err){ + process.emit("uncaughtException",err) return false } } diff --git a/src/core/startup/manageMigration.ts b/src/core/startup/manageMigration.ts index 82499c1..cd0467b 100644 --- a/src/core/startup/manageMigration.ts +++ b/src/core/startup/manageMigration.ts @@ -1,4 +1,5 @@ import {opendiscord, api, utilities} from "../../index" +import fs from "fs" /**Check if migration is required. Returns the last version used in the database. */ async function isMigrationRequired(): Promise { @@ -81,6 +82,22 @@ async function unloadMigrationContext(){ opendiscord.debug.debug("-- MIGRATION CONTEXT END --") } +/**Create a backup of the (dev)config & database before migrating. */ +function createMigrationBackup(){ + if (fs.existsSync("./.backup/")) fs.rmSync("./.backup/",{force:true,recursive:true}) + fs.mkdirSync("./.backup/") + + const devconfigFlag = opendiscord.flags.get("opendiscord:dev-config") + const isDevConfig = devconfigFlag ? devconfigFlag.value : false + const devDatabaseFlag = opendiscord.flags.get("opendiscord:dev-database") + const isDevDatabase = devDatabaseFlag ? devDatabaseFlag.value : false + + if (isDevConfig) fs.cpSync("./devconfig/","./.backup/devconfig/",{force:true,recursive:true}) + else fs.cpSync("./config/","./.backup/config/",{force:true,recursive:true}) + if (isDevDatabase) fs.cpSync("./devdatabase/","./.backup/devdatabase/",{force:true,recursive:true}) + else fs.cpSync("./database/","./.backup/database/",{force:true,recursive:true}) +} + /**Execute all version migration functions which are handled in the restricted migration context. */ async function loadAllVersionMigrations(lastVersion:api.ODVersion){ const migrations = (await import("./migration.js")).migrations @@ -90,6 +107,11 @@ async function loadAllVersionMigrations(lastVersion:api.ODVersion){ else if (comparison == "higher") return 1 else return -1 }) + if (migrations.length > 0){ + //create backup of config & database + createMigrationBackup() + } + for (const migration of migrations){ if (migration.version.compare(lastVersion) == "higher"){ const success = await migration.migrate() @@ -97,6 +119,7 @@ async function loadAllVersionMigrations(lastVersion:api.ODVersion){ {key:"success",value:success ? "true" : "false"}, {key:"afterInit",value:"false"} ]) + else throw new api.ODSystemError("Migration Error: Unable to migrate database & config to the new version of the bot.") } } } @@ -110,6 +133,11 @@ export async function loadAllAfterInitVersionMigrations(lastVersion:api.ODVersio else if (comparison == "higher") return 1 else return -1 }) + if (migrations.length > 0){ + //create backup of config & database + createMigrationBackup() + } + for (const migration of migrations){ if (migration.version.compare(lastVersion) == "higher"){ const success = await migration.migrateAfterInit() @@ -117,6 +145,7 @@ export async function loadAllAfterInitVersionMigrations(lastVersion:api.ODVersio {key:"success",value:success ? "true" : "false"}, {key:"afterInit",value:"true"} ]) + else throw new api.ODSystemError("Migration Error: Unable to migrate database & config to the new version of the bot.") } } } \ No newline at end of file diff --git a/src/core/startup/migration.ts b/src/core/startup/migration.ts index 18675a0..64376bc 100644 --- a/src/core/startup/migration.ts +++ b/src/core/startup/migration.ts @@ -31,5 +31,109 @@ export const migrations = [ new utilities.ODVersionMigration(api.ODVersion.fromString("opendiscord:version","v4.0.6"),async () => {},async () => {}), //MIGRATE TO v4.0.7 - new utilities.ODVersionMigration(api.ODVersion.fromString("opendiscord:version","v4.0.7"),async () => {},async () => {}) + new utilities.ODVersionMigration(api.ODVersion.fromString("opendiscord:version","v4.0.7"),async () => {},async () => {}), + + //MIGRATE TO v4.1.0 + new utilities.ODVersionMigration(api.ODVersion.fromString("opendiscord:version","v4.1.0"),async () => {},async () => { + //AFTER INIT MIGRATION + + //migrate config + const generalConfig = opendiscord.configs.get("opendiscord:general") + const optionConfig = opendiscord.configs.get("opendiscord:options") + + if (!generalConfig.data.status.state){ + //only migrate config when it hasn't been done manually by the user. + + if (!generalConfig.data._INFO) throw new api.ODSystemError("Couldn't find general.json '_INFO' category.") + generalConfig.data._INFO.version = "open-ticket-v4.1.0" + + if (!generalConfig.data.status) throw new api.ODSystemError("Couldn't find general.json 'status' category.") + generalConfig.data.status.mode = generalConfig.data.status["status"] ?? "online" + generalConfig.data.status.state = "" + delete generalConfig.data.status["status"] + + if (!generalConfig.data.system) throw new api.ODSystemError("Couldn't find general.json 'system' category.") + generalConfig.data.system.displayFieldsWithQuestions = false + generalConfig.data.system.showGlobalAdminsInPanelRoles = false + generalConfig.data.system.alwaysShowReason = false + generalConfig.data.system.pinEmoji = "📌" + generalConfig.data.system.showPreAutocloseWarning = false + generalConfig.data.system.askPriorityOnTicketCreation = false + generalConfig.data.system.disableAutocloseAfterReopen = true + generalConfig.data.system.autodeleteRequiresClosedTicket = true + generalConfig.data.system.adminOnlyDeleteWithoutTranscript = true + generalConfig.data.system.allowCloseBeforeMessage = false + generalConfig.data.system.allowCloseBeforeAdminMessage = true + generalConfig.data.system.pinFirstTicketMessage = false + + generalConfig.data.system.channelTopic = { + showOptionName:true, + showOptionDescription:false, + showOptionTopic:true, + showClosed:true, + showClaimed:false, + showPinned:false, + showPriority:false, + showCreator:false, + showParticipants:false + } + + if (!generalConfig.data.system.permissions) throw new api.ODSystemError("Couldn't find general.json 'system.permissions' category.") + generalConfig.data.system.permissions.transfer = "admin" + generalConfig.data.system.permissions.topic = "admin" + generalConfig.data.system.permissions.priority = "admin" + + if (!generalConfig.data.system.messages) throw new api.ODSystemError("Couldn't find general.json 'system.messages' category.") + generalConfig.data.system.messages.transferring = {dm:false,logs:true} + generalConfig.data.system.messages.topicChange = {dm:false,logs:true} + generalConfig.data.system.messages.priorityChange = {dm:false,logs:true} + generalConfig.data.system.messages.reactionRole = generalConfig.data.system.messages["roleAdding"] ?? {dm:false,logs:true} + delete generalConfig.data.system.messages["roleAdding"] + delete generalConfig.data.system.messages["roleRemoving"] + + for (const option of optionConfig.data){ + if (option.type != "ticket") continue + option.channel.topic = option.channel["description"] ?? "" + delete option.channel["description"] + + option.slowMode = { + enabled:false, + slowModeSeconds:20 + } + } + + await generalConfig.save() + await optionConfig.save() + } + + //migrate database + const optionDatabase = opendiscord.databases.get("opendiscord:options") + const ticketDatabase = opendiscord.databases.get("opendiscord:tickets") + + for (const option of (await optionDatabase.getCategory("opendiscord:used-option") ?? [])){ + const optionData = option.value + + const topicData = optionData.data.find((d) => d.id == "opendiscord:channel-description") + if (topicData) topicData.id = "opendiscord:channel-topic" + if (!optionData.data.find((d) => d.id == "opendiscord:slowmode-enabled")) optionData.data.push({id:"opendiscord:slowmode-enabled",value:false}) + if (!optionData.data.find((d) => d.id == "opendiscord:slowmode-seconds")) optionData.data.push({id:"opendiscord:slowmode-seconds",value:20}) + + optionDatabase.set("opendiscord:used-option",option.key,optionData) + } + + for (const ticket of (await ticketDatabase.getCategory("opendiscord:ticket") ?? [])){ + const ticketData = ticket.value + + if (!ticketData.data.find((d) => d.id == "opendiscord:previous-creators")) ticketData.data.push({id:"opendiscord:previous-creators",value:[]}) + if (!ticketData.data.find((d) => d.id == "opendiscord:reopened")) ticketData.data.push({id:"opendiscord:reopened",value:false}) + if (!ticketData.data.find((d) => d.id == "opendiscord:reopened-by")) ticketData.data.push({id:"opendiscord:reopened-by",value:null}) + if (!ticketData.data.find((d) => d.id == "opendiscord:reopened-on")) ticketData.data.push({id:"opendiscord:reopened-on",value:null}) + if (!ticketData.data.find((d) => d.id == "opendiscord:priority")) ticketData.data.push({id:"opendiscord:priority",value:-1}) + if (!ticketData.data.find((d) => d.id == "opendiscord:topic")) ticketData.data.push({id:"opendiscord:topic",value:""}) + if (!ticketData.data.find((d) => d.id == "opendiscord:message-sent")) ticketData.data.push({id:"opendiscord:message-sent",value:true}) + if (!ticketData.data.find((d) => d.id == "opendiscord:admin-message-sent")) ticketData.data.push({id:"opendiscord:admin-message-sent",value:true}) + + ticketDatabase.set("opendiscord:ticket",ticket.key,ticketData) + } + }) ] \ No newline at end of file diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 1698b00..adab440 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -245,8 +245,9 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis checker:new api.ODCheckerObjectStructure("opendiscord:status",{children:[ {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:status-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the status. When disabled, the bot will be online without any status."})}, {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-type",{choices:["listening","watching","playing","custom"],cliDisplayName:"Type",cliDisplayDescription:"The type of status: Listening, Watching, Playing or Custom."})}, + {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-mode",{choices:["online","invisible","idle","dnd"],cliDisplayName:"Mode",cliDisplayDescription:"The profile status/mode of the bot: Online, Invisible, Idle or Do Not Disturb."})}, {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-text",{minLength:1,maxLength:128,cliDisplayName:"Text",cliDisplayDescription:"The text displayed in the status."})}, - {key:"status",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-status",{choices:["online","invisible","idle","dnd"],cliDisplayName:"Status",cliDisplayDescription:"The profile status of the bot: Online, Invisible, Idle or Do Not Disturb."})}, + {key:"state",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-state",{maxLength:128,cliDisplayName:"State",cliDisplayDescription:"Additional text displayed below the status 'text'."})}, ],cliDisplayName:"Bot Status",cliDisplayDescription:"Manage the status of the bot."}), cliDisplayName:"Bot Status", cliDisplayDescription:"Manage the status of the bot." @@ -254,16 +255,29 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis //SYSTEM {key:"system",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system",{children:[ - {key:"removeParticipantsOnClose",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:remove-participants-on-close",{cliDisplayName:"Remove Participants On Close",cliDisplayDescription:"When enabled, all participants except admins will be removed from the ticket."})}, - {key:"replyOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-ticket-creation",{cliDisplayName:"Reply On Ticket Creation",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when creating a ticket."})}, - {key:"replyOnReactionRole",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-reaction-role",{cliDisplayName:"Reply On Reaction Role",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when using a role button."})}, - {key:"useTranslatedConfigChecker",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-translated-config-checker",{cliDisplayName:"Use Translated Config Checker",cliDisplayDescription:"Use a translated config checker to better understand the errors the bot gives."})}, {key:"preferSlashOverText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:prefer-slash-over-text",{cliDisplayName:"Prefer Slash Over Text",cliDisplayDescription:"Prefer displaying slash commands over text commands in help menus."})}, {key:"sendErrorOnUnknownCommand",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:send-error-on-unknown-command",{cliDisplayName:"Send Error On Unknown Command",cliDisplayDescription:"Send an error when using the text-command prefix without a valid command."})}, {key:"questionFieldsInCodeBlock",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-fields-in-code-block",{cliDisplayName:"Questions Fields In Code Blocks",cliDisplayDescription:"Display question fields in code blocks instead of plain text."})}, + {key:"displayFieldsWithQuestions",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:display-fields-with-questions",{cliDisplayName:"Display Fields With Questions",cliDisplayDescription:"Display embed fields together with question fields (in a ticket message)."})}, + {key:"showGlobalAdminsInPanelRoles",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:global-admins-in-panel-roles",{cliDisplayName:"Show Global Admins In Panel Roles",cliDisplayDescription:"Show global admins roles together with ticket admins in panel embeds."})}, {key:"disableVerifyBars",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:disable-verify-bars",{cliDisplayName:"Disable Verifybars",cliDisplayDescription:"Disable the (✅/❌) verify buttons in all commands. (Not recommended)"})}, {key:"useRedErrorEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-red-error-embeds",{cliDisplayName:"Use Red Error Embeds",cliDisplayDescription:"Display all error messages with a red border instead of the default color of the bot."})}, + {key:"alwaysShowReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:always-show-reason",{cliDisplayName:"Always Show Reason",cliDisplayDescription:"Always show the reason field in embeds, even when there is no reason provided."})}, {key:"emojiStyle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:emoji-style",{choices:["before","after","double","disabled"],cliDisplayName:"Emoji Style",cliDisplayDescription:"Choose how the bot will display emojis in message titles. (Visit docs for more info)"})}, + {key:"pinEmoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:pin-emoji",1,1,false,{cliDisplayName:"Pin Emoji",cliDisplayDescription:"The emoji used when pinning tickets. This is '📌' by default."})}, + + {key:"replyOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-ticket-creation",{cliDisplayName:"Reply On Ticket Creation",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when creating a ticket."})}, + {key:"replyOnReactionRole",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-reaction-role",{cliDisplayName:"Reply On Reaction Role",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when using a role button."})}, + {key:"showPreAutocloseWarning",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:show-pre-autoclose-warning",{cliDisplayName:"Show Pre-Autoclose Warning",cliDisplayDescription:"Show a warning message before the ticket gets autoclosed. This will happen when only 1/4th of the autoclose time remains."})}, + {key:"askPriorityOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ask-priority-creation",{cliDisplayName:"Ask Priority On Ticket Creation",cliDisplayDescription:"Ask for the priority of this ticket on ticket creation. This will happen in a dropdown in the ticket message."})}, + {key:"removeParticipantsOnClose",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:remove-participants-on-close",{cliDisplayName:"Remove Participants On Close",cliDisplayDescription:"When enabled, all participants except admins will be removed from the ticket."})}, + {key:"disableAutocloseAfterReopen",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:disable-autoclose-reopen",{cliDisplayName:"Disable Autoclose On Reopen",cliDisplayDescription:"Disable autoclose for a ticket when it has been closed and re-opened."})}, + {key:"autodeleteRequiresClosedTicket",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:autodelete-requires-closed-ticket",{cliDisplayName:"Autodelete Requires Closed Ticket",cliDisplayDescription:"Only allow autodelete when the ticket is already closed."})}, + {key:"adminOnlyDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:adminonly-delete-without-transcript",{cliDisplayName:"Admin-only Delete Without Transcript",cliDisplayDescription:"When enabled, only global admins are able to delete a ticket without transcript."})}, + {key:"allowCloseBeforeMessage",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:allow-close-before-message",{cliDisplayName:"Allow Close Before Message",cliDisplayDescription:"Only allow ticket closing when at least 1 message has been sent by the creator. (admins are able to bypass)"})}, + {key:"allowCloseBeforeAdminMessage",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:allow-close-before-admin-message",{cliDisplayName:"Allow Close Before Admin Message",cliDisplayDescription:"Only allow ticket closing when at least 1 message has been sent by a global or ticket admin. (admins are able to bypass)"})}, + {key:"useTranslatedConfigChecker",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-translated-config-checker",{cliDisplayName:"Use Translated Config Checker",cliDisplayDescription:"Use a translated config checker to better understand the errors the bot gives."})}, + {key:"pinFirstTicketMessage",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:pin-first-ticket-message",{cliDisplayName:"Pin First Ticket Message",cliDisplayDescription:"Pin the (first) ticket message in the channel. This simulates old behaviour like Open Ticket v1, v2 & v3."})}, {key:"enableTicketClaimButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-claim-buttons",{cliDisplayName:"Enable Ticket Claim Buttons",cliDisplayDescription:"Enable/disable buttons for claiming a ticket. Be aware that this doesn't disable the command!"})}, {key:"enableTicketCloseButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-close-buttons",{cliDisplayName:"Enable Ticket Close Buttons",cliDisplayDescription:"Enable/disable buttons for closing a ticket. Be aware that this doesn't disable the command!"})}, @@ -283,6 +297,19 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets from a specific user that are able to exist in the server at the same time."})} ],cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."}),cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."})}, + {key:"channelTopic",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:channel-topic",{children:[ + {key:"showOptionName",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-name",{cliDisplayName:"Show Option Name",cliDisplayDescription:"Show the option name in the channel topic."})}, + {key:"showOptionDescription",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-description",{cliDisplayName:"Show Option Description",cliDisplayDescription:"Show the option description in the channel topic."})}, + {key:"showOptionTopic",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-topic",{cliDisplayName:"Show Option Topic",cliDisplayDescription:"Show the option topic text in the channel topic (configured in the options.json config)."})}, + {key:"showClosed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-closed",{cliDisplayName:"Show Closed Status",cliDisplayDescription:"Show the current close/reopen status in the channel topic (auto-updated)."})}, + {key:"showClaimed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-claimed",{cliDisplayName:"Show Claimed Status",cliDisplayDescription:"Show the current claim status in the channel topic (auto-updated)."})}, + {key:"showPinned",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-pinned",{cliDisplayName:"Show Pinned Status",cliDisplayDescription:"Show the current pin status in the channel topic (auto-updated)."})}, + {key:"showPriority",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-priority",{cliDisplayName:"Show Priority",cliDisplayDescription:"Show the current priority in the channel topic (auto-updated)."})}, + {key:"showCreator",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-creator",{cliDisplayName:"Show Creator",cliDisplayDescription:"Show the creator of the ticket in the channel topic (auto-updated on transfer)."})}, + {key:"showParticipants",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-participants",{cliDisplayName:"Show Participants",cliDisplayDescription:"Show the first 5 participants of the ticket in the channel topic (auto-updated)."})}, + + ],cliDisplayName:"Channel Topic",cliDisplayDescription:"Manage stats and text of ticket channel topics."})}, + {key:"permissions",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ {key:"help",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-help","role",false,["admin","everyone","none"],{cliDisplayName:"Help",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, {key:"panel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-panel","role",false,["admin","everyone","none"],{cliDisplayName:"Panel",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, @@ -302,7 +329,10 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"stats",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-stats","role",false,["admin","everyone","none"],{cliDisplayName:"Stats",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, {key:"clear",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-clear","role",false,["admin","everyone","none"],{cliDisplayName:"Clear Tickets",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autoclose","role",false,["admin","everyone","none"],{cliDisplayName:"Autoclose",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autodelete","role",false,["admin","everyone","none"],{cliDisplayName:"Autodelete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})} + {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autodelete","role",false,["admin","everyone","none"],{cliDisplayName:"Autodelete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"transfer",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-transfer","role",false,["admin","everyone","none"],{cliDisplayName:"Transfer",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"topic",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-topic","role",false,["admin","everyone","none"],{cliDisplayName:"Topic",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"priority",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-priority","role",false,["admin","everyone","none"],{cliDisplayName:"Priority",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, ],cliDisplayName:"Permissions",cliDisplayDescription:"Manage all button & command permissions in the bot. (Visit docs for more info)"})}, {key:"messages",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ @@ -317,8 +347,10 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"renaming",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-renaming","Ticket Renamed")}, {key:"moving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-moving","Ticket Moved")}, {key:"blacklisting",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-blacklisting","User Blacklisted")}, - {key:"roleAdding",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-role-adding","Role Added")}, - {key:"roleRemoving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-role-removing","Role Removed")} + {key:"transferring",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-transferring","Ticket Transferred")}, + {key:"topicChange",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-topic-change","Topic Changed")}, + {key:"priorityChange",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-priority-change","Priority Changed")}, + {key:"reactionRole",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-reaction-role","Reaction Role")}, ],cliDisplayName:"Messages",cliDisplayDescription:"Manage all messages & DM's for each action of the bot. (Visit docs for more info)"})}, ],cliDisplayName:"System",cliDisplayDescription:"Configure everything related to the ticket system."})} ],cliDisplayName:"General",cliDisplayDescription:"General settings for the bot."}) @@ -369,7 +401,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc {key:"user",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-user","user",false,[],{cliDisplayName:"User",cliDisplayDescription:"A discord user ID of the ticket claimer."})}, {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-category","category",false,[],{cliDisplayName:"Category",cliDisplayDescription:"A discord category ID to move the ticket to."})} ],cliDisplayName:"Claimed Category",cliDisplayDescription:"A collection of a user ID and a category ID. The ticket will be moved to the category when this user claims the ticket."}),cliDisplayName:"Claimed Categories",cliDisplayDescription:"Add categories to move the ticket to when a user claims a ticket."})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-description",{cliDisplayName:"Channel Description",cliDisplayDescription:"The description of the ticket channel. Visible in the discord client."})}, + {key:"topic",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-topic",{cliDisplayName:"Channel Topic",cliDisplayDescription:"The topic text of the ticket channel. Visible in the discord client when general.json 'channelTopic'.'showOptionTopic' is enabled."})}, ],cliDisplayName:"Channel",cliDisplayDescription:"Manage all settings related to the ticket channel and categories."})}, //DM MESSAGE @@ -415,6 +447,13 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:10,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option that are able to exist in the server at the same time."})}, {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:3,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option from a specific user that are able to exist in the server at the same time."})} ],cliDisplayName:"Option Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."}),cliDisplayName:"Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."})}, + + //SLOW MODE + {key:"slowMode",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-slowmode",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-limits",{children:[ + {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-slowmode-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable ticket slow mode."})}, + {key:"slowModeSeconds",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-slowmode-seconds",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:20,cliDisplayName:"Slow Mode Seconds",cliDisplayDescription:"The amount of seconds users need to wait between sending messages."})}, + ],cliDisplayName:"Option Slow Mode",cliDisplayDescription:"Add slow mode to this ticket option to restrict the amount of message spam users can send."}),cliDisplayName:"Slow Mode",cliDisplayDescription:"Add slow mode to this ticket option to restrict the amount of message spam users can send."})}, + ],cliDisplayName:"Ticket Option",cliDisplayDescription:"Manage all ticket-specific settings of this option/type."})}, //WEBSITE diff --git a/src/data/framework/configLoader.ts b/src/data/framework/configLoader.ts index 54a0a63..f902249 100644 --- a/src/data/framework/configLoader.ts +++ b/src/data/framework/configLoader.ts @@ -55,21 +55,35 @@ export const defaultGeneralFormatter = new fjs.ObjectFormatter(null,true,[ new fjs.ObjectFormatter("status",true,[ new fjs.PropertyFormatter("enabled"), new fjs.PropertyFormatter("type"), + new fjs.PropertyFormatter("mode"), new fjs.PropertyFormatter("text"), - new fjs.PropertyFormatter("status"), + new fjs.PropertyFormatter("state"), ]), new fjs.TextFormatter(""), new fjs.ObjectFormatter("system",true,[ - new fjs.PropertyFormatter("removeParticipantsOnClose"), - new fjs.PropertyFormatter("replyOnTicketCreation"), - new fjs.PropertyFormatter("replyOnReactionRole"), - new fjs.PropertyFormatter("useTranslatedConfigChecker"), new fjs.PropertyFormatter("preferSlashOverText"), new fjs.PropertyFormatter("sendErrorOnUnknownCommand"), new fjs.PropertyFormatter("questionFieldsInCodeBlock"), + new fjs.PropertyFormatter("displayFieldsWithQuestions"), + new fjs.PropertyFormatter("showGlobalAdminsInPanelRoles"), new fjs.PropertyFormatter("disableVerifyBars"), new fjs.PropertyFormatter("useRedErrorEmbeds"), + new fjs.PropertyFormatter("alwaysShowReason"), new fjs.PropertyFormatter("emojiStyle"), + new fjs.PropertyFormatter("pinEmoji"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("replyOnTicketCreation"), + new fjs.PropertyFormatter("replyOnReactionRole"), + new fjs.PropertyFormatter("showPreAutocloseWarning"), + new fjs.PropertyFormatter("askPriorityOnTicketCreation"), + new fjs.PropertyFormatter("removeParticipantsOnClose"), + new fjs.PropertyFormatter("disableAutocloseAfterReopen"), + new fjs.PropertyFormatter("autodeleteRequiresClosedTicket"), + new fjs.PropertyFormatter("adminOnlyDeleteWithoutTranscript"), + new fjs.PropertyFormatter("allowCloseBeforeMessage"), + new fjs.PropertyFormatter("allowCloseBeforeAdminMessage"), + new fjs.PropertyFormatter("useTranslatedConfigChecker"), + new fjs.PropertyFormatter("pinFirstTicketMessage"), new fjs.TextFormatter(""), new fjs.PropertyFormatter("enableTicketClaimButtons"), new fjs.PropertyFormatter("enableTicketCloseButtons"), @@ -89,6 +103,18 @@ export const defaultGeneralFormatter = new fjs.ObjectFormatter(null,true,[ new fjs.PropertyFormatter("userMaximum"), ]), new fjs.TextFormatter(""), + new fjs.ObjectFormatter("channelTopic",true,[ + new fjs.PropertyFormatter("showOptionName"), + new fjs.PropertyFormatter("showOptionDescription"), + new fjs.PropertyFormatter("showOptionTopic"), + new fjs.PropertyFormatter("showClosed"), + new fjs.PropertyFormatter("showClaimed"), + new fjs.PropertyFormatter("showPinned"), + new fjs.PropertyFormatter("showPriority"), + new fjs.PropertyFormatter("showCreator"), + new fjs.PropertyFormatter("showParticipants"), + ]), + new fjs.TextFormatter(""), new fjs.ObjectFormatter("permissions",true,[ new fjs.PropertyFormatter("help"), new fjs.PropertyFormatter("panel"), @@ -109,6 +135,9 @@ export const defaultGeneralFormatter = new fjs.ObjectFormatter(null,true,[ new fjs.PropertyFormatter("clear"), new fjs.PropertyFormatter("autoclose"), new fjs.PropertyFormatter("autodelete"), + new fjs.PropertyFormatter("transfer"), + new fjs.PropertyFormatter("topic"), + new fjs.PropertyFormatter("priority"), ]), new fjs.TextFormatter(""), new fjs.ObjectFormatter("messages",true,[ @@ -123,8 +152,10 @@ export const defaultGeneralFormatter = new fjs.ObjectFormatter(null,true,[ new fjs.DefaultFormatter("renaming",false), new fjs.DefaultFormatter("moving",false), new fjs.DefaultFormatter("blacklisting",false), - new fjs.DefaultFormatter("roleAdding",false), - new fjs.DefaultFormatter("roleRemoving",false), + new fjs.DefaultFormatter("transferring",false), + new fjs.DefaultFormatter("topicChange",false), + new fjs.DefaultFormatter("priorityChange",false), + new fjs.DefaultFormatter("reactionRole",false) ]), ]), ]) @@ -187,7 +218,7 @@ export const defaultOptionsFormatter = new fjs.ArrayFormatter(null,true,new fjs. new fjs.PropertyFormatter("user"), new fjs.PropertyFormatter("category"), ])), - new fjs.PropertyFormatter("description"), + new fjs.PropertyFormatter("topic"), ]), new fjs.TextFormatter(""), new fjs.ObjectFormatter("dmMessage",true,[ @@ -254,6 +285,10 @@ export const defaultOptionsFormatter = new fjs.ArrayFormatter(null,true,new fjs. new fjs.PropertyFormatter("globalMaximum"), new fjs.PropertyFormatter("userMaximum"), ]), + new fjs.ObjectFormatter("slowMode",true,[ + new fjs.PropertyFormatter("enabled"), + new fjs.PropertyFormatter("slowModeSeconds"), + ]), ])}, {key:"type",value:"website",formatter:new fjs.ObjectFormatter(null,true,[ new fjs.PropertyFormatter("id"), diff --git a/src/data/openticket/optionLoader.ts b/src/data/openticket/optionLoader.ts index 302f7d2..0124902 100644 --- a/src/data/openticket/optionLoader.ts +++ b/src/data/openticket/optionLoader.ts @@ -74,7 +74,7 @@ export const loadTicketOption = (option:api.ODJsonConfig_DefaultOptionTicketType new api.ODOptionData("opendiscord:channel-category-closed",option.channel.closedCategory), new api.ODOptionData("opendiscord:channel-category-backup",option.channel.backupCategory), new api.ODOptionData("opendiscord:channel-categories-claimed",option.channel.claimedCategory), - new api.ODOptionData("opendiscord:channel-description",option.channel.description), + new api.ODOptionData("opendiscord:channel-topic",option.channel.topic), new api.ODOptionData("opendiscord:dm-message-enabled",option.dmMessage.enabled), new api.ODOptionData("opendiscord:dm-message-text",option.dmMessage.text), @@ -100,7 +100,10 @@ export const loadTicketOption = (option:api.ODJsonConfig_DefaultOptionTicketType new api.ODOptionData("opendiscord:limits-enabled",option.limits.enabled), new api.ODOptionData("opendiscord:limits-maximum-global",option.limits.globalMaximum), - new api.ODOptionData("opendiscord:limits-maximum-user",option.limits.userMaximum) + new api.ODOptionData("opendiscord:limits-maximum-user",option.limits.userMaximum), + + new api.ODOptionData("opendiscord:slowmode-enabled",option.slowMode.enabled), + new api.ODOptionData("opendiscord:slowmode-seconds",option.slowMode.slowModeSeconds) ]) } diff --git a/src/data/openticket/ticketLoader.ts b/src/data/openticket/ticketLoader.ts index 33b7815..86b88ca 100644 --- a/src/data/openticket/ticketLoader.ts +++ b/src/data/openticket/ticketLoader.ts @@ -27,7 +27,10 @@ export const loadTicket = async (ticket:api.ODTicketJson) => { //manage backup option (+ sync version of options with latest OT version in database) if (configOption) await optionDatabase.set("opendiscord:used-option",configOption.id.value,configOption.toJson(opendiscord.versions.get("opendiscord:version"))) - else if (backupOption) opendiscord.options.add(backupOption) + else if (backupOption){ + opendiscord.options.add(backupOption) + await optionDatabase.set("opendiscord:used-option",backupOption.id.value,backupOption.toJson(opendiscord.versions.get("opendiscord:version"))) + } else throw new api.ODSystemError("Unable to use backup option! Normal option not found in config!") //load ticket & option diff --git a/src/index.ts b/src/index.ts index 587ae1c..e319bce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -412,7 +412,7 @@ const main = async () => { opendiscord.log("Loading client activity...","system") if (opendiscord.defaults.getDefault("clientActivityLoading")){ //load config status - if (generalConfig.data.status && generalConfig.data.status.enabled) opendiscord.client.activity.setStatus(generalConfig.data.status.type,generalConfig.data.status.text,generalConfig.data.status.status) + if (generalConfig.data.status && generalConfig.data.status.enabled) opendiscord.client.activity.setStatus(generalConfig.data.status.type,generalConfig.data.status.text,generalConfig.data.status.mode) } await opendiscord.events.get("onClientActivityLoad").emit([opendiscord.client.activity,opendiscord.client]) await opendiscord.events.get("afterClientActivityLoaded").emit([opendiscord.client.activity,opendiscord.client]) From 8436b19cbfc5ad9f2a8bff43606c7c5e6764b7dd Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:42:35 +0200 Subject: [PATCH 45/78] Updated bot versions to v4.1.0 --- .eggs/README.md | 5 ++- .eggs/openticket-egg-v4.1.0.json | 62 +++++++++++++++++++++++++++++++ .github/CONTRIBUTING.md | 2 +- .github/SECURITY.md | 23 ++++++------ README.md | 2 +- config/general.json | 2 +- config/panels.json | 2 +- languages/arabic.json | 2 +- languages/bengali.json | 2 +- languages/catalan.json | 2 +- languages/custom.json | 2 +- languages/czech.json | 2 +- languages/danish.json | 2 +- languages/dutch.json | 2 +- languages/english.json | 2 +- languages/estonian.json | 2 +- languages/finnish.json | 2 +- languages/french.json | 2 +- languages/german.json | 2 +- languages/greek.json | 2 +- languages/hindi.json | 2 +- languages/hungarian.json | 2 +- languages/indonesian.json | 2 +- languages/italian.json | 2 +- languages/japanese.json | 2 +- languages/korean.json | 2 +- languages/kurdish.json | 2 +- languages/latvian.json | 2 +- languages/lithuanian.json | 2 +- languages/norwegian.json | 2 +- languages/persian.json | 2 +- languages/polish.json | 2 +- languages/portuguese.json | 2 +- languages/romanian.json | 2 +- languages/russian.json | 2 +- languages/simplified-chinese.json | 2 +- languages/slovenian.json | 2 +- languages/spanish.json | 2 +- languages/swedish.json | 2 +- languages/tamil.json | 2 +- languages/thai.json | 2 +- languages/turkish.json | 2 +- languages/ukrainian.json | 2 +- languages/vietnamese.json | 2 +- package.json | 2 +- src/core/api/main.ts | 2 +- src/core/api/modules/base.ts | 8 ++-- src/index.ts | 2 +- 48 files changed, 125 insertions(+), 61 deletions(-) create mode 100644 .eggs/openticket-egg-v4.1.0.json diff --git a/.eggs/README.md b/.eggs/README.md index e439378..576d737 100644 --- a/.eggs/README.md +++ b/.eggs/README.md @@ -2,7 +2,7 @@ Open Ticket Logo [![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) -[![version](https://img.shields.io/badge/version-4.0.7-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.7) +[![version](https://img.shields.io/badge/version-4.1.0-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.1.0) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) [![Open Ticket supports Pterodactyl Eggs!](https://img.shields.io/badge/pterodactyl-supported-10539F?style=flat-square&logo=pterodactyl)](.eggs/README.md) @@ -20,6 +20,9 @@ It's recommended to provide at least `1GB` of **Memory/RAM** and `5GB` of **disk [**`openticket-egg-main.json` (Recommended)**](openticket-egg-main.json) - This egg will use the `main` branch of Open Ticket. +[**`openticket-egg-v4.1.0.json`**](openticket-egg-v4.1.0.json) +- This egg will always use Open Ticket `v4.1.0`. Open Ticket updates will not have an effect on this egg. + [**`openticket-egg-v4.0.7.json`**](openticket-egg-v4.0.7.json) - This egg will always use Open Ticket `v4.0.7`. Open Ticket updates will not have an effect on this egg. diff --git a/.eggs/openticket-egg-v4.1.0.json b/.eggs/openticket-egg-v4.1.0.json new file mode 100644 index 0000000..bb91ba9 --- /dev/null +++ b/.eggs/openticket-egg-v4.1.0.json @@ -0,0 +1,62 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", + "meta": { + "version": "PTDL_v2", + "update_url": null + }, + "exported_at": "2025-03-16T18:10:18+01:00", + "name": "Open Ticket (v4.1.0)", + "author": "support@dj-dj.be", + "description": "This is the official Pterodactyl egg for Open Ticket v4.1.0, the most advanced & customisable discord ticket bot that you will ever find! You can customise up to 300+ variables! This includes Html Transcripts, Advanced Plugins, Custom Embeds, Questions\/Modals, Stats & more!", + "features": null, + "docker_images": { + "ghcr.io\/parkervcp\/yolks:nodejs_20": "ghcr.io\/parkervcp\/yolks:nodejs_20" + }, + "file_denylist": [], + "startup": "if [[ ! -z ${NODE_PACKAGES} ]]; then npm install ${NODE_PACKAGES}; fi; if [[ ! -z ${UNNODE_PACKAGES} ]]; then npm uninstall ${UNNODE_PACKAGES}; fi; if [ -f \/home\/container\/package.json ]; then npm install; fi; node \"\/home\/container\/index.js\" ${NODE_FLAGS};", + "config": { + "files": "{}", + "startup": "{\r\n \"done\": \"STARTUP INFO:\"\r\n}", + "logs": "{}", + "stop": "^C" + }, + "scripts": { + "installation": { + "script": "#!\/bin\/bash\r\n# Open Ticket Installation Script (v4.1.0)\r\n# Inspired by: Node.js Installation Script\r\n# \u00a9 DJdj Development\r\n\r\necho -e \"[OT INSTALLER] installing dependencies. please wait...\"\r\napt update\r\napt install -y git curl\r\n\r\necho -e \"[OT INSTALLER] updating npm. please wait...\"\r\nnpm install npm@latest --location=global\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nBRANCH=\"v4.1.0\"\r\n\r\nif [ \"$(ls -A \/mnt\/server)\" ]; then\r\n echo -e \"[OT INSTALLER] \/mnt\/server directory is not empty.\"\r\n if [ -d .git ]; then\r\n echo -e \"[OT INSTALLER] .git directory exists\"\r\n if [ -f .git\/config ]; then\r\n echo -e \"[OT INSTALLER] loading info from git config\"\r\n ORIGIN=$(git config --get remote.origin.url)\r\n else\r\n echo -e \"[OT INSTALLER] files found with no git config\"\r\n echo -e \"[OT INSTALLER] closing out without touching things to not break anything\"\r\n exit 10\r\n fi\r\n fi\r\n\r\n if [ \"${ORIGIN}\" == \"https:\/\/github.com\/open-discord-bots\/open-ticket.git\" ]; then\r\n echo \"pulling latest from github\"\r\n git pull\r\n fi\r\nelse\r\n echo -e \"[OT INSTALLER] \/mnt\/server is empty.\"\r\n echo -e \"[OT INSTALLER] cloning files into repo.\"\r\n if [ -z ${BRANCH} ]; then\r\n echo -e \"[OT INSTALLER] cloning default branch\"\r\n git clone https:\/\/github.com\/open-discord-bots\/open-ticket.git .\r\n else\r\n echo -e \"[OT INSTALLER] cloning ${BRANCH}'\"\r\n git clone --single-branch --branch ${BRANCH} https:\/\/github.com\/open-discord-bots\/open-ticket.git .\r\n fi\r\nfi\r\n\r\necho -e \"[OT INSTALLER] installing Open Ticket node.js packages.\"\r\nif [ -f \/mnt\/server\/package.json ]; then\r\n \/usr\/local\/bin\/npm install --omit=dev\r\nfi\r\n\r\necho -e \"[OT INSTALLER] installing custom node.js packages.\"\r\nif [[ ! -z ${NODE_PACKAGES} ]]; then\r\n \/usr\/local\/bin\/npm install ${NODE_PACKAGES}\r\nfi\r\n\r\necho -e \"[OT INSTALLER] install complete!\"\r\nexit 0", + "container": "node:latest", + "entrypoint": "bash" + } + }, + "variables": [ + { + "name": "Additional Npm Packages", + "description": "Specify additional npm packages used by Open Ticket plugins. Use spaces to separate.", + "env_variable": "NODE_PACKAGES", + "default_value": "", + "user_viewable": false, + "user_editable": true, + "rules": "string|nullable", + "field_type": "text" + }, + { + "name": "Startup Flags", + "description": "Start the bot with additional Open Ticket flags. Separate using spaces.\r\nA full reference list can be found in the documentation.", + "env_variable": "NODE_FLAGS", + "default_value": "", + "user_viewable": false, + "user_editable": true, + "rules": "string|nullable", + "field_type": "text" + }, + { + "name": "Uninstall Npm Packages", + "description": "A list of npm packages to uninstall. Separate by spaces.", + "env_variable": "UNNODE_PACKAGES", + "default_value": "", + "user_viewable": false, + "user_editable": true, + "rules": "string|nullable", + "field_type": "text" + } + ] +} \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 087a2c9..5b8629a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing Guidelines Open Ticket Logo -[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.0.7-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.0.7) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) +[![discord](https://img.shields.io/badge/discord-support%20server-5865F2.svg?style=flat-square&logo=discord)](https://discord.com/invite/26vT9wt3n3) [![version](https://img.shields.io/badge/version-4.1.0-brightgreen.svg?style=flat-square)](https://github.com/open-discord-bots/open-ticket/releases/tag/v4.1.0) [![Sponsor DJj123dj](https://img.shields.io/badge/sponsor-DJj123dj-ea4aaa?style=flat-square&logo=githubsponsors)](https://github.com/sponsors/DJj123dj) These are the Contributing Guidelines of Open Ticket!
Here you can find everything you need to know about contributing to Open Ticket.
diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 9e7a2b7..e4ae1aa 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -17,18 +17,17 @@ This list will be updated on every release. - 🟧 Deprecated **(docs)** - ❌ Fully Deprecated -| Version | Supported | Notes | -|------------|-----------|------------------------------------------| -| 4.1.0 | 🟦 | | -| 4.0.7 | ✅ | | -| 4.0.6 | ✅ | | -| 4.0.5 | ✅ | | -| 4.0.4 | ✅ | Supported until July 2025 (LTS) | -| 4.0.3 | 🚧 | Transcripts v2.0 (Offline August 2025) | -| 4.0.2 | 🟧 | Documentation Only (discord.js bug) | -| 4.0.1 | 🟧 | Documentation Only | -| 4.0.0 | 🟧 | Documentation Only | -| < 4.0.0 | ❌ | | +| Version | Supported | Notes | +|------------|-----------|---------------------------------------------------------------| +| 4.2.0 | 🟦 | Transcripts v2.0 will be taken offline when v4.2 is released. | +| 4.1.1 | 🟦 | | +| 4.1.0 | ✅ | | +| 4.0.7 | ✅ | Supported Until October 2025 (LTS) | +| 4.0.6 | 🚧 | | +| 4.0.5 | 🚧 | | +| 4.0.4 | 🚧 | | +| < 4.0.4 | 🟧 | Deprecated Transcripts v2.0, Documentation Only | +| < 4.0.0 | ❌ | | ### 🕷️ Reporting Vulnerabilities You can report vulnerabilities, errors & bugs using one of the following methods: diff --git a/README.md b/README.md index 1ab25ca..c4f5e31 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
Powered By
Open Ticket
Discord Invite Link -Open Ticket Version +Open Ticket Version Open Ticket Documentation Open Ticket License Open Ticket Stars diff --git a/config/general.json b/config/general.json index 10106b8..f950d10 100644 --- a/config/general.json +++ b/config/general.json @@ -2,7 +2,7 @@ "_INFO":{ "support":"https://otdocs.dj-dj.be", "discord":"https://discord.dj-dj.be", - "version":"open-ticket-v4.0.7" + "version":"open-ticket-v4.1.0" }, "token":"insert your bot token here! (or leave empty when using 'tokenFromENV')", diff --git a/config/panels.json b/config/panels.json index 8b2d9d5..7c093b0 100644 --- a/config/panels.json +++ b/config/panels.json @@ -17,7 +17,7 @@ "image":"https://www.example.com/image.png (or leave empty)", "thumbnail":"https://www.example.com/image.png (or leave empty)", - "footer":"Open Ticket v4.0.7 (or leave empty)", + "footer":"Open Ticket v4.1.0 (or leave empty)", "fields":[ {"name":"field name","value":"field value","inline":false} ], diff --git a/languages/arabic.json b/languages/arabic.json index 28419b6..82cb98e 100644 --- a/languages/arabic.json +++ b/languages/arabic.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.7", + "otversion": "v4.1.0", "translators": ["palestinian"], "lastedited": "06/12/2024", "language": "Arabic", diff --git a/languages/bengali.json b/languages/bengali.json index 096246a..ad3d921 100644 --- a/languages/bengali.json +++ b/languages/bengali.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Bengali", diff --git a/languages/catalan.json b/languages/catalan.json index 34b078f..6f296d7 100644 --- a/languages/catalan.json +++ b/languages/catalan.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["guillee3"], "lastedited":"21/08/2024", "language":"Catalan", diff --git a/languages/custom.json b/languages/custom.json index 95a194b..60ed194 100644 --- a/languages/custom.json +++ b/languages/custom.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["DJj123dj"], "lastedited":"21/08/2024", "language":"Custom", diff --git a/languages/czech.json b/languages/czech.json index d8ea5a2..e23406f 100644 --- a/languages/czech.json +++ b/languages/czech.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["spyeye_"], "lastedited":"21/08/2024", "language":"Czech", diff --git a/languages/danish.json b/languages/danish.json index 5584331..e902380 100644 --- a/languages/danish.json +++ b/languages/danish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["the_gamer"], "lastedited":"26/09/2024", "language":"Danish", diff --git a/languages/dutch.json b/languages/dutch.json index ac49426..6168706 100644 --- a/languages/dutch.json +++ b/languages/dutch.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["DJj123dj"], "lastedited":"21/08/2024", "language":"Dutch", diff --git a/languages/english.json b/languages/english.json index 886fd42..9fe9246 100644 --- a/languages/english.json +++ b/languages/english.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["DJj123dj"], "lastedited":"21/08/2024", "language":"English", diff --git a/languages/estonian.json b/languages/estonian.json index 4d3ef65..e5d0ac4 100644 --- a/languages/estonian.json +++ b/languages/estonian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["iamnotmega","ChatGPT"], "lastedited":"21/10/2024", "language":"Estonian", diff --git a/languages/finnish.json b/languages/finnish.json index 67a39c6..e3a4589 100644 --- a/languages/finnish.json +++ b/languages/finnish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["iamnotmega","ChatGPT"], "lastedited":"21/10/2024", "language":"Finnish", diff --git a/languages/french.json b/languages/french.json index dd904aa..cc55ce9 100644 --- a/languages/french.json +++ b/languages/french.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["guillee3"], "lastedited":"04/12/2024", "language":"French", diff --git a/languages/german.json b/languages/german.json index 0cc8439..67e9b0e 100644 --- a/languages/german.json +++ b/languages/german.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["benzorich"], "lastedited":"06/10/2024", "language":"German", diff --git a/languages/greek.json b/languages/greek.json index ddb4cc9..bfd1dac 100644 --- a/languages/greek.json +++ b/languages/greek.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Greek", diff --git a/languages/hindi.json b/languages/hindi.json index 8edfd3f..78e4577 100644 --- a/languages/hindi.json +++ b/languages/hindi.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["an_developer"], "lastedited":"14/12/2024", "language":"Hindi", diff --git a/languages/hungarian.json b/languages/hungarian.json index a2ea9fc..7a0b035 100644 --- a/languages/hungarian.json +++ b/languages/hungarian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["Kornel0706"], "lastedited":"22/08/2024", "language":"Hungarian", diff --git a/languages/indonesian.json b/languages/indonesian.json index f454ef0..f9a43dd 100644 --- a/languages/indonesian.json +++ b/languages/indonesian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["erxg"], "lastedited":"25/08/2024", "language":"Indonesian", diff --git a/languages/italian.json b/languages/italian.json index 5de0595..aed6b27 100644 --- a/languages/italian.json +++ b/languages/italian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["fraden1mvp."], "lastedited":"08/10/2024", "language":"Italian", diff --git a/languages/japanese.json b/languages/japanese.json index 4e9ae5e..4b101f6 100644 --- a/languages/japanese.json +++ b/languages/japanese.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Japanese", diff --git a/languages/korean.json b/languages/korean.json index 97f9db7..d7fcf98 100644 --- a/languages/korean.json +++ b/languages/korean.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Korean", diff --git a/languages/kurdish.json b/languages/kurdish.json index e39ca75..b995c29 100644 --- a/languages/kurdish.json +++ b/languages/kurdish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Kurdish", diff --git a/languages/latvian.json b/languages/latvian.json index b5178bd..c45ca91 100644 --- a/languages/latvian.json +++ b/languages/latvian.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.7", + "otversion": "v4.1.0", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Latvian", diff --git a/languages/lithuanian.json b/languages/lithuanian.json index e2bc2b7..3dca0a4 100644 --- a/languages/lithuanian.json +++ b/languages/lithuanian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["TsgIndrius"], "lastedited":"26/01/2025", "language":"Lithuanian", diff --git a/languages/norwegian.json b/languages/norwegian.json index 7d336d9..59a98e5 100644 --- a/languages/norwegian.json +++ b/languages/norwegian.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.7", + "otversion": "v4.1.0", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Norwegian", diff --git a/languages/persian.json b/languages/persian.json index c297a2a..1f88c1f 100644 --- a/languages/persian.json +++ b/languages/persian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["dysashop","zhavis"], "lastedited":"28/05/2025", "language":"Persian", diff --git a/languages/polish.json b/languages/polish.json index da2b7da..e4df15d 100644 --- a/languages/polish.json +++ b/languages/polish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["DanoGlez"], "lastedited":"28/01/2025", "language":"Polish", diff --git a/languages/portuguese.json b/languages/portuguese.json index f891f35..82ca261 100644 --- a/languages/portuguese.json +++ b/languages/portuguese.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["quiradon"], "lastedited":"20/08/2024", "language":"Portuguese", diff --git a/languages/romanian.json b/languages/romanian.json index 9680832..6f471ce 100644 --- a/languages/romanian.json +++ b/languages/romanian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["SankeDev"], "lastedited":"27/08/2024", "language":"Romanian", diff --git a/languages/russian.json b/languages/russian.json index 4dcee03..de15f34 100644 --- a/languages/russian.json +++ b/languages/russian.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.7", + "otversion": "v4.1.0", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Russian", diff --git a/languages/simplified-chinese.json b/languages/simplified-chinese.json index 0bbe699..b6ebe4a 100644 --- a/languages/simplified-chinese.json +++ b/languages/simplified-chinese.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Simplified Chainese", diff --git a/languages/slovenian.json b/languages/slovenian.json index c82ad2d..37c3fca 100644 --- a/languages/slovenian.json +++ b/languages/slovenian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Solvenian", diff --git a/languages/spanish.json b/languages/spanish.json index aa8a9c9..7fb2552 100644 --- a/languages/spanish.json +++ b/languages/spanish.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["Redactado","Josuens"], "lastedited":"22/08/2024", "language":"Spanish", diff --git a/languages/swedish.json b/languages/swedish.json index 094963b..f8c24f6 100644 --- a/languages/swedish.json +++ b/languages/swedish.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.7", + "otversion": "v4.1.0", "translators": ["NoOneNook"], "lastedited": "25/03/2025", "language": "Svenska", diff --git a/languages/tamil.json b/languages/tamil.json index 7ea4393..69ae29a 100644 --- a/languages/tamil.json +++ b/languages/tamil.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["HanumeshGupta"], "lastedited":"19/06/2025", "language":"Tamil", diff --git a/languages/thai.json b/languages/thai.json index 38b733e..1a937cc 100644 --- a/languages/thai.json +++ b/languages/thai.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["modshd"], "lastedited":"13/11/2024", "language":"Thai", diff --git a/languages/turkish.json b/languages/turkish.json index ec60dda..c327bc8 100644 --- a/languages/turkish.json +++ b/languages/turkish.json @@ -1,6 +1,6 @@ { "_TRANSLATION": { - "otversion": "v4.0.7", + "otversion": "v4.1.0", "translators": ["palestinian"], "lastedited": "26/11/2024", "language": "Turkish", diff --git a/languages/ukrainian.json b/languages/ukrainian.json index 2f7d805..9e7831d 100644 --- a/languages/ukrainian.json +++ b/languages/ukrainian.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["Anderskiy"], "lastedited":"31/08/2024", "language":"Ukrainian", diff --git a/languages/vietnamese.json b/languages/vietnamese.json index af2b358..6f72614 100644 --- a/languages/vietnamese.json +++ b/languages/vietnamese.json @@ -1,6 +1,6 @@ { "_TRANSLATION":{ - "otversion":"v4.0.7", + "otversion":"v4.1.0", "translators":["ngocdiep2006"], "lastedited":"07/04/2025", "language":"Vietnamese", diff --git a/package.json b/package.json index e2db622..c5d0b7d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "open-ticket", "author": "DJdj Development", - "version": "4.0.7", + "version": "4.1.0", "description": "The most advanced open-source discord ticket bot with HTML transcripts, plugins, questions, claiming, pinning & more! Using discord.js v14 & JSON database! ", "keywords": [ "ticket-bot", diff --git a/src/core/api/main.ts b/src/core/api/main.ts index 063213e..ddb8a25 100644 --- a/src/core/api/main.ts +++ b/src/core/api/main.ts @@ -130,7 +130,7 @@ export class ODMain { constructor(){ this.versions = new ODVersionManager_Default() - this.versions.add(ODVersion.fromString("opendiscord:version","v4.0.7")) + this.versions.add(ODVersion.fromString("opendiscord:version","v4.1.0")) this.versions.add(ODVersion.fromString("opendiscord:api","v1.0.0")) this.versions.add(ODVersion.fromString("opendiscord:transcripts","v2.0.0")) this.versions.add(ODVersion.fromString("opendiscord:livestatus","v2.0.0")) diff --git a/src/core/api/modules/base.ts b/src/core/api/modules/base.ts index 433ba87..d70e03d 100644 --- a/src/core/api/modules/base.ts +++ b/src/core/api/modules/base.ts @@ -560,8 +560,8 @@ export class ODHTTPGetRequest { this.throwOnError = throwOnError const newConfig = config ?? {} newConfig.method = "GET" - if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.0.7"}) - else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.0.7"} + if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.1.0"}) + else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.1.0"} this.config = newConfig } @@ -615,8 +615,8 @@ export class ODHTTPPostRequest { this.throwOnError = throwOnError const newConfig = config ?? {} newConfig.method = "POST" - if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.0.7"}) - else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.0.7"} + if (newConfig.headers) Object.assign(newConfig.headers,{"User-Agent":"OpenDiscordBots-OpenTicket/4.1.0"}) + else newConfig.headers = {"User-Agent":"OpenDiscordBots-OpenTicket/4.1.0"} this.config = newConfig } diff --git a/src/index.ts b/src/index.ts index e319bce..fe323ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ INFORMATION: ============ - Open Ticket v4.0.7 - © DJdj Development + Open Ticket v4.1.0 - © DJdj Development support us: https://github.com/sponsors/DJj123dj discord: https://discord.dj-dj.be From f4f6f30fa23f99eca9367fcb631013c007d10f14 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:19:18 +0200 Subject: [PATCH 46/78] Added: Show global admins in panel message --- src/data/openticket/panelLoader.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/data/openticket/panelLoader.ts b/src/data/openticket/panelLoader.ts index 3820ae0..e522669 100644 --- a/src/data/openticket/panelLoader.ts +++ b/src/data/openticket/panelLoader.ts @@ -43,6 +43,7 @@ export const loadPanel = (panel:api.ODJsonConfig_DefaultPanelType) => { export function describePanelOptions(mode:"fields",panel:api.ODPanel): {name:string,value:string}[] export function describePanelOptions(mode:"text",panel:api.ODPanel): string export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): {name:string,value:string}[]|string { + const generalConfig = opendiscord.configs.get("opendiscord:general") const layout = panel.get("opendiscord:describe-options-layout").value const dropdownMode = panel.get("opendiscord:dropdown").value const options: api.ODOption[] = [] @@ -92,8 +93,15 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { if (opt.exists("opendiscord:limits-enabled") && opt.get("opendiscord:limits-enabled").value) description = description + "\nMax Tickets: `"+opt.get("opendiscord:limits-maximum-user").value+"`" } if (layout == "detailed"){ + const optionAdmins = [...opt.get("opendiscord:admins").value] + if (generalConfig.data.system.showGlobalAdminsInPanelRoles){ + for (const admin of generalConfig.data.globalAdmins){ + if (!optionAdmins.includes(admin)) optionAdmins.push(admin) + } + } + //TODO TRANSLATION!!! - if (opt.exists("opendiscord:admins")) description = description + "\nAdmins: "+opt.get("opendiscord:admins").value.map((admin) => discord.roleMention(admin)).join(", ") + if (opt.exists("opendiscord:admins")) description = description + "\nAdmins: "+optionAdmins.map((admin) => discord.roleMention(admin)).join(", ") } if (description == "") description = "``" @@ -144,8 +152,15 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { if (opt.exists("opendiscord:limits-enabled") && opt.get("opendiscord:limits-enabled").value) description = description + "\nMax Tickets: `"+opt.get("opendiscord:limits-maximum-user").value+"`" } if (layout == "detailed"){ + const optionAdmins = [...opt.get("opendiscord:admins").value] + if (generalConfig.data.system.showGlobalAdminsInPanelRoles){ + for (const admin of generalConfig.data.globalAdmins){ + if (!optionAdmins.includes(admin)) optionAdmins.push(admin) + } + } + //TODO TRANSLATION!!! - if (opt.exists("opendiscord:admins")) description = description + "\nAdmins: "+opt.get("opendiscord:admins").value.map((admin) => discord.roleMention(admin)).join(", ") + if (opt.exists("opendiscord:admins")) description = description + "\nAdmins: "+optionAdmins.map((admin) => discord.roleMention(admin)).join(", ") } if (layout == "simple") return "**"+utilities.emojiTitle(emoji,name)+":** "+description From 810db76e91597eec14c80cde253b1defc8ea4aad Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:46:42 +0200 Subject: [PATCH 47/78] Finished adding 10 new config checker validators --- src/core/api/defaults/checker.ts | 10 ++++++++++ src/core/api/modules/checker.ts | 21 +++++++++++++++------ src/data/framework/checkerLoader.ts | 10 ++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/core/api/defaults/checker.ts b/src/core/api/defaults/checker.ts index 62a15fb..9cc654c 100644 --- a/src/core/api/defaults/checker.ts +++ b/src/core/api/defaults/checker.ts @@ -241,9 +241,18 @@ export type ODCheckerTranslationRegisterMessageIds_Default = ( "opendiscord:string-starts-with"| "opendiscord:string-ends-with"| "opendiscord:string-contains"| + "opendiscord:string-inverted-contains"| "opendiscord:string-choices"| + "opendiscord:string-lowercase"| + "opendiscord:string-uppercase"| + "opendiscord:string-special-characters"| + "opendiscord:string-no-spaces"| "opendiscord:string-regex"| + "opendiscord:string-capital-word"| + "opendiscord:string-capital-sentence"| + "opendiscord:string-punctuation"| + "opendiscord:number-nan"| "opendiscord:number-too-short"| "opendiscord:number-too-long"| "opendiscord:number-length-invalid"| @@ -255,6 +264,7 @@ export type ODCheckerTranslationRegisterMessageIds_Default = ( "opendiscord:number-starts-with"| "opendiscord:number-ends-with"| "opendiscord:number-contains"| + "opendiscord:number-inverted-contains"| "opendiscord:number-choices"| "opendiscord:number-float"| "opendiscord:number-negative"| diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index 8b6f8d7..1a12483 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -624,9 +624,8 @@ export class ODCheckerStringStructure extends ODCheckerStructure { }else{ //warnings if ((this.options.capitalLetterWarning == "word" && !value.split(" ").every((word) => word.length == 0 || /^[^a-z].*/.test(word)))) checker.createMessage("opendiscord:string-capital-word","warning",`It's recommended that each word in this string starts with a capital letter!`,lt,null,[],this.id,(this.options.docs ?? null)) - if ((this.options.capitalLetterWarning == "sentence" && !value.split(/ *[.?!] */).every((sentence) => sentence.length == 0 || /^[^a-z].*/.test(sentence)))) checker.createMessage("opendiscord:string-capital-word","warning",`It looks like some sentences in this string don't start with a capital letter!`,lt,null,[],this.id,(this.options.docs ?? null)) - - //TODO: punctuation!!! + if ((this.options.capitalLetterWarning == "sentence" && !value.split(/ *[.?!] */).every((sentence) => sentence.length == 0 || /^[^a-z].*/.test(sentence)))) checker.createMessage("opendiscord:string-capital-sentence","warning",`It looks like some sentences in this string don't start with a capital letter!`,lt,null,[],this.id,(this.options.docs ?? null)) + if (this.options.punctuationWarning && value.length > 0 && (!value.endsWith(".") && !value.endsWith("?") && !value.endsWith("!") && !value.endsWith("'") && !value.endsWith('"') && !value.endsWith(",") && !value.endsWith(";") && !value.endsWith(":") && !value.endsWith("="))) checker.createMessage("opendiscord:string-punctuation","warning",`It looks like the sentence in this string doesn't end with a punctuation mark!`,lt,null,[],this.id,(this.options.docs ?? null)) return super.check(checker,value,locationTrace) } @@ -637,6 +636,8 @@ export class ODCheckerStringStructure extends ODCheckerStructure { * This interface has the options for `ODCheckerNumberStructure`! */ export interface ODCheckerNumberStructureOptions extends ODCheckerStructureOptions { + /**Is `NaN` (not a number) allowed? (`false` by default) */ + nanAllowed?:boolean /**The minimum length of this number */ minLength?:number, /**The maximum length of this number */ @@ -659,6 +660,8 @@ export interface ODCheckerNumberStructureOptions extends ODCheckerStructureOptio endsWith?:string, /**This number needs to contain ... */ contains?:string, + /**This number is not allowed to contain ... */ + invertedContains?:string, /**You need to choose between ... */ choices?:number[], /**Are numbers with a decimal value allowed? */ @@ -693,6 +696,9 @@ export class ODCheckerNumberStructure extends ODCheckerStructure { if (typeof value != "number"){ checker.createMessage("opendiscord:invalid-type","error","This property needs to be the type: number!",lt,null,["number"],this.id,(this.options.docs ?? null)) return false + }else if (!this.options.nanAllowed && isNaN(value)){ + checker.createMessage("opendiscord:number-nan","error",`This number can't NaN (Not A Number)!`,lt,null,[],this.id,(this.options.docs ?? null)) + return false }else if (typeof this.options.minLength != "undefined" && value.toString().length < this.options.minLength){ checker.createMessage("opendiscord:number-too-short","error",`This number can't be shorter than ${this.options.minLength} characters!`,lt,null,[this.options.minLength.toString()],this.id,(this.options.docs ?? null)) return false @@ -724,19 +730,22 @@ export class ODCheckerNumberStructure extends ODCheckerStructure { }else if (typeof this.options.contains != "undefined" && !value.toString().includes(this.options.contains)){ checker.createMessage("opendiscord:number-contains","error",`This number needs to contain "${this.options.contains}"!`,lt,null,[`"${this.options.contains}"`],this.id,(this.options.docs ?? null)) return false + }else if (typeof this.options.invertedContains != "undefined" && value.toString().includes(this.options.invertedContains)){ + checker.createMessage("opendiscord:number-inverted-contains","error",`This number is not allowed to contain "${this.options.invertedContains}"!`,lt,null,[`"${this.options.invertedContains}"`],this.id,(this.options.docs ?? null)) + return false }else if (typeof this.options.choices != "undefined" && !this.options.choices.includes(value)){ checker.createMessage("opendiscord:number-choices","error",`This number can only be one of the following values: "${this.options.choices.join(`", "`)}"!`,lt,null,[`"${this.options.choices.join(`", "`)}"`],this.id,(this.options.docs ?? null)) return false }else if (typeof this.options.floatAllowed != "undefined" && !this.options.floatAllowed && (value % 1) !== 0){ checker.createMessage("opendiscord:number-float","error","This number can't be a decimal!",lt,null,[],this.id,(this.options.docs ?? null)) return false - }else if (typeof this.options.negativeAllowed != "undefined" && value < 0){ + }else if (typeof this.options.negativeAllowed != "undefined" && !this.options.negativeAllowed && value < 0){ checker.createMessage("opendiscord:number-negative","error","This number can't be negative!",lt,null,[],this.id,(this.options.docs ?? null)) return false - }else if (typeof this.options.positiveAllowed != "undefined" && value > 0){ + }else if (typeof this.options.positiveAllowed != "undefined" && !this.options.positiveAllowed && value > 0){ checker.createMessage("opendiscord:number-positive","error","This number can't be positive!",lt,null,[],this.id,(this.options.docs ?? null)) return false - }else if (typeof this.options.zeroAllowed != "undefined" && value === 0){ + }else if (typeof this.options.zeroAllowed != "undefined" && !this.options.zeroAllowed && value === 0){ checker.createMessage("opendiscord:number-zero","error","This number can't be zero!",lt,null,[],this.id,(this.options.docs ?? null)) return false }else return super.check(checker,value,locationTrace) diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index adab440..4067638 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -78,9 +78,18 @@ export const registerDefaultCheckerMessageTranslations = (tm:api.ODCheckerTransl tm.quickTranslate(lm,"checker.messages.stringStartsWith","message","opendiscord:string-starts-with") // This string needs to start with {0}! tm.quickTranslate(lm,"checker.messages.stringEndsWith","message","opendiscord:string-ends-with") // This string needs to end with {0}! tm.quickTranslate(lm,"checker.messages.stringContains","message","opendiscord:string-contains") // This string needs to contain {0}! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-inverted-contains") // This string is not allowed to contain {0}! tm.quickTranslate(lm,"checker.messages.stringChoices","message","opendiscord:string-choices") // This string can only be one of the following values: {0}! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-lowercase") // This string must be written in lowercase only! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-uppercase") // This string must be written in uppercase only! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-special-characters") // This string is not allowed to contain any special characters! (a-z, 0-9 & space only) + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-no-spaces") // This string is not allowed to contain spaces! tm.quickTranslate(lm,"checker.messages.stringRegex","message","opendiscord:string-regex") // This string is invalid! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-capital-word") // It's recommended that each word in this string starts with a capital letter! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-capital-sentence") // It looks like some sentences in this string don't start with a capital letter! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-punctuation") // It looks like the sentence in this string doesn't end with a punctuation mark! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:number-nan") // This number can't NaN (Not A Number)! tm.quickTranslate(lm,"checker.messages.numberTooShort","message","opendiscord:number-too-short") // This number can't be shorter than {0} characters! tm.quickTranslate(lm,"checker.messages.numberTooLong","message","opendiscord:number-too-long") // This number can't be longer than {0} characters! tm.quickTranslate(lm,"checker.messages.numberLengthInvalid","message","opendiscord:number-length-invalid") // This number needs to be {0} characters long! @@ -92,6 +101,7 @@ export const registerDefaultCheckerMessageTranslations = (tm:api.ODCheckerTransl tm.quickTranslate(lm,"checker.messages.numberStartsWith","message","opendiscord:number-starts-with") // This number needs to start with {0}! tm.quickTranslate(lm,"checker.messages.numberEndsWith","message","opendiscord:number-ends-with") // This number needs to end with {0}! tm.quickTranslate(lm,"checker.messages.numberContains","message","opendiscord:number-contains") // This number needs to contain {0}! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:number-inverted-contains") // This number is not allowed to contain {0}! tm.quickTranslate(lm,"checker.messages.numberChoices","message","opendiscord:number-choices") // This number can only be one of the following values: {0}! tm.quickTranslate(lm,"checker.messages.numberFloat","message","opendiscord:number-float") // This number can't be a decimal! tm.quickTranslate(lm,"checker.messages.numberNegative","message","opendiscord:number-negative") // This number can't be negative! From 337dc3827e722284fb7ef2e27859eb1f2e500267 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:57:28 +0200 Subject: [PATCH 48/78] Refactor config checker children options Made the "priority" and "optional" fields in ODCheckerObjectStructure children optional. This improves readability and code simplicity --- src/core/api/modules/checker.ts | 6 +- src/data/framework/checkerLoader.ts | 534 ++++++++++++++-------------- 2 files changed, 270 insertions(+), 270 deletions(-) diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index 1a12483..d776f79 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -462,7 +462,7 @@ export class ODCheckerStructure { */ export interface ODCheckerObjectStructureOptions extends ODCheckerStructureOptions { /**Add a checker for a property in an object (can also be optional) */ - children:{key:string, priority:number, optional:boolean, cliHideInEditMode?:boolean, checker:ODCheckerStructure}[], + children:{key:string, priority?:number, optional?:boolean, cliHideInEditMode?:boolean, checker:ODCheckerStructure}[], /**A list of keys to skip when creating this object with the Interactive Setup CLI. The default value of these properties will be used instead. */ cliInitSkipKeys?:string[], /**The key of a (primitive) property in this object to show the value of in the Interactive Setup CLI when listed in an array. */ @@ -496,8 +496,8 @@ export class ODCheckerObjectStructure extends ODCheckerStructure { //sort children if (typeof this.options.children == "undefined") return super.check(checker,value,locationTrace) const sortedChildren = this.options.children.sort((a,b) => { - if (a.priority < b.priority) return -1 - else if (a.priority > b.priority) return 1 + if ((a.priority ?? 0) < (b.priority ?? 0)) return -1 + else if ((a.priority ?? 0) > (b.priority ?? 0)) return 1 else return 0 }) diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 4067638..b7eee42 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -152,52 +152,52 @@ export const registerDefaultCheckerCustomTranslations = (tm:api.ODCheckerTransla //UTILITY FUNCTIONS const createMsgStructure = (id:api.ODValidId,displayName:string) => { return new api.ODCheckerObjectStructure(id,{children:[ - {key:"dm",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-dm",{cliInitDefaultValue:false,cliDisplayName:"DM Enabled",cliDisplayDescription:"Will this action be sent in DM to the creator of the ticket?"})}, - {key:"logs",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:msg-logs",{cliInitDefaultValue:true,cliDisplayName:"Logs Enabled",cliDisplayDescription:"Will this action be sent in the Discord log channel?"})}, + {key:"dm",checker:new api.ODCheckerBooleanStructure("opendiscord:msg-dm",{cliInitDefaultValue:false,cliDisplayName:"DM Enabled",cliDisplayDescription:"Will this action be sent in DM to the creator of the ticket?"})}, + {key:"logs",checker:new api.ODCheckerBooleanStructure("opendiscord:msg-logs",{cliInitDefaultValue:true,cliDisplayName:"Logs Enabled",cliDisplayDescription:"Will this action be sent in the Discord log channel?"})}, ],cliDisplayName:displayName,cliDisplayDescription:"Configure which places this action gets logged/sent to."}) } const createTicketEmbedStructure = (id:api.ODValidId) => { return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the embed of this message."})}, - {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-text",{maxLength:256,cliDisplayName:"Title",cliDisplayDescription:"The title of this embed."})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-description",{maxLength:4096,cliDisplayName:"Description",cliDisplayDescription:"The description of this embed."})}, - {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:ticket-embed-color",true,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Set a custom color for this embed. When empty, the default bot color will be used."})}, + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the embed of this message."})}, + {key:"title",checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-text",{maxLength:256,cliDisplayName:"Title",cliDisplayDescription:"The title of this embed."})}, + {key:"description",checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-description",{maxLength:4096,cliDisplayName:"Description",cliDisplayDescription:"The description of this embed."})}, + {key:"customColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:ticket-embed-color",true,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Set a custom color for this embed. When empty, the default bot color will be used."})}, - {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Image",cliDisplayDescription:"Add an image to the embed using an image URL."})}, - {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Thumbnail",cliDisplayDescription:"Add a thumbnail to the embed using an image URL."})}, - {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-embed-fields",{children:[ - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-name",{minLength:1,maxLength:256,cliDisplayName:"Field Name",cliDisplayDescription:"The name/title of this embed field."})}, - {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-value",{minLength:1,maxLength:1024,cliDisplayName:"Field Value",cliDisplayDescription:"The value/description of this embed field."})}, - {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-field-inline",{cliDisplayName:"Field Inline",cliDisplayDescription:"Should this field be displayed inline with other fields?"})} + {key:"image",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Image",cliDisplayDescription:"Add an image to the embed using an image URL."})}, + {key:"thumbnail",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Thumbnail",cliDisplayDescription:"Add a thumbnail to the embed using an image URL."})}, + {key:"fields",checker:new api.ODCheckerArrayStructure("opendiscord:ticket-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-embed-fields",{children:[ + {key:"name",checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-name",{minLength:1,maxLength:256,cliDisplayName:"Field Name",cliDisplayDescription:"The name/title of this embed field."})}, + {key:"value",checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-value",{minLength:1,maxLength:1024,cliDisplayName:"Field Value",cliDisplayDescription:"The value/description of this embed field."})}, + {key:"inline",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-field-inline",{cliDisplayName:"Field Inline",cliDisplayDescription:"Should this field be displayed inline with other fields?"})} ],cliDisplayName:"Field",cliDisplayDescription:"Customise and configure an embed field."}),cliDisplayName:"Fields",cliDisplayDescription:"Customise and configure embed fields."})}, - {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-timestamp",{cliDisplayName:"Timestamp",cliDisplayDescription:"Add a timestamp to the embed."})} + {key:"timestamp",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-timestamp",{cliDisplayName:"Timestamp",cliDisplayDescription:"Add a timestamp to the embed."})} ],cliDisplayName:"Message Embed",cliDisplayDescription:"Configure the embed of this message."}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",image:"",thumbnail:"",fields:[],timestamp:false},cliDisplayName:"Message Embed",cliDisplayDescription:"Configure the embed of this message."}) } const createTicketPingStructure = (id:api.ODValidId) => { return new api.ODCheckerObjectStructure(id,{children:[ - {key:"@here",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-here",{cliDisplayName:"@here Ping",cliDisplayDescription:"Enable/disable an '@here' ping."})}, - {key:"@everyone",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-everyone",{cliDisplayName:"@everyone Ping",cliDisplayDescription:"Enable/disable an '@everyone' ping."})}, - {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false,cliDisplayPropertyName:"custom role id",cliDisplayName:"Custom Role Ping",cliDisplayDescription:"Choose your own roles to ping in this message."},{cliDisplayName:"Custom Role",cliDisplayDescription:"The discord role ID of a custom mention/ping."})}, + {key:"@here",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-here",{cliDisplayName:"@here Ping",cliDisplayDescription:"Enable/disable an '@here' ping."})}, + {key:"@everyone",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-everyone",{cliDisplayName:"@everyone Ping",cliDisplayDescription:"Enable/disable an '@everyone' ping."})}, + {key:"custom",checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false,cliDisplayPropertyName:"custom role id",cliDisplayName:"Custom Role Ping",cliDisplayDescription:"Choose your own roles to ping in this message."},{cliDisplayName:"Custom Role",cliDisplayDescription:"The discord role ID of a custom mention/ping."})}, ],cliInitDefaultValue:{"@here":true,"@everyone":false,custom:[],cliDisplayName:"Message Pings",cliDisplayDescription:"Configure the pings/mentions of this message."}}) } const createPanelEmbedStructure = (id:api.ODValidId) => { return new api.ODCheckerEnabledObjectStructure(id,{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure(id,{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the embed of this panel."})}, - {key:"title",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-text",{maxLength:256,cliDisplayName:"Title",cliDisplayDescription:"The title of this embed."})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-description",{maxLength:4096,cliDisplayName:"Description",cliDisplayDescription:"The description of this embed."})}, - {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:panel-embed-color",true,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Set a custom color for this embed. When empty, the default bot color will be used."})}, - {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-url",true,{allowHttp:false},{cliDisplayName:"URL",cliDisplayDescription:"Set a URL which will be displayed in the title of the embed."})}, + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the embed of this panel."})}, + {key:"title",checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-text",{maxLength:256,cliDisplayName:"Title",cliDisplayDescription:"The title of this embed."})}, + {key:"description",checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-description",{maxLength:4096,cliDisplayName:"Description",cliDisplayDescription:"The description of this embed."})}, + {key:"customColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:panel-embed-color",true,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Set a custom color for this embed. When empty, the default bot color will be used."})}, + {key:"url",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-url",true,{allowHttp:false},{cliDisplayName:"URL",cliDisplayDescription:"Set a URL which will be displayed in the title of the embed."})}, - {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Image",cliDisplayDescription:"Add an image to the embed using an image URL."})}, - {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Thumbnail",cliDisplayDescription:"Add a thumbnail to the embed using an image URL."})}, + {key:"image",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Image",cliDisplayDescription:"Add an image to the embed using an image URL."})}, + {key:"thumbnail",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Thumbnail",cliDisplayDescription:"Add a thumbnail to the embed using an image URL."})}, - {key:"footer",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-footer",{maxLength:2048,cliDisplayName:"Footer",cliDisplayDescription:"The footer of this embed."})}, - {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:panel-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panel-embed-fields",{children:[ - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-name",{minLength:1,maxLength:256,cliDisplayName:"Field Name",cliDisplayDescription:"The name/title of this embed field."})}, - {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-value",{minLength:1,maxLength:1024,cliDisplayName:"Field Value",cliDisplayDescription:"The value/description of this embed field."})}, - {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-field-inline",{cliDisplayName:"Field Inline",cliDisplayDescription:"Should this field be displayed inline with other fields?"})} + {key:"footer",checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-footer",{maxLength:2048,cliDisplayName:"Footer",cliDisplayDescription:"The footer of this embed."})}, + {key:"fields",checker:new api.ODCheckerArrayStructure("opendiscord:panel-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panel-embed-fields",{children:[ + {key:"name",checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-name",{minLength:1,maxLength:256,cliDisplayName:"Field Name",cliDisplayDescription:"The name/title of this embed field."})}, + {key:"value",checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-value",{minLength:1,maxLength:1024,cliDisplayName:"Field Value",cliDisplayDescription:"The value/description of this embed field."})}, + {key:"inline",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-field-inline",{cliDisplayName:"Field Inline",cliDisplayDescription:"Should this field be displayed inline with other fields?"})} ],cliDisplayName:"Field",cliDisplayDescription:"Customise and configure an embed field."}),cliDisplayName:"Fields",cliDisplayDescription:"Customise and configure embed fields."})}, - {key:"timestamp",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-timestamp",{cliDisplayName:"Timestamp",cliDisplayDescription:"Add a timestamp to the embed."})} + {key:"timestamp",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-timestamp",{cliDisplayName:"Timestamp",cliDisplayDescription:"Add a timestamp to the embed."})} ],cliDisplayName:"Panel Embed",cliDisplayDescription:"Configure the embed of this panel."}),cliInitDefaultValue:{enabled:false,title:"",description:"",customColor:"",url:"",image:"",thumbnail:"",footer:"",fields:[],timestamp:false},cliDisplayName:"Panel Embed",cliDisplayDescription:"Configure the embed of this panel."}) } @@ -210,10 +210,10 @@ function loadFromEnv(){ //STRUCTURES export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendiscord:general",{children:[ //INFO - {key:"_INFO",optional:false,priority:0,cliHideInEditMode:true,checker:new api.ODCheckerObjectStructure("opendiscord:info",{children:[ - {key:"support",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-support",{choices:["https://otdocs.dj-dj.be"]})}, - {key:"discord",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-discord",{choices:["https://discord.dj-dj.be"]})}, - {key:"version",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-version",{custom(checker,value,locationTrace,locationId,locationDocs) { + {key:"_INFO",cliHideInEditMode:true,checker:new api.ODCheckerObjectStructure("opendiscord:info",{children:[ + {key:"support",checker:new api.ODCheckerStringStructure("opendiscord:info-support",{choices:["https://otdocs.dj-dj.be"]})}, + {key:"discord",checker:new api.ODCheckerStringStructure("opendiscord:info-discord",{choices:["https://discord.dj-dj.be"]})}, + {key:"version",checker:new api.ODCheckerStringStructure("opendiscord:info-version",{custom(checker,value,locationTrace,locationId,locationDocs) { const lt = checker.locationTraceDeref(locationTrace) if (typeof value != "string") return false @@ -225,10 +225,10 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis ]})}, //BASIC - {key:"token",optional:false,priority:0,checker:(loadFromEnv()) ? new api.ODCheckerStringStructure("opendiscord:token-disabled",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."}) : new api.ODCheckerCustomStructure_DiscordToken("opendiscord:token",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."})}, - {key:"tokenFromENV",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:token-env",{cliDisplayName:"Token From ENV",cliDisplayDescription:"Use the token from the .env file instead of general.json."})}, - {key:"mainColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:main-color",true,false,{cliDisplayName:"Main Color",cliDisplayDescription:"The main color of your bot, used in almost all embeds."})}, - {key:"language",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:language",{ + {key:"token",checker:(loadFromEnv()) ? new api.ODCheckerStringStructure("opendiscord:token-disabled",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."}) : new api.ODCheckerCustomStructure_DiscordToken("opendiscord:token",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."})}, + {key:"tokenFromENV",checker:new api.ODCheckerBooleanStructure("opendiscord:token-env",{cliDisplayName:"Token From ENV",cliDisplayDescription:"Use the token from the .env file instead of general.json."})}, + {key:"mainColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:main-color",true,false,{cliDisplayName:"Main Color",cliDisplayDescription:"The main color of your bot, used in almost all embeds."})}, + {key:"language",checker:new api.ODCheckerStringStructure("opendiscord:language",{ custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) @@ -242,125 +242,125 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis cliDisplayName:"Language", cliDisplayDescription:"The language of the bot. Visit README.md for a list of available translations." })}, - {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:prefix",{minLength:1,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix used for the text-commands from the bot."})}, - {key:"serverId",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:server-id","server",false,[],{cliDisplayName:"Server Id",cliDisplayDescription:"The ID of the discord server you will be using this bot in."})}, - {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role",cliDisplayName:"Global Admin Roles",cliDisplayDescription:"A list of role IDs that are able to interact with all commands and tickets."},{cliDisplayName:"Global Admin Role",cliDisplayDescription:"The discord role ID of a global admin."})}, - {key:"slashCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:slash-commands",{cliDisplayName:"Enable Slash Commands",cliDisplayDescription:"Enable/disable slash commands in the bot. When disabled, the commands will not be displayed."})}, - {key:"textCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:text-commands",{cliDisplayName:"Enable Text Commands",cliDisplayDescription:"Enable/disable text commands in the bot. (Disabling is recommended in large servers)"})}, + {key:"prefix",checker:new api.ODCheckerStringStructure("opendiscord:prefix",{minLength:1,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix used for the text-commands from the bot."})}, + {key:"serverId",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:server-id","server",false,[],{cliDisplayName:"Server Id",cliDisplayDescription:"The ID of the discord server you will be using this bot in."})}, + {key:"globalAdmins",checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role",cliDisplayName:"Global Admin Roles",cliDisplayDescription:"A list of role IDs that are able to interact with all commands and tickets."},{cliDisplayName:"Global Admin Role",cliDisplayDescription:"The discord role ID of a global admin."})}, + {key:"slashCommands",checker:new api.ODCheckerBooleanStructure("opendiscord:slash-commands",{cliDisplayName:"Enable Slash Commands",cliDisplayDescription:"Enable/disable slash commands in the bot. When disabled, the commands will not be displayed."})}, + {key:"textCommands",checker:new api.ODCheckerBooleanStructure("opendiscord:text-commands",{cliDisplayName:"Enable Text Commands",cliDisplayDescription:"Enable/disable text commands in the bot. (Disabling is recommended in large servers)"})}, //STATUS - {key:"status",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:status",{ + {key:"status",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:status",{ property:"enabled", enabledValue:true, checker:new api.ODCheckerObjectStructure("opendiscord:status",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:status-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the status. When disabled, the bot will be online without any status."})}, - {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-type",{choices:["listening","watching","playing","custom"],cliDisplayName:"Type",cliDisplayDescription:"The type of status: Listening, Watching, Playing or Custom."})}, - {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-mode",{choices:["online","invisible","idle","dnd"],cliDisplayName:"Mode",cliDisplayDescription:"The profile status/mode of the bot: Online, Invisible, Idle or Do Not Disturb."})}, - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-text",{minLength:1,maxLength:128,cliDisplayName:"Text",cliDisplayDescription:"The text displayed in the status."})}, - {key:"state",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:status-state",{maxLength:128,cliDisplayName:"State",cliDisplayDescription:"Additional text displayed below the status 'text'."})}, + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:status-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the status. When disabled, the bot will be online without any status."})}, + {key:"type",checker:new api.ODCheckerStringStructure("opendiscord:status-type",{choices:["listening","watching","playing","custom"],cliDisplayName:"Type",cliDisplayDescription:"The type of status: Listening, Watching, Playing or Custom."})}, + {key:"mode",checker:new api.ODCheckerStringStructure("opendiscord:status-mode",{choices:["online","invisible","idle","dnd"],cliDisplayName:"Mode",cliDisplayDescription:"The profile status/mode of the bot: Online, Invisible, Idle or Do Not Disturb."})}, + {key:"text",checker:new api.ODCheckerStringStructure("opendiscord:status-text",{minLength:1,maxLength:128,cliDisplayName:"Text",cliDisplayDescription:"The text displayed in the status."})}, + {key:"state",checker:new api.ODCheckerStringStructure("opendiscord:status-state",{maxLength:128,cliDisplayName:"State",cliDisplayDescription:"Additional text displayed below the status 'text'."})}, ],cliDisplayName:"Bot Status",cliDisplayDescription:"Manage the status of the bot."}), cliDisplayName:"Bot Status", cliDisplayDescription:"Manage the status of the bot." })}, //SYSTEM - {key:"system",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system",{children:[ - {key:"preferSlashOverText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:prefer-slash-over-text",{cliDisplayName:"Prefer Slash Over Text",cliDisplayDescription:"Prefer displaying slash commands over text commands in help menus."})}, - {key:"sendErrorOnUnknownCommand",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:send-error-on-unknown-command",{cliDisplayName:"Send Error On Unknown Command",cliDisplayDescription:"Send an error when using the text-command prefix without a valid command."})}, - {key:"questionFieldsInCodeBlock",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-fields-in-code-block",{cliDisplayName:"Questions Fields In Code Blocks",cliDisplayDescription:"Display question fields in code blocks instead of plain text."})}, - {key:"displayFieldsWithQuestions",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:display-fields-with-questions",{cliDisplayName:"Display Fields With Questions",cliDisplayDescription:"Display embed fields together with question fields (in a ticket message)."})}, - {key:"showGlobalAdminsInPanelRoles",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:global-admins-in-panel-roles",{cliDisplayName:"Show Global Admins In Panel Roles",cliDisplayDescription:"Show global admins roles together with ticket admins in panel embeds."})}, - {key:"disableVerifyBars",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:disable-verify-bars",{cliDisplayName:"Disable Verifybars",cliDisplayDescription:"Disable the (✅/❌) verify buttons in all commands. (Not recommended)"})}, - {key:"useRedErrorEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-red-error-embeds",{cliDisplayName:"Use Red Error Embeds",cliDisplayDescription:"Display all error messages with a red border instead of the default color of the bot."})}, - {key:"alwaysShowReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:always-show-reason",{cliDisplayName:"Always Show Reason",cliDisplayDescription:"Always show the reason field in embeds, even when there is no reason provided."})}, - {key:"emojiStyle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:emoji-style",{choices:["before","after","double","disabled"],cliDisplayName:"Emoji Style",cliDisplayDescription:"Choose how the bot will display emojis in message titles. (Visit docs for more info)"})}, - {key:"pinEmoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:pin-emoji",1,1,false,{cliDisplayName:"Pin Emoji",cliDisplayDescription:"The emoji used when pinning tickets. This is '📌' by default."})}, + {key:"system",checker:new api.ODCheckerObjectStructure("opendiscord:system",{children:[ + {key:"preferSlashOverText",checker:new api.ODCheckerBooleanStructure("opendiscord:prefer-slash-over-text",{cliDisplayName:"Prefer Slash Over Text",cliDisplayDescription:"Prefer displaying slash commands over text commands in help menus."})}, + {key:"sendErrorOnUnknownCommand",checker:new api.ODCheckerBooleanStructure("opendiscord:send-error-on-unknown-command",{cliDisplayName:"Send Error On Unknown Command",cliDisplayDescription:"Send an error when using the text-command prefix without a valid command."})}, + {key:"questionFieldsInCodeBlock",checker:new api.ODCheckerBooleanStructure("opendiscord:question-fields-in-code-block",{cliDisplayName:"Questions Fields In Code Blocks",cliDisplayDescription:"Display question fields in code blocks instead of plain text."})}, + {key:"displayFieldsWithQuestions",checker:new api.ODCheckerBooleanStructure("opendiscord:display-fields-with-questions",{cliDisplayName:"Display Fields With Questions",cliDisplayDescription:"Display embed fields together with question fields (in a ticket message)."})}, + {key:"showGlobalAdminsInPanelRoles",checker:new api.ODCheckerBooleanStructure("opendiscord:global-admins-in-panel-roles",{cliDisplayName:"Show Global Admins In Panel Roles",cliDisplayDescription:"Show global admins roles together with ticket admins in panel embeds."})}, + {key:"disableVerifyBars",checker:new api.ODCheckerBooleanStructure("opendiscord:disable-verify-bars",{cliDisplayName:"Disable Verifybars",cliDisplayDescription:"Disable the (✅/❌) verify buttons in all commands. (Not recommended)"})}, + {key:"useRedErrorEmbeds",checker:new api.ODCheckerBooleanStructure("opendiscord:use-red-error-embeds",{cliDisplayName:"Use Red Error Embeds",cliDisplayDescription:"Display all error messages with a red border instead of the default color of the bot."})}, + {key:"alwaysShowReason",checker:new api.ODCheckerBooleanStructure("opendiscord:always-show-reason",{cliDisplayName:"Always Show Reason",cliDisplayDescription:"Always show the reason field in embeds, even when there is no reason provided."})}, + {key:"emojiStyle",checker:new api.ODCheckerStringStructure("opendiscord:emoji-style",{choices:["before","after","double","disabled"],cliDisplayName:"Emoji Style",cliDisplayDescription:"Choose how the bot will display emojis in message titles. (Visit docs for more info)"})}, + {key:"pinEmoji",checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:pin-emoji",1,1,false,{cliDisplayName:"Pin Emoji",cliDisplayDescription:"The emoji used when pinning tickets. This is '📌' by default."})}, - {key:"replyOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-ticket-creation",{cliDisplayName:"Reply On Ticket Creation",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when creating a ticket."})}, - {key:"replyOnReactionRole",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-reaction-role",{cliDisplayName:"Reply On Reaction Role",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when using a role button."})}, - {key:"showPreAutocloseWarning",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:show-pre-autoclose-warning",{cliDisplayName:"Show Pre-Autoclose Warning",cliDisplayDescription:"Show a warning message before the ticket gets autoclosed. This will happen when only 1/4th of the autoclose time remains."})}, - {key:"askPriorityOnTicketCreation",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ask-priority-creation",{cliDisplayName:"Ask Priority On Ticket Creation",cliDisplayDescription:"Ask for the priority of this ticket on ticket creation. This will happen in a dropdown in the ticket message."})}, - {key:"removeParticipantsOnClose",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:remove-participants-on-close",{cliDisplayName:"Remove Participants On Close",cliDisplayDescription:"When enabled, all participants except admins will be removed from the ticket."})}, - {key:"disableAutocloseAfterReopen",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:disable-autoclose-reopen",{cliDisplayName:"Disable Autoclose On Reopen",cliDisplayDescription:"Disable autoclose for a ticket when it has been closed and re-opened."})}, - {key:"autodeleteRequiresClosedTicket",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:autodelete-requires-closed-ticket",{cliDisplayName:"Autodelete Requires Closed Ticket",cliDisplayDescription:"Only allow autodelete when the ticket is already closed."})}, - {key:"adminOnlyDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:adminonly-delete-without-transcript",{cliDisplayName:"Admin-only Delete Without Transcript",cliDisplayDescription:"When enabled, only global admins are able to delete a ticket without transcript."})}, - {key:"allowCloseBeforeMessage",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:allow-close-before-message",{cliDisplayName:"Allow Close Before Message",cliDisplayDescription:"Only allow ticket closing when at least 1 message has been sent by the creator. (admins are able to bypass)"})}, - {key:"allowCloseBeforeAdminMessage",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:allow-close-before-admin-message",{cliDisplayName:"Allow Close Before Admin Message",cliDisplayDescription:"Only allow ticket closing when at least 1 message has been sent by a global or ticket admin. (admins are able to bypass)"})}, - {key:"useTranslatedConfigChecker",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:use-translated-config-checker",{cliDisplayName:"Use Translated Config Checker",cliDisplayDescription:"Use a translated config checker to better understand the errors the bot gives."})}, - {key:"pinFirstTicketMessage",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:pin-first-ticket-message",{cliDisplayName:"Pin First Ticket Message",cliDisplayDescription:"Pin the (first) ticket message in the channel. This simulates old behaviour like Open Ticket v1, v2 & v3."})}, + {key:"replyOnTicketCreation",checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-ticket-creation",{cliDisplayName:"Reply On Ticket Creation",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when creating a ticket."})}, + {key:"replyOnReactionRole",checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-reaction-role",{cliDisplayName:"Reply On Reaction Role",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when using a role button."})}, + {key:"showPreAutocloseWarning",checker:new api.ODCheckerBooleanStructure("opendiscord:show-pre-autoclose-warning",{cliDisplayName:"Show Pre-Autoclose Warning",cliDisplayDescription:"Show a warning message before the ticket gets autoclosed. This will happen when only 1/4th of the autoclose time remains."})}, + {key:"askPriorityOnTicketCreation",checker:new api.ODCheckerBooleanStructure("opendiscord:ask-priority-creation",{cliDisplayName:"Ask Priority On Ticket Creation",cliDisplayDescription:"Ask for the priority of this ticket on ticket creation. This will happen in a dropdown in the ticket message."})}, + {key:"removeParticipantsOnClose",checker:new api.ODCheckerBooleanStructure("opendiscord:remove-participants-on-close",{cliDisplayName:"Remove Participants On Close",cliDisplayDescription:"When enabled, all participants except admins will be removed from the ticket."})}, + {key:"disableAutocloseAfterReopen",checker:new api.ODCheckerBooleanStructure("opendiscord:disable-autoclose-reopen",{cliDisplayName:"Disable Autoclose On Reopen",cliDisplayDescription:"Disable autoclose for a ticket when it has been closed and re-opened."})}, + {key:"autodeleteRequiresClosedTicket",checker:new api.ODCheckerBooleanStructure("opendiscord:autodelete-requires-closed-ticket",{cliDisplayName:"Autodelete Requires Closed Ticket",cliDisplayDescription:"Only allow autodelete when the ticket is already closed."})}, + {key:"adminOnlyDeleteWithoutTranscript",checker:new api.ODCheckerBooleanStructure("opendiscord:adminonly-delete-without-transcript",{cliDisplayName:"Admin-only Delete Without Transcript",cliDisplayDescription:"When enabled, only global admins are able to delete a ticket without transcript."})}, + {key:"allowCloseBeforeMessage",checker:new api.ODCheckerBooleanStructure("opendiscord:allow-close-before-message",{cliDisplayName:"Allow Close Before Message",cliDisplayDescription:"Only allow ticket closing when at least 1 message has been sent by the creator. (admins are able to bypass)"})}, + {key:"allowCloseBeforeAdminMessage",checker:new api.ODCheckerBooleanStructure("opendiscord:allow-close-before-admin-message",{cliDisplayName:"Allow Close Before Admin Message",cliDisplayDescription:"Only allow ticket closing when at least 1 message has been sent by a global or ticket admin. (admins are able to bypass)"})}, + {key:"useTranslatedConfigChecker",checker:new api.ODCheckerBooleanStructure("opendiscord:use-translated-config-checker",{cliDisplayName:"Use Translated Config Checker",cliDisplayDescription:"Use a translated config checker to better understand the errors the bot gives."})}, + {key:"pinFirstTicketMessage",checker:new api.ODCheckerBooleanStructure("opendiscord:pin-first-ticket-message",{cliDisplayName:"Pin First Ticket Message",cliDisplayDescription:"Pin the (first) ticket message in the channel. This simulates old behaviour like Open Ticket v1, v2 & v3."})}, - {key:"enableTicketClaimButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-claim-buttons",{cliDisplayName:"Enable Ticket Claim Buttons",cliDisplayDescription:"Enable/disable buttons for claiming a ticket. Be aware that this doesn't disable the command!"})}, - {key:"enableTicketCloseButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-close-buttons",{cliDisplayName:"Enable Ticket Close Buttons",cliDisplayDescription:"Enable/disable buttons for closing a ticket. Be aware that this doesn't disable the command!"})}, - {key:"enableTicketPinButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-pin-buttons",{cliDisplayName:"Enable Ticket Pin Buttons",cliDisplayDescription:"Enable/disable buttons for pinning a ticket. Be aware that this doesn't disable the command!"})}, - {key:"enableTicketDeleteButtons",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-delete-buttons",{cliDisplayName:"Enable Ticket Delete Buttons",cliDisplayDescription:"Enable/disable buttons for deleting a ticket. Be aware that this doesn't disable the command!"})}, - {key:"enableTicketActionWithReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-action-with-reason",{cliDisplayName:"Enable Ticket Action With Reason",cliDisplayDescription:"Enable/disable buttons to write an additional reason for all ticket actions."})}, - {key:"enableDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-delete-without-transcript",{cliDisplayName:"Enable Delete Without Transcript",cliDisplayDescription:"Enable/disable the ability to delete tickets without a transcript."})}, + {key:"enableTicketClaimButtons",checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-claim-buttons",{cliDisplayName:"Enable Ticket Claim Buttons",cliDisplayDescription:"Enable/disable buttons for claiming a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketCloseButtons",checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-close-buttons",{cliDisplayName:"Enable Ticket Close Buttons",cliDisplayDescription:"Enable/disable buttons for closing a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketPinButtons",checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-pin-buttons",{cliDisplayName:"Enable Ticket Pin Buttons",cliDisplayDescription:"Enable/disable buttons for pinning a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketDeleteButtons",checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-delete-buttons",{cliDisplayName:"Enable Ticket Delete Buttons",cliDisplayDescription:"Enable/disable buttons for deleting a ticket. Be aware that this doesn't disable the command!"})}, + {key:"enableTicketActionWithReason",checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-action-with-reason",{cliDisplayName:"Enable Ticket Action With Reason",cliDisplayDescription:"Enable/disable buttons to write an additional reason for all ticket actions."})}, + {key:"enableDeleteWithoutTranscript",checker:new api.ODCheckerBooleanStructure("opendiscord:enable-delete-without-transcript",{cliDisplayName:"Enable Delete Without Transcript",cliDisplayDescription:"Enable/disable the ability to delete tickets without a transcript."})}, - {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:logs-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable discord logs in a discord channel."})}, - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:logs-channel","channel",false,[],{cliDisplayName:"Log Channel",cliDisplayDescription:"The ID of the discord channel to log messages to. You can configure the messages somewhere else."})}, + {key:"logs",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:logs-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable discord logs in a discord channel."})}, + {key:"channel",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:logs-channel","channel",false,[],{cliDisplayName:"Log Channel",cliDisplayDescription:"The ID of the discord channel to log messages to. You can configure the messages somewhere else."})}, ],cliDisplayName:"Discord Logs",cliDisplayDescription:"Manage everything related to logs in a discord channel."}),cliDisplayName:"Discord Logs",cliDisplayDescription:"Manage everything related to logs in a discord channel."})}, - {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:limits",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable global limits."})}, - {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets that are able to exist in the server at the same time."})}, - {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets from a specific user that are able to exist in the server at the same time."})} + {key:"limits",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:limits",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable global limits."})}, + {key:"globalMaximum",checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets that are able to exist in the server at the same time."})}, + {key:"userMaximum",checker:new api.ODCheckerNumberStructure("opendiscord:limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets from a specific user that are able to exist in the server at the same time."})} ],cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."}),cliDisplayName:"Global Limits",cliDisplayDescription:"Manage global limits for ticket creation to reduce the workload on your support team."})}, - {key:"channelTopic",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:channel-topic",{children:[ - {key:"showOptionName",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-name",{cliDisplayName:"Show Option Name",cliDisplayDescription:"Show the option name in the channel topic."})}, - {key:"showOptionDescription",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-description",{cliDisplayName:"Show Option Description",cliDisplayDescription:"Show the option description in the channel topic."})}, - {key:"showOptionTopic",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-topic",{cliDisplayName:"Show Option Topic",cliDisplayDescription:"Show the option topic text in the channel topic (configured in the options.json config)."})}, - {key:"showClosed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-closed",{cliDisplayName:"Show Closed Status",cliDisplayDescription:"Show the current close/reopen status in the channel topic (auto-updated)."})}, - {key:"showClaimed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-claimed",{cliDisplayName:"Show Claimed Status",cliDisplayDescription:"Show the current claim status in the channel topic (auto-updated)."})}, - {key:"showPinned",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-pinned",{cliDisplayName:"Show Pinned Status",cliDisplayDescription:"Show the current pin status in the channel topic (auto-updated)."})}, - {key:"showPriority",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-priority",{cliDisplayName:"Show Priority",cliDisplayDescription:"Show the current priority in the channel topic (auto-updated)."})}, - {key:"showCreator",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-creator",{cliDisplayName:"Show Creator",cliDisplayDescription:"Show the creator of the ticket in the channel topic (auto-updated on transfer)."})}, - {key:"showParticipants",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-participants",{cliDisplayName:"Show Participants",cliDisplayDescription:"Show the first 5 participants of the ticket in the channel topic (auto-updated)."})}, + {key:"channelTopic",checker:new api.ODCheckerObjectStructure("opendiscord:channel-topic",{children:[ + {key:"showOptionName",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-name",{cliDisplayName:"Show Option Name",cliDisplayDescription:"Show the option name in the channel topic."})}, + {key:"showOptionDescription",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-description",{cliDisplayName:"Show Option Description",cliDisplayDescription:"Show the option description in the channel topic."})}, + {key:"showOptionTopic",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-topic",{cliDisplayName:"Show Option Topic",cliDisplayDescription:"Show the option topic text in the channel topic (configured in the options.json config)."})}, + {key:"showClosed",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-closed",{cliDisplayName:"Show Closed Status",cliDisplayDescription:"Show the current close/reopen status in the channel topic (auto-updated)."})}, + {key:"showClaimed",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-claimed",{cliDisplayName:"Show Claimed Status",cliDisplayDescription:"Show the current claim status in the channel topic (auto-updated)."})}, + {key:"showPinned",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-pinned",{cliDisplayName:"Show Pinned Status",cliDisplayDescription:"Show the current pin status in the channel topic (auto-updated)."})}, + {key:"showPriority",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-priority",{cliDisplayName:"Show Priority",cliDisplayDescription:"Show the current priority in the channel topic (auto-updated)."})}, + {key:"showCreator",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-creator",{cliDisplayName:"Show Creator",cliDisplayDescription:"Show the creator of the ticket in the channel topic (auto-updated on transfer)."})}, + {key:"showParticipants",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-participants",{cliDisplayName:"Show Participants",cliDisplayDescription:"Show the first 5 participants of the ticket in the channel topic (auto-updated)."})}, ],cliDisplayName:"Channel Topic",cliDisplayDescription:"Manage stats and text of ticket channel topics."})}, - {key:"permissions",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ - {key:"help",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-help","role",false,["admin","everyone","none"],{cliDisplayName:"Help",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"panel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-panel","role",false,["admin","everyone","none"],{cliDisplayName:"Panel",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"ticket",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-ticket","role",false,["admin","everyone","none"],{cliDisplayName:"Ticket",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"close",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-close","role",false,["admin","everyone","none"],{cliDisplayName:"Close",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"delete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-delete","role",false,["admin","everyone","none"],{cliDisplayName:"Delete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"reopen",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-reopen","role",false,["admin","everyone","none"],{cliDisplayName:"Reopen",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"claim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-claim","role",false,["admin","everyone","none"],{cliDisplayName:"Claim",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"unclaim",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unclaim","role",false,["admin","everyone","none"],{cliDisplayName:"Unclaim",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"pin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-pin","role",false,["admin","everyone","none"],{cliDisplayName:"Pin",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"unpin",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unpin","role",false,["admin","everyone","none"],{cliDisplayName:"Unpin",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"move",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-move","role",false,["admin","everyone","none"],{cliDisplayName:"Move",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"rename",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-rename","role",false,["admin","everyone","none"],{cliDisplayName:"Rename",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"add",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-add","role",false,["admin","everyone","none"],{cliDisplayName:"Add User",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"remove",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-remove","role",false,["admin","everyone","none"],{cliDisplayName:"Remove User",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"blacklist",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-blacklist","role",false,["admin","everyone","none"],{cliDisplayName:"Blacklist",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"stats",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-stats","role",false,["admin","everyone","none"],{cliDisplayName:"Stats",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"clear",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-clear","role",false,["admin","everyone","none"],{cliDisplayName:"Clear Tickets",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autoclose","role",false,["admin","everyone","none"],{cliDisplayName:"Autoclose",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autodelete","role",false,["admin","everyone","none"],{cliDisplayName:"Autodelete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"transfer",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-transfer","role",false,["admin","everyone","none"],{cliDisplayName:"Transfer",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"topic",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-topic","role",false,["admin","everyone","none"],{cliDisplayName:"Topic",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, - {key:"priority",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-priority","role",false,["admin","everyone","none"],{cliDisplayName:"Priority",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"permissions",checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ + {key:"help",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-help","role",false,["admin","everyone","none"],{cliDisplayName:"Help",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"panel",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-panel","role",false,["admin","everyone","none"],{cliDisplayName:"Panel",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"ticket",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-ticket","role",false,["admin","everyone","none"],{cliDisplayName:"Ticket",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"close",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-close","role",false,["admin","everyone","none"],{cliDisplayName:"Close",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"delete",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-delete","role",false,["admin","everyone","none"],{cliDisplayName:"Delete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"reopen",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-reopen","role",false,["admin","everyone","none"],{cliDisplayName:"Reopen",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"claim",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-claim","role",false,["admin","everyone","none"],{cliDisplayName:"Claim",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"unclaim",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unclaim","role",false,["admin","everyone","none"],{cliDisplayName:"Unclaim",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"pin",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-pin","role",false,["admin","everyone","none"],{cliDisplayName:"Pin",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"unpin",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-unpin","role",false,["admin","everyone","none"],{cliDisplayName:"Unpin",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"move",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-move","role",false,["admin","everyone","none"],{cliDisplayName:"Move",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"rename",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-rename","role",false,["admin","everyone","none"],{cliDisplayName:"Rename",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"add",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-add","role",false,["admin","everyone","none"],{cliDisplayName:"Add User",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"remove",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-remove","role",false,["admin","everyone","none"],{cliDisplayName:"Remove User",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"blacklist",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-blacklist","role",false,["admin","everyone","none"],{cliDisplayName:"Blacklist",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"stats",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-stats","role",false,["admin","everyone","none"],{cliDisplayName:"Stats",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"clear",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-clear","role",false,["admin","everyone","none"],{cliDisplayName:"Clear Tickets",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"autoclose",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autoclose","role",false,["admin","everyone","none"],{cliDisplayName:"Autoclose",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"autodelete",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-autodelete","role",false,["admin","everyone","none"],{cliDisplayName:"Autodelete",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"transfer",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-transfer","role",false,["admin","everyone","none"],{cliDisplayName:"Transfer",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"topic",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-topic","role",false,["admin","everyone","none"],{cliDisplayName:"Topic",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, + {key:"priority",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:permissions-priority","role",false,["admin","everyone","none"],{cliDisplayName:"Priority",cliHideDescriptionInParent:true,cliDisplayDescription:"Set the permissions to 'everyone' for everyone, 'admin' for admin only, 'none' to disable or a custom discord role ID."})}, ],cliDisplayName:"Permissions",cliDisplayDescription:"Manage all button & command permissions in the bot. (Visit docs for more info)"})}, - {key:"messages",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ - {key:"creation",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-creation","Ticket Created")}, - {key:"closing",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-closing","Ticket Closed")}, - {key:"deleting",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-deleting","Ticket Deleted")}, - {key:"reopening",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-reopening","Ticket Reopened")}, - {key:"claiming",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-claiming","Ticket Claimed")}, - {key:"pinning",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-pinning","Ticket Pinned")}, - {key:"adding",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-adding","User Added")}, - {key:"removing",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-removing","User Removed")}, - {key:"renaming",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-renaming","Ticket Renamed")}, - {key:"moving",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-moving","Ticket Moved")}, - {key:"blacklisting",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-blacklisting","User Blacklisted")}, - {key:"transferring",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-transferring","Ticket Transferred")}, - {key:"topicChange",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-topic-change","Topic Changed")}, - {key:"priorityChange",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-priority-change","Priority Changed")}, - {key:"reactionRole",optional:false,priority:0,checker:createMsgStructure("opendiscord:msg-reaction-role","Reaction Role")}, + {key:"messages",checker:new api.ODCheckerObjectStructure("opendiscord:system-permissions",{children:[ + {key:"creation",checker:createMsgStructure("opendiscord:msg-creation","Ticket Created")}, + {key:"closing",checker:createMsgStructure("opendiscord:msg-closing","Ticket Closed")}, + {key:"deleting",checker:createMsgStructure("opendiscord:msg-deleting","Ticket Deleted")}, + {key:"reopening",checker:createMsgStructure("opendiscord:msg-reopening","Ticket Reopened")}, + {key:"claiming",checker:createMsgStructure("opendiscord:msg-claiming","Ticket Claimed")}, + {key:"pinning",checker:createMsgStructure("opendiscord:msg-pinning","Ticket Pinned")}, + {key:"adding",checker:createMsgStructure("opendiscord:msg-adding","User Added")}, + {key:"removing",checker:createMsgStructure("opendiscord:msg-removing","User Removed")}, + {key:"renaming",checker:createMsgStructure("opendiscord:msg-renaming","Ticket Renamed")}, + {key:"moving",checker:createMsgStructure("opendiscord:msg-moving","Ticket Moved")}, + {key:"blacklisting",checker:createMsgStructure("opendiscord:msg-blacklisting","User Blacklisted")}, + {key:"transferring",checker:createMsgStructure("opendiscord:msg-transferring","Ticket Transferred")}, + {key:"topicChange",checker:createMsgStructure("opendiscord:msg-topic-change","Topic Changed")}, + {key:"priorityChange",checker:createMsgStructure("opendiscord:msg-priority-change","Priority Changed")}, + {key:"reactionRole",checker:createMsgStructure("opendiscord:msg-reaction-role","Reaction Role")}, ],cliDisplayName:"Messages",cliDisplayDescription:"Manage all messages & DM's for each action of the bot. (Visit docs for more info)"})}, ],cliDisplayName:"System",cliDisplayDescription:"Configure everything related to the ticket system."})} ],cliDisplayName:"General",cliDisplayDescription:"General settings for the bot."}) @@ -368,15 +368,15 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],cliDisplayPropertyName:"option",propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ //TICKET {name:"Ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],cliInitSkipKeys:["readonlyAdmins"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this ticket option. Used in panels."})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this ticket option."})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this ticket option."})}, + {key:"id",checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this ticket option. Used in panels."})}, + {key:"name",checker:new api.ODCheckerStringStructure("opendiscord:ticket-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this ticket option."})}, + {key:"description",checker:new api.ODCheckerStringStructure("opendiscord:ticket-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this ticket option."})}, //TICKET BUTTON - {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ - {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, - {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, - {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"],cliDisplayName:"Color",cliDisplayDescription:"The color of the button. Does not apply when using a dropdown panel."})}, + {key:"button",checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ + {key:"emoji",checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"label",checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"color",checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"],cliDisplayName:"Color",cliDisplayDescription:"The color of the button. Does not apply when using a dropdown panel."})}, ],custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) //check if emoji & label exists @@ -389,10 +389,10 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc },cliDisplayName:"Button",cliDisplayDescription:"Customise the button/dropdown layout of this ticket option."})}, //TICKET ADMINS - {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role",cliDisplayName:"Ticket Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to interact with this ticket option."},{cliDisplayName:"Ticket Admin Role",cliDisplayDescription:"The discord role ID of a ticket admin."})}, - {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliInitDefaultValue:[],cliDisplayPropertyName:"read-only ticket admin role",cliDisplayName:"Readonly Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to read this ticket option."},{cliDisplayName:"Readonly Admin Role",cliDisplayDescription:"The discord role ID of a readonly admin."})}, - {key:"allowCreationByBlacklistedUsers",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-allow-blacklisted-users",{cliDisplayName:"Allow Creation By Blacklisted Users",cliDisplayDescription:"When enabled, the blacklist doesn't apply to this ticket option/type and users are still able to create a ticket."})}, - {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question",cliDisplayName:"Questions",cliDisplayDescription:"A list of valid question IDs to ask before creating this ticket."},{cliDisplayName:"Question ID",cliDisplayDescription:"A valid question ID from the questions.json config.",cliAutocompleteFunc:async () => { + {key:"ticketAdmins",checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role",cliDisplayName:"Ticket Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to interact with this ticket option."},{cliDisplayName:"Ticket Admin Role",cliDisplayDescription:"The discord role ID of a ticket admin."})}, + {key:"readonlyAdmins",checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliInitDefaultValue:[],cliDisplayPropertyName:"read-only ticket admin role",cliDisplayName:"Readonly Admin Roles",cliDisplayDescription:"A list of role IDs that are only able to read this ticket option."},{cliDisplayName:"Readonly Admin Role",cliDisplayDescription:"The discord role ID of a readonly admin."})}, + {key:"allowCreationByBlacklistedUsers",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-allow-blacklisted-users",{cliDisplayName:"Allow Creation By Blacklisted Users",cliDisplayDescription:"When enabled, the blacklist doesn't apply to this ticket option/type and users are still able to create a ticket."})}, + {key:"questions",checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question",cliDisplayName:"Questions",cliDisplayDescription:"A list of valid question IDs to ask before creating this ticket."},{cliDisplayName:"Question ID",cliDisplayDescription:"A valid question ID from the questions.json config.",cliAutocompleteFunc:async () => { const uncheckedRawData = opendiscord.configs.get("opendiscord:questions").data if (!Array.isArray(uncheckedRawData)) return null const idList = uncheckedRawData.filter((option) => typeof option == "object" && typeof option["id"] == "string").map((option) => option.id) @@ -400,82 +400,82 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc }})}, //TICKET CHANNEL - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{cliInitSkipKeys:["backupCategory","claimedCategory"],children:[ - {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-prefix",{maxLength:25,regex:/^[^\s]*$/,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix of the name of the ticket channel. (e.g. 'question-')"})}, - {key:"suffix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-suffix",{choices:["user-name","user-id","random-number","random-hex","counter-dynamic","counter-fixed"],cliDisplayName:"Suffix",cliDisplayDescription:"The suffix mode to use. The number/text will be appended after the prefix."})}, + {key:"channel",checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{cliInitSkipKeys:["backupCategory","claimedCategory"],children:[ + {key:"prefix",checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-prefix",{maxLength:25,regex:/^[^\s]*$/,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix of the name of the ticket channel. (e.g. 'question-')"})}, + {key:"suffix",checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-suffix",{choices:["user-name","user-id","random-number","random-hex","counter-dynamic","counter-fixed"],cliDisplayName:"Suffix",cliDisplayDescription:"The suffix mode to use. The number/text will be appended after the prefix."})}, - {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-category","category",true,[],{cliDisplayName:"Category",cliDisplayDescription:"The category the ticket will be created in. Leave empty for no category."})}, - {key:"closedCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-closed-category","category",true,[],{cliDisplayName:"Closed Category",cliDisplayDescription:"An additional category where the ticket will be moved to when closed."})}, - {key:"backupCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-backup-category","category",true,[],{cliDisplayName:"Backup Category",cliDisplayDescription:"An additional category where the ticket will be created in when the primary category is full (50 channels)."})}, - {key:"claimedCategory",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],cliDisplayPropertyName:"claim category",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel-claimed-category",{children:[ - {key:"user",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-user","user",false,[],{cliDisplayName:"User",cliDisplayDescription:"A discord user ID of the ticket claimer."})}, - {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-category","category",false,[],{cliDisplayName:"Category",cliDisplayDescription:"A discord category ID to move the ticket to."})} + {key:"category",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-category","category",true,[],{cliDisplayName:"Category",cliDisplayDescription:"The category the ticket will be created in. Leave empty for no category."})}, + {key:"closedCategory",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-closed-category","category",true,[],{cliDisplayName:"Closed Category",cliDisplayDescription:"An additional category where the ticket will be moved to when closed."})}, + {key:"backupCategory",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-backup-category","category",true,[],{cliDisplayName:"Backup Category",cliDisplayDescription:"An additional category where the ticket will be created in when the primary category is full (50 channels)."})}, + {key:"claimedCategory",checker:new api.ODCheckerArrayStructure("opendiscord:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],cliDisplayPropertyName:"claim category",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel-claimed-category",{children:[ + {key:"user",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-user","user",false,[],{cliDisplayName:"User",cliDisplayDescription:"A discord user ID of the ticket claimer."})}, + {key:"category",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-category","category",false,[],{cliDisplayName:"Category",cliDisplayDescription:"A discord category ID to move the ticket to."})} ],cliDisplayName:"Claimed Category",cliDisplayDescription:"A collection of a user ID and a category ID. The ticket will be moved to the category when this user claims the ticket."}),cliDisplayName:"Claimed Categories",cliDisplayDescription:"Add categories to move the ticket to when a user claims a ticket."})}, - {key:"topic",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-topic",{cliDisplayName:"Channel Topic",cliDisplayDescription:"The topic text of the ticket channel. Visible in the discord client when general.json 'channelTopic'.'showOptionTopic' is enabled."})}, + {key:"topic",checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-topic",{cliDisplayName:"Channel Topic",cliDisplayDescription:"The topic text of the ticket channel. Visible in the discord client when general.json 'channelTopic'.'showOptionTopic' is enabled."})}, ],cliDisplayName:"Channel",cliDisplayDescription:"Manage all settings related to the ticket channel and categories."})}, //DM MESSAGE - {key:"dmMessage",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-dm-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-dm-message",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-message-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the DM message on ticket creation."})}, - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096,cliDisplayName:"Message Text",cliDisplayDescription:"The raw text of the DM message. Leave empty to only use the embed."})}, - {key:"embed",optional:false,priority:0,checker:createTicketEmbedStructure("opendiscord:ticket-message-embed")} + {key:"dmMessage",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-dm-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-dm-message",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-message-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the DM message on ticket creation."})}, + {key:"text",checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096,cliDisplayName:"Message Text",cliDisplayDescription:"The raw text of the DM message. Leave empty to only use the embed."})}, + {key:"embed",checker:createTicketEmbedStructure("opendiscord:ticket-message-embed")} ],cliDisplayName:"DM Message",cliDisplayDescription:"The DM message is the message that will be sent to the creator of the ticket when he/she creates a ticket."}),cliDisplayName:"DM Message",cliDisplayDescription:"The DM message is the message that will be sent to the creator of the ticket when he/she creates a ticket."})}, //TICKET MESSAGE - {key:"ticketMessage",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-message",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-message-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the ticket message on ticket creation. (Recommended)"})}, - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096,cliDisplayName:"Message Text",cliDisplayDescription:"The raw text of the ticket message. Leave empty to only use the embed."})}, - {key:"embed",optional:false,priority:0,checker:createTicketEmbedStructure("opendiscord:ticket-message-embed")}, - {key:"ping",optional:false,priority:0,checker:createTicketPingStructure("opendiscord:ticket-message-ping")} + {key:"ticketMessage",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-message",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-message",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-message-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the ticket message on ticket creation. (Recommended)"})}, + {key:"text",checker:new api.ODCheckerStringStructure("opendiscord:ticket-message-text",{maxLength:4096,cliDisplayName:"Message Text",cliDisplayDescription:"The raw text of the ticket message. Leave empty to only use the embed."})}, + {key:"embed",checker:createTicketEmbedStructure("opendiscord:ticket-message-embed")}, + {key:"ping",checker:createTicketPingStructure("opendiscord:ticket-message-ping")} ],cliDisplayName:"Ticket Message",cliDisplayDescription:"The Ticket Message is the message that will be sent in the ticket itself. It contains a few buttons for quick access to actions."}),cliDisplayName:"Ticket Message",cliDisplayDescription:"The Ticket Message is the message that will be sent in the ticket itself. It contains a few buttons for quick access to actions."})}, //AUTOCLOSE - {key:"autoclose",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-autoclose",{children:[ - {key:"enableInactiveHours",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-hours",{cliDisplayName:"Enable Inactive Hours",cliDisplayDescription:"Enable/disable closing the ticket when it has been inactive for the configured amount of time."})}, - {key:"inactiveHours",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autoclose-hours",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:8544,cliDisplayName:"Inactive Hours",cliDisplayDescription:"The amount of hours the ticket must be inactive."})}, - {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-leave",{cliDisplayName:"Enable User Leave",cliDisplayDescription:"Instantly close the ticket when the creator of the ticket leaves the server."})}, - {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-disable-claim",{cliDisplayName:"Disable On Claim",cliDisplayDescription:"Disable the autoclose system when the ticket is claimed by any admin."})}, + {key:"autoclose",checker:new api.ODCheckerObjectStructure("opendiscord:ticket-autoclose",{children:[ + {key:"enableInactiveHours",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-hours",{cliDisplayName:"Enable Inactive Hours",cliDisplayDescription:"Enable/disable closing the ticket when it has been inactive for the configured amount of time."})}, + {key:"inactiveHours",checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autoclose-hours",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:8544,cliDisplayName:"Inactive Hours",cliDisplayDescription:"The amount of hours the ticket must be inactive."})}, + {key:"enableUserLeave",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-enable-leave",{cliDisplayName:"Enable User Leave",cliDisplayDescription:"Instantly close the ticket when the creator of the ticket leaves the server."})}, + {key:"disableOnClaim",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autoclose-disable-claim",{cliDisplayName:"Disable On Claim",cliDisplayDescription:"Disable the autoclose system when the ticket is claimed by any admin."})}, ],cliDisplayName:"Autoclose",cliDisplayDescription:"Manage the autoclose system for this ticket type/option."})}, //AUTODELETE - {key:"autodelete",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-autodelete",{children:[ - {key:"enableInactiveDays",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-days",{cliDisplayName:"Enable Inactive Days",cliDisplayDescription:"Enable/disable deleting the ticket when it has been inactive for the configured amount of time."})}, - {key:"inactiveDays",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autodelete-days",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:356,cliDisplayName:"Inactive Days",cliDisplayDescription:"The amount of days the ticket must be inactive."})}, - {key:"enableUserLeave",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-leave",{cliDisplayName:"Enable User Leave",cliDisplayDescription:"Instantly delete the ticket when the creator of the ticket leaves the server."})}, - {key:"disableOnClaim",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-disable-claim",{cliDisplayName:"Disable On Claim",cliDisplayDescription:"Disable the autodelete system when the ticket is claimed by any admin."})}, + {key:"autodelete",checker:new api.ODCheckerObjectStructure("opendiscord:ticket-autodelete",{children:[ + {key:"enableInactiveDays",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-days",{cliDisplayName:"Enable Inactive Days",cliDisplayDescription:"Enable/disable deleting the ticket when it has been inactive for the configured amount of time."})}, + {key:"inactiveDays",checker:new api.ODCheckerNumberStructure("opendiscord:ticket-autodelete-days",{zeroAllowed:false,negativeAllowed:false,floatAllowed:true,min:1,max:356,cliDisplayName:"Inactive Days",cliDisplayDescription:"The amount of days the ticket must be inactive."})}, + {key:"enableUserLeave",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-enable-leave",{cliDisplayName:"Enable User Leave",cliDisplayDescription:"Instantly delete the ticket when the creator of the ticket leaves the server."})}, + {key:"disableOnClaim",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-autodelete-disable-claim",{cliDisplayName:"Disable On Claim",cliDisplayDescription:"Disable the autodelete system when the ticket is claimed by any admin."})}, ],cliDisplayName:"Autodelete",cliDisplayDescription:"Manage the autodelete system for this ticket type/option."})}, //COOLDOWN - {key:"cooldown",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-cooldown",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-cooldown",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-cooldown-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the cooldown of this ticket option."})}, - {key:"cooldownMinutes",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-cooldown-minutes",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,max:512640,cliDisplayName:"Cooldown Minutes",cliDisplayDescription:"The amount of minutes a user needs to wait before creating another ticket of this type/option."})}, + {key:"cooldown",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-cooldown",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-cooldown",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-cooldown-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the cooldown of this ticket option."})}, + {key:"cooldownMinutes",checker:new api.ODCheckerNumberStructure("opendiscord:ticket-cooldown-minutes",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,max:512640,cliDisplayName:"Cooldown Minutes",cliDisplayDescription:"The amount of minutes a user needs to wait before creating another ticket of this type/option."})}, ],cliDisplayName:"Cooldown",cliDisplayDescription:"Manage cooldowns for this ticket type/option."}),cliDisplayName:"Cooldown",cliDisplayDescription:"Manage cooldowns for this ticket type/option."})}, //LIMITS - {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-limits",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the limits of this ticket option. This is not related to the global ticket limits."})}, - {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:10,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option that are able to exist in the server at the same time."})}, - {key:"userMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:3,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option from a specific user that are able to exist in the server at the same time."})} + {key:"limits",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-limits",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-limits-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the limits of this ticket option. This is not related to the global ticket limits."})}, + {key:"globalMaximum",checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:10,cliDisplayName:"Global Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option that are able to exist in the server at the same time."})}, + {key:"userMaximum",checker:new api.ODCheckerNumberStructure("opendiscord:ticket-limits-user",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:3,cliDisplayName:"User Maximum",cliDisplayDescription:"The maximum amount of tickets of this type/option from a specific user that are able to exist in the server at the same time."})} ],cliDisplayName:"Option Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."}),cliDisplayName:"Limits",cliDisplayDescription:"Manage option-based limits for ticket creation to reduce the workload on your support team."})}, //SLOW MODE - {key:"slowMode",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-slowmode",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-limits",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-slowmode-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable ticket slow mode."})}, - {key:"slowModeSeconds",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:ticket-slowmode-seconds",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:20,cliDisplayName:"Slow Mode Seconds",cliDisplayDescription:"The amount of seconds users need to wait between sending messages."})}, + {key:"slowMode",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:ticket-slowmode",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-limits",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-slowmode-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable ticket slow mode."})}, + {key:"slowModeSeconds",checker:new api.ODCheckerNumberStructure("opendiscord:ticket-slowmode-seconds",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1,cliInitDefaultValue:20,cliDisplayName:"Slow Mode Seconds",cliDisplayDescription:"The amount of seconds users need to wait between sending messages."})}, ],cliDisplayName:"Option Slow Mode",cliDisplayDescription:"Add slow mode to this ticket option to restrict the amount of message spam users can send."}),cliDisplayName:"Slow Mode",cliDisplayDescription:"Add slow mode to this ticket option to restrict the amount of message spam users can send."})}, ],cliDisplayName:"Ticket Option",cliDisplayDescription:"Manage all ticket-specific settings of this option/type."})}, //WEBSITE {name:"Website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("opendiscord:website",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:website-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this website option. Used in panels."})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this website option."})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this website option."})}, + {key:"id",checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:website-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this website option. Used in panels."})}, + {key:"name",checker:new api.ODCheckerStringStructure("opendiscord:website-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this website option."})}, + {key:"description",checker:new api.ODCheckerStringStructure("opendiscord:website-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this website option."})}, //WEBSITE BUTTON - {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ - {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, - {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"button",checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ + {key:"emoji",checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"label",checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, ],custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) //check if emoji & label exists @@ -488,20 +488,20 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc },cliDisplayName:"Button",cliDisplayDescription:"Customise the button layout of this website option."})}, //WEBSITE URL - {key:"url",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:website-url",false,{allowHttp:false},{cliDisplayName:"URL",cliDisplayDescription:"The URL this button will link to."})}, + {key:"url",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:website-url",false,{allowHttp:false},{cliDisplayName:"URL",cliDisplayDescription:"The URL this button will link to."})}, ],cliDisplayName:"Website Option",cliDisplayDescription:"Manage all settings of this website/url option."})}, //REACTION ROLES {name:"Reaction Role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("opendiscord:role",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:role-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this role option. Used in panels."})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this role option."})}, - {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this role option."})}, + {key:"id",checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:role-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this role option. Used in panels."})}, + {key:"name",checker:new api.ODCheckerStringStructure("opendiscord:role-name",{minLength:2,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this role option."})}, + {key:"description",checker:new api.ODCheckerStringStructure("opendiscord:role-description",{maxLength:256,cliDisplayName:"Description",cliDisplayDescription:"The description of this role option."})}, //ROLE BUTTON - {key:"button",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ - {key:"emoji",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, - {key:"label",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, - {key:"color",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"],cliDisplayName:"Color",cliDisplayDescription:"The color of the button. Does not apply when using a dropdown panel."})}, + {key:"button",checker:new api.ODCheckerObjectStructure("opendiscord:ticket-button",{children:[ + {key:"emoji",checker:new api.ODCheckerCustomStructure_EmojiString("opendiscord:ticket-button-emoji",0,1,true,{cliDisplayName:"Emoji",cliDisplayDescription:"The emoji of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"label",checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-label",{maxLength:80,cliDisplayName:"Label",cliDisplayDescription:"The label of the button. At least 1 of 2 (emoji/label) must be provided."})}, + {key:"color",checker:new api.ODCheckerStringStructure("opendiscord:ticket-button-color",{choices:["gray","red","green","blue"],cliDisplayName:"Color",cliDisplayDescription:"The color of the button. Does not apply when using a dropdown panel."})}, ],custom:(checker,value,locationTrace,locationId,locationDocs) => { const lt = checker.locationTraceDeref(locationTrace) //check if emoji & label exists @@ -514,18 +514,18 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc },cliDisplayName:"Button",cliDisplayDescription:"Customise the button layout of this reaction role option."})}, //ROLE SETTINGS - {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1,cliDisplayPropertyName:"role",cliDisplayName:"Roles",cliDisplayDescription:"A list of roles to add/remove when clicking on the button."},{cliDisplayName:"Role",cliDisplayDescription:"The discord role ID you want to add/remove."})}, - {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-mode",{choices:["add","remove","add&remove"],cliDisplayName:"Mode",cliDisplayDescription:"Decide how the button will work: add-only, remove-only or add & remove."})}, - {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false,cliDisplayPropertyName:"role",cliDisplayName:"Remove Roles On Add",cliDisplayDescription:"An additional list of roles to remove when the roles of this option are added. (Can be used to select between roles)"},{cliDisplayName:"Remove Role",cliDisplayDescription:"The discord role ID you want to remove when other roles are added."})}, - {key:"addOnMemberJoin",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:role-add-on-join",{cliDisplayName:"Add On Member Join",cliDisplayDescription:"Automatically add these roles to a user when joining the server."})}, + {key:"roles",checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1,cliDisplayPropertyName:"role",cliDisplayName:"Roles",cliDisplayDescription:"A list of roles to add/remove when clicking on the button."},{cliDisplayName:"Role",cliDisplayDescription:"The discord role ID you want to add/remove."})}, + {key:"mode",checker:new api.ODCheckerStringStructure("opendiscord:role-mode",{choices:["add","remove","add&remove"],cliDisplayName:"Mode",cliDisplayDescription:"Decide how the button will work: add-only, remove-only or add & remove."})}, + {key:"removeRolesOnAdd",checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false,cliDisplayPropertyName:"role",cliDisplayName:"Remove Roles On Add",cliDisplayDescription:"An additional list of roles to remove when the roles of this option are added. (Can be used to select between roles)"},{cliDisplayName:"Remove Role",cliDisplayDescription:"The discord role ID you want to remove when other roles are added."})}, + {key:"addOnMemberJoin",checker:new api.ODCheckerBooleanStructure("opendiscord:role-add-on-join",{cliDisplayName:"Add On Member Join",cliDisplayDescription:"Automatically add these roles to a user when joining the server."})}, ],cliDisplayName:"Reaction Role Option",cliDisplayDescription:"Manage all settings of this reaction role option."})}, ],cliDisplayName:"Option",cliDisplayDescription:"Manage an option of one of the 3 types: ticket, website, role."}),cliDisplayName:"Options",cliDisplayDescription:"A list of all options in the bot. Here you can add, modify & remove ticket types, website buttons & reaction roles!"}) export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendiscord:panels",{allowedTypes:["object"],cliDisplayPropertyName:"panel",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panels",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","dropdown"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this panel. Used in the /panel command."})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-name",{minLength:3,maxLength:50,cliDisplayName:"Name",cliDisplayDescription:"The name of this panel."})}, - {key:"dropdown",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-dropdown",{cliDisplayName:"Dropdown",cliDisplayDescription:"Decide whether to use buttons or a dropdown in the panel. Dropdowns only support options of the 'ticket' type!"})}, - {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,minLength:1,maxLength:25,cliDisplayPropertyName:"option",cliDisplayName:"Options",cliDisplayDescription:"A list of valid option IDs to show in this panel."},{cliDisplayName:"Option ID",cliDisplayDescription:"A valid option ID from the options.json config.",cliAutocompleteFunc:async () => { + {key:"id",checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this panel. Used in the /panel command."})}, + {key:"name",checker:new api.ODCheckerStringStructure("opendiscord:panel-name",{minLength:3,maxLength:50,cliDisplayName:"Name",cliDisplayDescription:"The name of this panel."})}, + {key:"dropdown",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-dropdown",{cliDisplayName:"Dropdown",cliDisplayDescription:"Decide whether to use buttons or a dropdown in the panel. Dropdowns only support options of the 'ticket' type!"})}, + {key:"options",checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,minLength:1,maxLength:25,cliDisplayPropertyName:"option",cliDisplayName:"Options",cliDisplayDescription:"A list of valid option IDs to show in this panel."},{cliDisplayName:"Option ID",cliDisplayDescription:"A valid option ID from the options.json config.",cliAutocompleteFunc:async () => { const uncheckedRawData = opendiscord.configs.get("opendiscord:options").data if (!Array.isArray(uncheckedRawData)) return null const idList = uncheckedRawData.filter((option) => typeof option == "object" && typeof option["id"] == "string").map((option) => option.id) @@ -533,104 +533,104 @@ export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendisco }})}, //EMBED & TEXT - {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-text",{maxLength:4096,cliDisplayName:"Panel Text",cliDisplayDescription:"The raw text of the panel message. Leave empty to use the embed."})}, - {key:"embed",optional:false,priority:0,checker:createPanelEmbedStructure("opendiscord:panel-embed")}, + {key:"text",checker:new api.ODCheckerStringStructure("opendiscord:panel-text",{maxLength:4096,cliDisplayName:"Panel Text",cliDisplayDescription:"The raw text of the panel message. Leave empty to use the embed."})}, + {key:"embed",checker:createPanelEmbedStructure("opendiscord:panel-embed")}, //SETTINGS - {key:"settings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:panel-settings",{cliInitSkipKeys:["dropdownPlaceholder","describeOptionsCustomTitle"],children:[ - {key:"dropdownPlaceholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-placeholder",{maxLength:100,cliInitDefaultValue:"Create a ticket!",cliDisplayName:"Dropdown Placeholder",cliDisplayDescription:"Configure the text displayed in the dropdown when nothing is selected."})}, - {key:"enableMaxTicketsWarningInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-text",{cliDisplayName:"Enable Max Tickets Warning (Text)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the text contents of the panel."})}, - {key:"enableMaxTicketsWarningInEmbed",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-embed",{cliDisplayName:"Enable Max Tickets Warning (Embed)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the embed of the panel."})}, + {key:"settings",checker:new api.ODCheckerObjectStructure("opendiscord:panel-settings",{cliInitSkipKeys:["dropdownPlaceholder","describeOptionsCustomTitle"],children:[ + {key:"dropdownPlaceholder",checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-placeholder",{maxLength:100,cliInitDefaultValue:"Create a ticket!",cliDisplayName:"Dropdown Placeholder",cliDisplayDescription:"Configure the text displayed in the dropdown when nothing is selected."})}, + {key:"enableMaxTicketsWarningInText",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-text",{cliDisplayName:"Enable Max Tickets Warning (Text)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the text contents of the panel."})}, + {key:"enableMaxTicketsWarningInEmbed",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-maxtickets-embed",{cliDisplayName:"Enable Max Tickets Warning (Embed)",cliDisplayDescription:"Enable/disable the warning which shows how many tickets you can create in the embed of the panel."})}, - {key:"describeOptionsLayout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-layout",{choices:["simple","normal","detailed"],cliDisplayName:"Describe Options Layout",cliDisplayDescription:"The layout to use in the auto-generated option descriptions (simple, normal, detailed)."})}, - {key:"describeOptionsCustomTitle",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-title",{maxLength:512,cliDisplayName:"Describe Options Title",cliDisplayDescription:"Customise the title to use in the auto-generated option descriptions."})}, - {key:"describeOptionsInText",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-text",{cliDisplayName:"Describe Options In Text",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the raw text contents of the panel."})}, - {key:"describeOptionsInEmbedFields",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-fields",{cliDisplayName:"Describe Options In Embed Fields",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the embed fields of the panel."})}, - {key:"describeOptionsInEmbedDescription",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-embed",{cliDisplayName:"Describe Options In Embed Description",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the embed description of the panel."})}, + {key:"describeOptionsLayout",checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-layout",{choices:["simple","normal","detailed"],cliDisplayName:"Describe Options Layout",cliDisplayDescription:"The layout to use in the auto-generated option descriptions (simple, normal, detailed)."})}, + {key:"describeOptionsCustomTitle",checker:new api.ODCheckerStringStructure("opendiscord:panel-settings-describe-title",{maxLength:512,cliDisplayName:"Describe Options Title",cliDisplayDescription:"Customise the title to use in the auto-generated option descriptions."})}, + {key:"describeOptionsInText",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-text",{cliDisplayName:"Describe Options In Text",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the raw text contents of the panel."})}, + {key:"describeOptionsInEmbedFields",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-fields",{cliDisplayName:"Describe Options In Embed Fields",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the embed fields of the panel."})}, + {key:"describeOptionsInEmbedDescription",checker:new api.ODCheckerBooleanStructure("opendiscord:panel-settings-describe-embed",{cliDisplayName:"Describe Options In Embed Description",cliDisplayDescription:"Enable/disable showing the auto-generated option descriptions in the embed description of the panel."})}, ],cliDisplayName:"Settings",cliDisplayDescription:"Manage additional settings & customisability for this panel."})}, ],cliDisplayName:"Panel",cliDisplayDescription:"Manage, customise and configure a panel to your preference."}),cliDisplayName:"Panels",cliDisplayDescription:"A list of all panels in the bot. Here you can add, modify & remove existing panels or customise them to your preference."}) export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("opendiscord:questions",{allowedTypes:["object"],cliDisplayPropertyName:"question",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:questions",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ - {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this question. Used in ticket options."})}, - {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-name",{minLength:3,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this question."})}, - {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-type",{choices:["short","paragraph"],cliDisplayName:"Type",cliDisplayDescription:"The type of this question (short/paragraph)."})}, + {key:"id",checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40,cliDisplayName:"Id",cliDisplayDescription:"The id of this question. Used in ticket options."})}, + {key:"name",checker:new api.ODCheckerStringStructure("opendiscord:question-name",{minLength:3,maxLength:45,cliDisplayName:"Name",cliDisplayDescription:"The name of this question."})}, + {key:"type",checker:new api.ODCheckerStringStructure("opendiscord:question-type",{choices:["short","paragraph"],cliDisplayName:"Type",cliDisplayDescription:"The type of this question (short/paragraph)."})}, - {key:"required",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-required",{cliDisplayName:"Required",cliDisplayDescription:"Is this question required? If not, it can be left empty."})}, - {key:"placeholder",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-placeholder",{maxLength:100,cliDisplayName:"Placeholder",cliDisplayDescription:"The placeholder to show in the field when nothing has been written yet."})}, + {key:"required",checker:new api.ODCheckerBooleanStructure("opendiscord:question-required",{cliDisplayName:"Required",cliDisplayDescription:"Is this question required? If not, it can be left empty."})}, + {key:"placeholder",checker:new api.ODCheckerStringStructure("opendiscord:question-placeholder",{maxLength:100,cliDisplayName:"Placeholder",cliDisplayDescription:"The placeholder to show in the field when nothing has been written yet."})}, - {key:"length",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:question-length",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:question-length",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:question-length-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable length validation for this question."})}, - {key:"min",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:question-length-min",{min:0,max:1024,negativeAllowed:false,floatAllowed:false,cliDisplayName:"Min Length",cliDisplayDescription:"The minimum amount of characters required."})}, - {key:"max",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:question-length-max",{min:1,max:1024,negativeAllowed:false,floatAllowed:false,cliInitDefaultValue:100,cliDisplayName:"Max Length",cliDisplayDescription:"The maximum amount of characters allowed."})}, + {key:"length",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:question-length",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:question-length",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:question-length-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable length validation for this question."})}, + {key:"min",checker:new api.ODCheckerNumberStructure("opendiscord:question-length-min",{min:0,max:1024,negativeAllowed:false,floatAllowed:false,cliDisplayName:"Min Length",cliDisplayDescription:"The minimum amount of characters required."})}, + {key:"max",checker:new api.ODCheckerNumberStructure("opendiscord:question-length-max",{min:1,max:1024,negativeAllowed:false,floatAllowed:false,cliInitDefaultValue:100,cliDisplayName:"Max Length",cliDisplayDescription:"The maximum amount of characters allowed."})}, ],cliDisplayName:"Length Validation",cliDisplayDescription:"Add length validation to the question. This way, the contents must be at least/most ... characters."}),cliDisplayName:"Length Validation",cliDisplayDescription:"Add length validation to the question. This way, the contents must be at least/most ... characters."})}, ],cliDisplayName:"Question",cliDisplayDescription:"Manage, customise and configure a question to your preference."}),cliDisplayName:"Questions",cliDisplayDescription:"A list of all questions in the bot. Here you can add, modify & remove existing questions or customise them to your preference."}) export const defaultTranscriptsStructure = new api.ODCheckerObjectStructure("opendiscord:transcripts",{children:[ //GENERAL - {key:"general",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-general",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-general",{children:[ - {key:"enabled",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the transcript system."})}, + {key:"general",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-general",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-general",{children:[ + {key:"enabled",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable the transcript system."})}, - {key:"enableChannel",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-channel",{cliDisplayName:"Enable Channel",cliDisplayDescription:"Send the transcript to a specific channel in your server (configurable in 'channel' property)."})}, - {key:"enableCreatorDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-creator-dm",{cliDisplayName:"Enable Creator DM",cliDisplayDescription:"Send the transcript in DM to the creator of the ticket."})}, - {key:"enableParticipantDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-participant-dm",{cliDisplayName:"Enable Participant DM",cliDisplayDescription:"Send the transcript in DM to all non-admin participants of the ticket."})}, - {key:"enableActiveAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-active-admin-dm",{cliDisplayName:"Enable Active Admin DM",cliDisplayDescription:"Send the transcript in DM to all admins that actively wrote in the ticket."})}, - {key:"enableEveryAdminDM",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-every-admin-dm",{cliDisplayName:"Enable Every Admin DM",cliDisplayDescription:"Send the transcript in DM to all admins assigned to the ticket."})}, + {key:"enableChannel",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-channel",{cliDisplayName:"Enable Channel",cliDisplayDescription:"Send the transcript to a specific channel in your server (configurable in 'channel' property)."})}, + {key:"enableCreatorDM",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-creator-dm",{cliDisplayName:"Enable Creator DM",cliDisplayDescription:"Send the transcript in DM to the creator of the ticket."})}, + {key:"enableParticipantDM",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-participant-dm",{cliDisplayName:"Enable Participant DM",cliDisplayDescription:"Send the transcript in DM to all non-admin participants of the ticket."})}, + {key:"enableActiveAdminDM",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-active-admin-dm",{cliDisplayName:"Enable Active Admin DM",cliDisplayDescription:"Send the transcript in DM to all admins that actively wrote in the ticket."})}, + {key:"enableEveryAdminDM",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-enable-every-admin-dm",{cliDisplayName:"Enable Every Admin DM",cliDisplayDescription:"Send the transcript in DM to all admins assigned to the ticket."})}, - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:transcripts-channel","channel",true,[],{cliDisplayName:"Channel",cliDisplayDescription:"The discord channel ID to send the transcript to."})}, - {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-mode",{choices:["html","text"],cliDisplayName:"Transcript Mode",cliDisplayDescription:"The transcript type to use: 'text' or 'html'."})}, + {key:"channel",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:transcripts-channel","channel",true,[],{cliDisplayName:"Channel",cliDisplayDescription:"The discord channel ID to send the transcript to."})}, + {key:"mode",checker:new api.ODCheckerStringStructure("opendiscord:transcripts-mode",{choices:["html","text"],cliDisplayName:"Transcript Mode",cliDisplayDescription:"The transcript type to use: 'text' or 'html'."})}, ],cliDisplayName:"General",cliDisplayDescription:"General settings for the transcripts."}),cliDisplayName:"General",cliDisplayDescription:"General settings for the transcripts."})}, //EMBED SETTINGS - {key:"embedSettings",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-embed-settings",{children:[ - {key:"customColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-embed-color",false,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Use a custom color in the embed. When empty, the default bot color will be used."})}, - {key:"listAllParticipants",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-list-participants",{cliDisplayName:"List Participants",cliDisplayDescription:"List all participants of the ticket in the embed."})}, - {key:"includeTicketStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-include-ticket-stats",{cliDisplayName:"Include Ticket Stats",cliDisplayDescription:"Include some stats from the ticket in the embed."})}, + {key:"embedSettings",checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-embed-settings",{children:[ + {key:"customColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-embed-color",false,true,{cliDisplayName:"Custom Color",cliDisplayDescription:"Use a custom color in the embed. When empty, the default bot color will be used."})}, + {key:"listAllParticipants",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-list-participants",{cliDisplayName:"List Participants",cliDisplayDescription:"List all participants of the ticket in the embed."})}, + {key:"includeTicketStats",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-embed-include-ticket-stats",{cliDisplayName:"Include Ticket Stats",cliDisplayDescription:"Include some stats from the ticket in the embed."})}, ],cliDisplayName:"Embed Settings",cliDisplayDescription:"Settings and customisability related to the embed which contains the transcript."})}, //TEXT STYLE - {key:"textTranscriptStyle",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-text",{children:[ - {key:"layout",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-layout",{choices:["simple","normal","detailed"],cliDisplayName:"Layout",cliDisplayDescription:"The layout to use in the text-transcripts (simple, normal, detailed)."})}, - {key:"includeStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-stats",{cliDisplayName:"Include Stats",cliDisplayDescription:"Include statistics in the transcript?"})}, - {key:"includeIds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-ids",{cliDisplayName:"Include Ids",cliDisplayDescription:"Include role, channel & user ID's in the transcript?"})}, - {key:"includeEmbeds",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-embeds",{cliDisplayName:"Include Embeds",cliDisplayDescription:"Include message embeds in the transcript?"})}, - {key:"includeFiles",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-files",{cliDisplayName:"Include Files",cliDisplayDescription:"Include files & attachments in the transcript?"})}, - {key:"includeBotMessages",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-bots",{cliDisplayName:"Include Bots",cliDisplayDescription:"Include messages sent by bots/apps?"})}, + {key:"textTranscriptStyle",checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-text",{children:[ + {key:"layout",checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-layout",{choices:["simple","normal","detailed"],cliDisplayName:"Layout",cliDisplayDescription:"The layout to use in the text-transcripts (simple, normal, detailed)."})}, + {key:"includeStats",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-stats",{cliDisplayName:"Include Stats",cliDisplayDescription:"Include statistics in the transcript?"})}, + {key:"includeIds",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-ids",{cliDisplayName:"Include Ids",cliDisplayDescription:"Include role, channel & user ID's in the transcript?"})}, + {key:"includeEmbeds",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-embeds",{cliDisplayName:"Include Embeds",cliDisplayDescription:"Include message embeds in the transcript?"})}, + {key:"includeFiles",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-files",{cliDisplayName:"Include Files",cliDisplayDescription:"Include files & attachments in the transcript?"})}, + {key:"includeBotMessages",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-text-include-bots",{cliDisplayName:"Include Bots",cliDisplayDescription:"Include messages sent by bots/apps?"})}, - {key:"fileMode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-file-mode",{choices:["custom","channel-name","channel-id","user-name","user-id"],cliDisplayName:"File Mode",cliDisplayDescription:"Select the mode the transcript will be named: custom, channel-name, user-name, user-id."})}, - {key:"customFileName",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:transcripts-file-name",{maxLength:512,regex:/^[^\.#%&{}\\<>*?/!'":@`|=]*$/,cliDisplayName:"Custom File Name",cliDisplayDescription:"Use this as transcript name when the mode is set to 'custom'."})}, + {key:"fileMode",checker:new api.ODCheckerStringStructure("opendiscord:transcripts-text-file-mode",{choices:["custom","channel-name","channel-id","user-name","user-id"],cliDisplayName:"File Mode",cliDisplayDescription:"Select the mode the transcript will be named: custom, channel-name, user-name, user-id."})}, + {key:"customFileName",checker:new api.ODCheckerStringStructure("opendiscord:transcripts-file-name",{maxLength:512,regex:/^[^\.#%&{}\\<>*?/!'":@`|=]*$/,cliDisplayName:"Custom File Name",cliDisplayDescription:"Use this as transcript name when the mode is set to 'custom'."})}, ],cliDisplayName:"Text Transcript Style",cliDisplayDescription:"Configure the 'Text Transcripts' from Open Ticket."})}, //HTML STYLE - {key:"htmlTranscriptStyle",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html",{children:[ + {key:"htmlTranscriptStyle",checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html",{children:[ //HTML BACKGROUND - {key:"background",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-background",{property:"enableCustomBackground",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-background",{children:[ - {key:"enableCustomBackground",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-background-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable background customisation in the HTML Transcripts."})}, - {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-background-color",false,true,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the background."})}, - {key:"backgroundImage",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-background-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Background Image",cliDisplayDescription:"A URL to an image to use in the background. This will overwrite the background color."})}, + {key:"background",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-background",{property:"enableCustomBackground",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-background",{children:[ + {key:"enableCustomBackground",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-background-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable background customisation in the HTML Transcripts."})}, + {key:"backgroundColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-background-color",false,true,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the background."})}, + {key:"backgroundImage",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-background-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]},{cliDisplayName:"Background Image",cliDisplayDescription:"A URL to an image to use in the background. This will overwrite the background color."})}, ],cliDisplayName:"Background Style",cliDisplayDescription:"Customise the background of the HTML Transcripts."}),cliDisplayName:"Background Style",cliDisplayDescription:"Customise the background of the HTML Transcripts."})}, //HTML HEADER - {key:"header",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-header",{property:"enableCustomHeader",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-header",{children:[ - {key:"enableCustomHeader",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-header-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable header customisation in the HTML Transcripts."})}, - {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-bgcolor",false,false,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the header background."})}, - {key:"decoColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-decocolor",false,false,{cliDisplayName:"Decoration Color",cliDisplayDescription:"The hex-color of the header decoration (e.g. horizontal line)."})}, - {key:"textColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-textcolor",false,false,{cliDisplayName:"Text Color",cliDisplayDescription:"The hex-color of the header text."})}, + {key:"header",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-header",{property:"enableCustomHeader",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-header",{children:[ + {key:"enableCustomHeader",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-header-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable header customisation in the HTML Transcripts."})}, + {key:"backgroundColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-bgcolor",false,false,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the header background."})}, + {key:"decoColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-decocolor",false,false,{cliDisplayName:"Decoration Color",cliDisplayDescription:"The hex-color of the header decoration (e.g. horizontal line)."})}, + {key:"textColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-header-textcolor",false,false,{cliDisplayName:"Text Color",cliDisplayDescription:"The hex-color of the header text."})}, ],cliDisplayName:"Header Style",cliDisplayDescription:"Customise the header of the HTML Transcripts."}),cliDisplayName:"Header Style",cliDisplayDescription:"Customise the header of the HTML Transcripts."})}, //HTML STATS - {key:"stats",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-stats",{property:"enableCustomStats",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-stats",{children:[ - {key:"enableCustomStats",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-stats-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable stats customisation in the HTML Transcripts."})}, - {key:"backgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-bgcolor",false,false,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the stats background."})}, - {key:"keyTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-keycolor",false,false,{cliDisplayName:"Key Text Color",cliDisplayDescription:"The hex-color of the stats key text."})}, - {key:"valueTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-valuecolor",false,false,{cliDisplayName:"Value Text Color",cliDisplayDescription:"The hex-color of the stats value text."})}, - {key:"hideBackgroundColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidebgcolor",false,false,{cliDisplayName:"Hide Background Color",cliDisplayDescription:"The hex-color of the stats hide button background."})}, - {key:"hideTextColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidecolor",false,false,{cliDisplayName:"Hide Text Color",cliDisplayDescription:"The hex-color of the stats hide button text."})}, + {key:"stats",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-stats",{property:"enableCustomStats",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-stats",{children:[ + {key:"enableCustomStats",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-stats-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable stats customisation in the HTML Transcripts."})}, + {key:"backgroundColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-bgcolor",false,false,{cliDisplayName:"Background Color",cliDisplayDescription:"The hex-color of the stats background."})}, + {key:"keyTextColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-keycolor",false,false,{cliDisplayName:"Key Text Color",cliDisplayDescription:"The hex-color of the stats key text."})}, + {key:"valueTextColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-valuecolor",false,false,{cliDisplayName:"Value Text Color",cliDisplayDescription:"The hex-color of the stats value text."})}, + {key:"hideBackgroundColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidebgcolor",false,false,{cliDisplayName:"Hide Background Color",cliDisplayDescription:"The hex-color of the stats hide button background."})}, + {key:"hideTextColor",checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:transcripts-html-stats-hidecolor",false,false,{cliDisplayName:"Hide Text Color",cliDisplayDescription:"The hex-color of the stats hide button text."})}, ],cliDisplayName:"Stats Style",cliDisplayDescription:"Customise the stats of the HTML Transcripts."}),cliDisplayName:"Stats Style",cliDisplayDescription:"Customise the stats of the HTML Transcripts."})}, //HTML FAVICON - {key:"favicon",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-favicon",{property:"enableCustomFavicon",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-favicon",{children:[ - {key:"enableCustomFavicon",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-favicon-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable favicon customisation in the HTML Transcripts."})}, - {key:"imageUrl",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-favicon-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp"]},{cliDisplayName:"LOREMIPSUM",cliDisplayDescription:"IPSUMLOREM"})}, + {key:"favicon",checker:new api.ODCheckerEnabledObjectStructure("opendiscord:transcripts-html-favicon",{property:"enableCustomFavicon",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:transcripts-html-favicon",{children:[ + {key:"enableCustomFavicon",checker:new api.ODCheckerBooleanStructure("opendiscord:transcripts-html-favicon-enabled",{cliDisplayName:"Enabled",cliDisplayDescription:"Enable/disable favicon customisation in the HTML Transcripts."})}, + {key:"imageUrl",checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:transcripts-html-favicon-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp"]},{cliDisplayName:"LOREMIPSUM",cliDisplayDescription:"IPSUMLOREM"})}, ],cliDisplayName:"Favicon Style",cliDisplayDescription:"Customise the favicon of the HTML Transcripts."}),cliDisplayName:"Favicon Style",cliDisplayDescription:"Customise the favicon of the HTML Transcripts."})}, ],cliDisplayName:"Html Transcript Style",cliDisplayDescription:"Configure the 'Html Transcripts' from Open Ticket."})}, ],cliDisplayName:"Transcripts",cliDisplayDescription:"All settings related to transcripts."}) From 1ef74f0b6ba7b8f8a9a87613cc28c9793418e9b1 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:10:58 +0200 Subject: [PATCH 49/78] Custom reason for create-ticket-permissions action Added support for custom reasons in the opendiscord:create-ticket-permissions action. Now it's easier for plugins to add a custom message when the validation fails. --- src/builders/embeds.ts | 4 ++-- src/builders/messages.ts | 4 ++-- src/commands/ticket.ts | 3 +++ src/core/api/defaults/action.ts | 2 +- src/core/api/defaults/builder.ts | 4 ++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index f06d209..1265655 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -37,12 +37,12 @@ const errorEmbeds = () => { embeds.add(new api.ODEmbed("opendiscord:error")) embeds.get("opendiscord:error").workers.add( new api.ODWorker("opendiscord:error",0,async (instance,params,source) => { - const {user,error,layout} = params + const {user,error,layout,customTitle} = params const method = getMethodFromSource(source) instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.internalError"))) + instance.setTitle(utilities.emojiTitle("❌",customTitle ?? lang.getTranslation("errors.titles.internalError"))) instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setDescription(lang.getTranslationWithParams("errors.descriptions.internalError",[method]) + (layout == "simple") ? "\n"+error : "") if (layout == "advanced" && error) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+error+"```"}) diff --git a/src/builders/messages.ts b/src/builders/messages.ts index a371e28..8d79f82 100644 --- a/src/builders/messages.ts +++ b/src/builders/messages.ts @@ -332,8 +332,8 @@ const errorMessages = () => { messages.add(new api.ODMessage("opendiscord:error")) messages.get("opendiscord:error").workers.add( new api.ODWorker("opendiscord:error",0,async (instance,params,source) => { - const {guild,channel,user,error,layout} = params - instance.addEmbed(await embeds.getSafe("opendiscord:error").build(source,{guild,channel,user,error,layout})) + const {guild,channel,user,error,layout,customTitle} = params + instance.addEmbed(await embeds.getSafe("opendiscord:error").build(source,{guild,channel,user,error,layout,customTitle})) instance.setEphemeral(true) }) ) diff --git a/src/commands/ticket.ts b/src/commands/ticket.ts index 40e2cc6..3de8693 100644 --- a/src/commands/ticket.ts +++ b/src/commands/ticket.ts @@ -70,6 +70,7 @@ export const registerCommandResponders = async () => { else if (res.reason == "global-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) else if (res.reason == "option-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) else if (res.reason == "option-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) + else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? "You are unable to create a ticket. `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:"Permission Error"})) //TODO TRANSLATION!!! else instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) return cancel() } @@ -132,6 +133,7 @@ export const registerButtonResponders = async () => { else if (res.reason == "global-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) else if (res.reason == "option-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) else if (res.reason == "option-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) + else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? "You are unable to create a ticket. `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:"Permission Error"})) //TODO TRANSLATION!!! else instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) return cancel() } @@ -187,6 +189,7 @@ export const registerDropdownResponders = async () => { else if (res.reason == "global-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) else if (res.reason == "option-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) else if (res.reason == "option-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) + else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? "You are unable to create a ticket. `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:"Permission Error"})) //TODO TRANSLATION!!! else instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) return cancel() } diff --git a/src/core/api/defaults/action.ts b/src/core/api/defaults/action.ts index f3ef684..a6ec1a7 100644 --- a/src/core/api/defaults/action.ts +++ b/src/core/api/defaults/action.ts @@ -19,7 +19,7 @@ export interface ODActionManagerIds_Default { "opendiscord:create-ticket-permissions":{ source:"panel-button"|"panel-dropdown"|"slash"|"text"|"other", params:{guild:discord.Guild,user:discord.User,option:ODTicketOption}, - result:{valid:boolean,reason:"blacklist"|"cooldown"|"global-limit"|"global-user-limit"|"option-limit"|"option-user-limit"|null,cooldownUntil?:Date}, + result:{valid:boolean,reason:"blacklist"|"cooldown"|"global-limit"|"global-user-limit"|"option-limit"|"option-user-limit"|"custom"|null,cooldownUntil?:Date,customReason?:string}, workers:"opendiscord:check-blacklist"|"opendiscord:check-cooldown"|"opendiscord:check-global-limits"|"opendiscord:check-option-limits"|"opendiscord:valid" }, "opendiscord:create-transcript":{ diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index b105a18..34bf3d9 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -224,7 +224,7 @@ export class ODFile_Default},workers:"opendiscord:verifybar-unpin-message"} "opendiscord:verifybar-autoclose-message":{source:"verifybar",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message},workers:"opendiscord:verifybar-autoclose-message"} - "opendiscord:error":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:string,layout:"simple"|"advanced"},workers:"opendiscord:error"}, + "opendiscord:error":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:string,layout:"simple"|"advanced",customTitle?:string},workers:"opendiscord:error"}, "opendiscord:error-option-missing":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorMissingOption},workers:"opendiscord:error-option-missing"}, "opendiscord:error-option-invalid":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorInvalidOption},workers:"opendiscord:error-option-invalid"}, "opendiscord:error-unknown-command":{source:"slash"|"text"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,error:ODTextCommandErrorUnknownCommand},workers:"opendiscord:error-unknown-command"}, From 793c6f060351558c0f453efd6ab4889c217a4e49 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 28 Aug 2025 08:59:16 +0200 Subject: [PATCH 50/78] Added: Slow Mode & Updated discord.js --- package.json | 2 +- src/actions/createTicket.ts | 13 ++++++++----- src/core/api/modules/client.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index c5d0b7d..dbb3e3e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@types/node": "^22.5.0", "@types/terminal-kit": "^2.5.7", "ansis": "^2.3.0", - "discord.js": "^14.19.3", + "discord.js": "^14.22.1", "formatted-json-stringify": "^1.2.1", "terminal-kit": "^3.1.2", "typescript": "^5.5.4" diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index 9bd1467..e877a90 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -73,7 +73,7 @@ export const registerActions = async () => { permissions.push({ type:discord.OverwriteType.Role, id:admin, - allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages","PinMessages","EmbedLinks"], deny:[] }) }) @@ -82,7 +82,7 @@ export const registerActions = async () => { permissions.push({ type:discord.OverwriteType.Role, id:admin, - allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages","PinMessages","EmbedLinks"], deny:[] }) }) @@ -93,15 +93,17 @@ export const registerActions = async () => { type:discord.OverwriteType.Role, id:admin, allow:["ViewChannel","ReadMessageHistory"], - deny:["SendMessages","AddReactions","AttachFiles","SendPolls"] + deny:["SendMessages","AddReactions","AttachFiles","SendPolls","PinMessages"] }) }) permissions.push({ type:discord.OverwriteType.Member, id:user.id, - allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory"], + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","EmbedLinks","PinMessages"], deny:[] }) + + const slowMode = option.get("opendiscord:slowmode-enabled").value ? option.get("opendiscord:slowmode-seconds").value : undefined //create channel const channel = await guild.channels.create({ @@ -111,7 +113,8 @@ export const registerActions = async () => { topic:channelTopic, parent:category, reason:"Ticket Created By "+user.displayName, - permissionOverwrites:permissions + permissionOverwrites:permissions, + rateLimitPerUser:slowMode }) await opendiscord.events.get("afterTicketChannelCreated").emit([option,channel,user]) diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index 7425a7d..d51423c 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -148,7 +148,7 @@ export class ODClientManager { if (!this.token) reject("Client doesn't have a token!") try { - this.client.once("ready",async () => { + this.client.once("clientReady",async () => { this.ready = true //set slashCommandManager & contextMenuManager to client applicationCommandManager From 82f2aeaa90365f891e415a0cd22de24c1b6a8c15 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 28 Aug 2025 09:00:07 +0200 Subject: [PATCH 51/78] Added autocomplete to panel, ticket & move cmds Ticket/panel options are now autocompleted instead of being a choice in the slash commands. This also results in the max amount of tickets limit to be increased to infinity. --- src/commands/autocomplete.ts | 32 +++++++++++++++++++++ src/core/api/defaults/responder.ts | 3 +- src/core/api/modules/responder.ts | 7 +++-- src/data/framework/commandLoader.ts | 43 ++--------------------------- src/index.ts | 2 +- 5 files changed, 42 insertions(+), 45 deletions(-) create mode 100644 src/commands/autocomplete.ts diff --git a/src/commands/autocomplete.ts b/src/commands/autocomplete.ts new file mode 100644 index 0000000..e7cac2a --- /dev/null +++ b/src/commands/autocomplete.ts @@ -0,0 +1,32 @@ +/////////////////////////////////////// +//AUTOCOMPLETE COMMAND UTILS +/////////////////////////////////////// +import {opendiscord, api, utilities} from "../index" +import * as discord from "discord.js" + +export const registerAutocompleteResponders = async () => { + //PANEL ID AUTOCOMPLETE + opendiscord.responders.autocomplete.add(new api.ODAutocompleteResponder("opendiscord:panel-id","panel","id")) + opendiscord.responders.autocomplete.get("opendiscord:panel-id").workers.add(new api.ODWorker("opendiscord:panel-id",0,async (instance,params,source,cancel) => { + //create panel choices + const panelChoices : {name:string, value:string}[] = [] + opendiscord.configs.get("opendiscord:panels").data.forEach((panel) => { + panelChoices.push({name:panel.name, value:panel.id}) + }) + + await instance.filteredAutocomplete(panelChoices) + })) + + //OPTION ID AUTOCOMPLETE + opendiscord.responders.autocomplete.add(new api.ODAutocompleteResponder("opendiscord:option-id",/ticket|move/,"id")) + opendiscord.responders.autocomplete.get("opendiscord:option-id").workers.add(new api.ODWorker("opendiscord:option-id",0,async (instance,params,source,cancel) => { + //create ticket choices + const ticketChoices : {name:string, value:string}[] = [] + opendiscord.configs.get("opendiscord:options").data.forEach((option) => { + if (option.type != "ticket") return + ticketChoices.push({name:option.name, value:option.id}) + }) + + instance.filteredAutocomplete(ticketChoices) + })) +} \ No newline at end of file diff --git a/src/core/api/defaults/responder.ts b/src/core/api/defaults/responder.ts index 95534bb..ffe8cb4 100644 --- a/src/core/api/defaults/responder.ts +++ b/src/core/api/defaults/responder.ts @@ -308,7 +308,8 @@ export class ODContextMenuResponder_Default { - if (!this.didReply){ + if (!this.didRespond){ process.emit("uncaughtException",new ODSystemError("Autocomplete responder instance failed to respond widthin 2.5sec!")) } },timeoutMs ?? 2500) @@ -1374,6 +1374,7 @@ export class ODAutocompleteResponderInstance { return {success:false} }else{ await this.interaction.respond(newChoices) + this.didRespond = true return {success:true} } }catch(err){ diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index b24d09b..4e10510 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -23,19 +23,6 @@ export const loadAllSlashCommands = async () => { if (!generalConfig.data.slashCommands) return - //create panel choices - const panelChoices : {name:string, value:string}[] = [] - opendiscord.configs.get("opendiscord:panels").data.forEach((panel) => { - panelChoices.push({name:panel.name, value:panel.id}) - }) - - //create ticket choices - const ticketChoices : {name:string, value:string}[] = [] - opendiscord.configs.get("opendiscord:options").data.forEach((option) => { - if (option.type != "ticket") return - ticketChoices.push({name:option.name, value:option.id}) - }) - const allowedCommands: string[] = [] for (const key in generalConfig.data.system.permissions){ if (generalConfig.data.system.permissions[key] != "none") allowedCommands.push(key) @@ -63,7 +50,7 @@ export const loadAllSlashCommands = async () => { description:lang.getTranslation("commands.panelId"), type:acot.String, required:true, - choices:panelChoices + autocomplete:true }, { name:"auto-update", @@ -72,14 +59,6 @@ export const loadAllSlashCommands = async () => { required:false } ] - },(current) => { - //check if this slash command needs to be updated - const idOption = current.options.find((opt) => opt.name == "id" && opt.type == acot.String) - if (!idOption || idOption.choices.length != panelChoices.length) return true - if (!panelChoices.every((panel) => { - return (idOption.choices.find((c) => c.value == panel.value && c.name == panel.name)) ? true : false - })) return true - return false })) //TICKET (when enabled) @@ -95,17 +74,9 @@ export const loadAllSlashCommands = async () => { description:lang.getTranslation("commands.ticketId"), type:acot.String, required:true, - choices:ticketChoices + autocomplete:true } ] - },(current) => { - //check if this slash command needs to be updated - const idOption = current.options.find((opt) => opt.name == "id" && opt.type == acot.String) - if (!idOption || idOption.choices.length != ticketChoices.length) return true - if (!ticketChoices.every((ticket) => { - return (idOption.choices.find((c) => c.value == ticket.value && c.name == ticket.name)) ? true : false - })) return true - return false })) //CLOSE @@ -267,7 +238,7 @@ export const loadAllSlashCommands = async () => { description:lang.getTranslation("commands.moveId"), type:acot.String, required:true, - choices:ticketChoices + autocomplete:true }, { name:"reason", @@ -276,14 +247,6 @@ export const loadAllSlashCommands = async () => { required:false } ] - },(current) => { - //check if this slash command needs to be updated - const idOption = current.options.find((opt) => opt.name == "id" && opt.type == acot.String) - if (!idOption || idOption.choices.length != ticketChoices.length) return true - if (!ticketChoices.every((ticket) => { - return (idOption.choices.find((c) => c.value == ticket.value && c.name == ticket.name)) ? true : false - })) return true - return false })) //RENAME diff --git a/src/index.ts b/src/index.ts index fe323ab..9963ce9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -721,7 +721,7 @@ const main = async () => { //load autocomplete responders opendiscord.log("Loading autocomplete responders...","system") if (opendiscord.defaults.getDefault("autocompleteRespondersLoading")){ - //TODO!! + await (await import("./commands/autocomplete.js")).registerAutocompleteResponders() } await opendiscord.events.get("onAutocompleteResponderLoad").emit([opendiscord.responders.autocomplete,opendiscord.responders,opendiscord.actions]) await opendiscord.events.get("afterAutocompleteRespondersLoaded").emit([opendiscord.responders.autocomplete,opendiscord.responders,opendiscord.actions]) From 706006e319fb61491c7766200af576e838a1dd50 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:20:02 +0200 Subject: [PATCH 52/78] Added 9 new statistics in the /stats command Ticket volume, Average tickets/user, System uptime, Current tickets, Ticket age, Message/Embed/File/Component amount. --- src/builders/embeds.ts | 7 +- src/core/api/defaults/stat.ts | 116 ++++++++++++++++++++++++++++- src/core/api/main.ts | 2 +- src/core/api/modules/stat.ts | 20 +++++ src/data/framework/statLoader.ts | 122 ++++++++++++++++++++++--------- 5 files changed, 226 insertions(+), 41 deletions(-) diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index 1265655..050c670 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -412,14 +412,17 @@ const statsEmbeds = () => { const scope = opendiscord.stats.get("opendiscord:ticket") const participantsScope = opendiscord.stats.get("opendiscord:participants") - if (!scope || !participantsScope) return + const messagesScope = opendiscord.stats.get("opendiscord:messages") + if (!scope || !participantsScope || !messagesScope) return const data = await scope.render(scopeData.id.value,guild,channel,user) const participantsData = await participantsScope.render(scopeData.id.value,guild,channel,user) + const messagesData = await messagesScope.render(scopeData.id.value,guild,channel,user) instance.setColor(generalConfig.data.mainColor) instance.setTitle(scope.name) instance.setDescription(data) - instance.addFields({name:participantsScope.name,value:participantsData,inline:false}) + instance.addFields({name:participantsScope.name,value:participantsData,inline:true}) + instance.addFields({name:messagesScope.name,value:messagesData,inline:true}) }) ) diff --git a/src/core/api/defaults/stat.ts b/src/core/api/defaults/stat.ts index dd4c016..47e306c 100644 --- a/src/core/api/defaults/stat.ts +++ b/src/core/api/defaults/stat.ts @@ -13,7 +13,8 @@ export interface ODStatsManagerIds_Default { "opendiscord:system":ODStatGlobalScope_DefaultSystem, "opendiscord:user":ODStatScope_DefaultUser, "opendiscord:ticket":ODStatScope_DefaultTicket, - "opendiscord:participants":ODStatScope_DefaultParticipants + "opendiscord:participants":ODStatScope_DefaultParticipants, + "opendiscord:messages":ODStatScope_DefaultMessages, } /**## ODStatsManager_Default `default_class` @@ -60,7 +61,9 @@ export interface ODStatGlobalScopeIds_DefaultGlobal { "opendiscord:tickets-pinned":ODBasicStat, "opendiscord:tickets-moved":ODBasicStat, "opendiscord:users-blacklisted":ODBasicStat, - "opendiscord:transcripts-created":ODBasicStat + "opendiscord:transcripts-created":ODBasicStat, + "opendiscord:ticket-volume":ODDynamicStat, + "opendiscord:average-tickets":ODDynamicStat, } /**## ODStatGlobalScope_DefaultGlobal `default_class` @@ -98,6 +101,13 @@ export class ODStatGlobalScope_DefaultGlobal extends ODStatGlobalScope { return super.getStat(id) } + getAllStats(id:StatsId): Promise<{id:string,value:ODValidStatValue}[]> + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> + + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + return super.getAllStats(id) + } + setStat(id:StatsId, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise @@ -119,6 +129,7 @@ export class ODStatGlobalScope_DefaultGlobal extends ODStatGlobalScope { */ export interface ODStatGlobalScopeIds_DefaultSystem { "opendiscord:startup-date":ODDynamicStat, + "opendiscord:system-uptime":ODDynamicStat, "opendiscord:version":ODDynamicStat } @@ -157,6 +168,13 @@ export class ODStatGlobalScope_DefaultSystem extends ODStatGlobalScope { return super.getStat(id) } + getAllStats(id:StatsId): Promise<{id:string,value:ODValidStatValue}[]> + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> + + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + return super.getAllStats(id) + } + setStat(id:StatsId, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise @@ -187,7 +205,8 @@ export interface ODStatScopeIds_DefaultUser { "opendiscord:tickets-pinned":ODBasicStat, "opendiscord:tickets-moved":ODBasicStat, "opendiscord:users-blacklisted":ODBasicStat, - "opendiscord:transcripts-created":ODBasicStat + "opendiscord:transcripts-created":ODBasicStat, + "opendiscord:current-tickets":ODDynamicStat, } /**## ODStatScope_DefaultUser `default_class` @@ -225,6 +244,13 @@ export class ODStatScope_DefaultUser extends ODStatScope { return super.getStat(id,scopeId) } + getAllStats(id:StatsId): Promise<{id:string,value:ODValidStatValue}[]> + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> + + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + return super.getAllStats(id) + } + setStat(id:StatsId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise @@ -250,7 +276,10 @@ export interface ODStatScopeIds_DefaultTicket { "opendiscord:claimed":ODDynamicStat, "opendiscord:pinned":ODDynamicStat, "opendiscord:creation-date":ODDynamicStat, - "opendiscord:creator":ODDynamicStat + "opendiscord:creator":ODDynamicStat, + "opendiscord:ticket-age":ODDynamicStat, + "opendiscord:response-time":ODDynamicStat, + "opendiscord:resolution-time":ODDynamicStat, } /**## ODStatScope_DefaultTicket `default_class` @@ -288,6 +317,13 @@ export class ODStatScope_DefaultTicket extends ODStatScope { return super.getStat(id,scopeId) } + getAllStats(id:StatsId): Promise<{id:string,value:ODValidStatValue}[]> + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> + + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + return super.getAllStats(id) + } + setStat(id:StatsId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise @@ -346,6 +382,13 @@ export class ODStatScope_DefaultParticipants extends ODStatScope { return super.getStat(id,scopeId) } + getAllStats(id:StatsId): Promise<{id:string,value:ODValidStatValue}[]> + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> + + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + return super.getAllStats(id) + } + setStat(id:StatsId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise @@ -356,6 +399,71 @@ export class ODStatScope_DefaultParticipants extends ODStatScope { resetStat(id:ODValidId, scopeId:string): Promise resetStat(id:ODValidId, scopeId:string): Promise + resetStat(id:ODValidId, scopeId:string): Promise { + return super.resetStat(id,scopeId) + } +} + +/**## ODStatScopeIds_DefaultMessages `type` + * This interface is a list of ids available in the `ODStatScope_DefaultMessages` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODStatScopeIds_DefaultMessages { + "opendiscord:count":ODDynamicStat +} + +/**## ODStatScope_DefaultMessages `default_class` + * This is a special class that adds type definitions & typescript to the ODStatsManager class. + * It doesn't add any extra features! + * + * This default class is made for the `opendiscord:participants` category in `opendiscord.stats`! + */ +export class ODStatScope_DefaultMessages extends ODStatScope { + get(id:StatsId): ODStatScopeIds_DefaultMessages[StatsId] + get(id:ODValidId): ODStat|null + + get(id:ODValidId): ODStat|null { + return super.get(id) + } + + remove(id:StatsId): ODStatScopeIds_DefaultMessages[StatsId] + remove(id:ODValidId): ODStat|null + + remove(id:ODValidId): ODStat|null { + return super.remove(id) + } + + exists(id:keyof ODStatScopeIds_DefaultMessages): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } + + getStat(id:StatsId, scopeId:string): Promise + getStat(id:ODValidId, scopeId:string): Promise + + getStat(id:ODValidId, scopeId:string): Promise { + return super.getStat(id,scopeId) + } + + getAllStats(id:StatsId): Promise<{id:string,value:ODValidStatValue}[]> + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> + + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + return super.getAllStats(id) + } + + setStat(id:StatsId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise + + setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise { + return super.setStat(id,scopeId,value,mode) + } + + resetStat(id:ODValidId, scopeId:string): Promise + resetStat(id:ODValidId, scopeId:string): Promise + resetStat(id:ODValidId, scopeId:string): Promise { return super.resetStat(id,scopeId) } diff --git a/src/core/api/main.ts b/src/core/api/main.ts index ddb8a25..cec0b11 100644 --- a/src/core/api/main.ts +++ b/src/core/api/main.ts @@ -132,7 +132,7 @@ export class ODMain { this.versions = new ODVersionManager_Default() this.versions.add(ODVersion.fromString("opendiscord:version","v4.1.0")) this.versions.add(ODVersion.fromString("opendiscord:api","v1.0.0")) - this.versions.add(ODVersion.fromString("opendiscord:transcripts","v2.0.0")) + this.versions.add(ODVersion.fromString("opendiscord:transcripts","v2.1.0")) this.versions.add(ODVersion.fromString("opendiscord:livestatus","v2.0.0")) this.debugfile = new ODDebugFileManager("./","otdebug.txt",5000,this.versions.get("opendiscord:version")) diff --git a/src/core/api/modules/stat.ts b/src/core/api/modules/stat.ts index 63ebcca..dccc1e6 100644 --- a/src/core/api/modules/stat.ts +++ b/src/core/api/modules/stat.ts @@ -140,6 +140,23 @@ export class ODStatScope extends ODManager { //return null on error return null } + /**Get the value of a statistic for all `scopeId`'s. The `scopeId` is the unique id of the user, channel, role, etc that the stats are related to. */ + async getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + if (!this.database) return [] + const newId = new ODId(id) + const data = await this.database.getCategory(this.id.value+"_"+newId.value) ?? [] + const output: {id:string,value:ODValidStatValue}[] = [] + + for (const stat of data){ + if (typeof stat.value == "string" || typeof stat.value == "boolean" || typeof stat.value == "number"){ + //return value received from database + output.push({id:stat.key,value:stat.value}) + } + } + + //return null on error + return output + } /**Set, increase or decrease the value of a statistic. The `scopeId` is the unique id of the user, channel, role, etc that the stats are related to. */ async setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise { if (!this.database) return false @@ -213,6 +230,9 @@ export class ODStatGlobalScope extends ODStatScope { getStat(id:ODValidId): Promise { return super.getStat(id,"GLOBAL") } + getAllStats(id:ODValidId): Promise<{id:string,value:ODValidStatValue}[]> { + return super.getAllStats(id) + } setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): Promise { return super.setStat(id,"GLOBAL",value,mode) } diff --git a/src/data/framework/statLoader.ts b/src/data/framework/statLoader.ts index 7f56020..50de840 100644 --- a/src/data/framework/statLoader.ts +++ b/src/data/framework/statLoader.ts @@ -10,6 +10,7 @@ export const loadAllStatScopes = async () => { stats.add(new api.ODStatScope("opendiscord:user",utilities.emojiTitle("📊",lang.getTranslation("stats.scopes.user")))) stats.add(new api.ODStatScope("opendiscord:ticket",utilities.emojiTitle("📊",lang.getTranslation("stats.scopes.ticket")))) stats.add(new api.ODStatScope("opendiscord:participants",utilities.emojiTitle("👥",lang.getTranslation("stats.scopes.participants")))) + stats.add(new api.ODStatScope("opendiscord:messages",utilities.emojiTitle("💬","Messages"))) //TODO TRANSLATION!!! } export const loadAllStats = async () => { @@ -18,24 +19,36 @@ export const loadAllStats = async () => { const global = stats.get("opendiscord:global") if (global){ - global.add(new api.ODBasicStat("opendiscord:tickets-created",10,lang.getTranslation("stats.properties.ticketsCreated"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-closed",9,lang.getTranslation("stats.properties.ticketsClosed"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-deleted",8,lang.getTranslation("stats.properties.ticketsDeleted"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-reopened",7,lang.getTranslation("stats.properties.ticketsReopened"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-autoclosed",6,lang.getTranslation("stats.properties.ticketsAutoclosed"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-autodeleted",5,"Tickets Autodeleted",0)) //TODO TRANSLATION!!! - global.add(new api.ODBasicStat("opendiscord:tickets-claimed",4,lang.getTranslation("stats.properties.ticketsClaimed"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-pinned",3,lang.getTranslation("stats.properties.ticketsPinned"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-moved",2,lang.getTranslation("stats.properties.ticketsMoved"),0)) - global.add(new api.ODBasicStat("opendiscord:users-blacklisted",1,lang.getTranslation("stats.properties.usersBlacklisted"),0)) - global.add(new api.ODBasicStat("opendiscord:transcripts-created",0,lang.getTranslation("stats.properties.transcriptsCreated"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-created",12,lang.getTranslation("stats.properties.ticketsCreated"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-closed",11,lang.getTranslation("stats.properties.ticketsClosed"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-deleted",10,lang.getTranslation("stats.properties.ticketsDeleted"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-reopened",9,lang.getTranslation("stats.properties.ticketsReopened"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-autoclosed",8,lang.getTranslation("stats.properties.ticketsAutoclosed"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-autodeleted",7,"Tickets Autodeleted",0)) //TODO TRANSLATION!!! + global.add(new api.ODBasicStat("opendiscord:tickets-claimed",6,lang.getTranslation("stats.properties.ticketsClaimed"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-pinned",5,lang.getTranslation("stats.properties.ticketsPinned"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-moved",4,lang.getTranslation("stats.properties.ticketsMoved"),0)) + global.add(new api.ODBasicStat("opendiscord:users-blacklisted",3,lang.getTranslation("stats.properties.usersBlacklisted"),0)) + global.add(new api.ODBasicStat("opendiscord:transcripts-created",2,lang.getTranslation("stats.properties.transcriptsCreated"),0)) + global.add(new api.ODDynamicStat("opendiscord:ticket-volume",1,() => { + return "Ticket Volume: `"+opendiscord.tickets.getLength()+"`" //TODO TRANSLATION!!! + })) + global.add(new api.ODDynamicStat("opendiscord:average-tickets",0,async () => { + const userTicketsCreated = await opendiscord.stats.get("opendiscord:user").getAllStats("opendiscord:tickets-created") + const average = userTicketsCreated.map((s) => s.value as number).filter((t) => t > 0).reduce((prev,curr) => prev+curr,0)/userTicketsCreated.length + const roundedAverage = Math.round(average*1000)/1000 + return "Average Tickets/User: `"+roundedAverage+"`" //TODO TRANSLATION!!! + })) } const system = stats.get("opendiscord:system") if (system){ - system.add(new api.ODDynamicStat("opendiscord:startup-date",1,() => { + system.add(new api.ODDynamicStat("opendiscord:startup-date",2,() => { return lang.getTranslation("params.uppercase.startupDate")+": "+discord.time(opendiscord.processStartupDate,"f") })) + system.add(new api.ODDynamicStat("opendiscord:system-uptime",1,() => { + return "System Uptime: "+discord.time(opendiscord.processStartupDate,"R") //TODO TRANSLATION!!! + })) system.add(new api.ODDynamicStat("opendiscord:version",0,() => { return lang.getTranslation("params.uppercase.version")+": `"+opendiscord.versions.get("opendiscord:version").toString()+"`" })) @@ -47,30 +60,29 @@ export const loadAllStats = async () => { return lang.getTranslation("params.uppercase.name")+": "+discord.userMention(scopeId) })) user.add(new api.ODDynamicStat("opendiscord:role",10,async (scopeId,guild,channel,user) => { - try{ - const scopeMember = await guild.members.fetch(scopeId) - if (!scopeMember) return "" + const scopeMember = await opendiscord.client.fetchGuildMember(guild,scopeId) + if (!scopeMember) return "" - const permissions = await opendiscord.permissions.getPermissions(scopeMember.user,channel,guild) - if (permissions.type == "developer") return lang.getTranslation("params.uppercase.role")+": 💻 `Developer`" //TODO TRANSLATION!!! - if (permissions.type == "owner") return lang.getTranslation("params.uppercase.role")+": 👑 `Server Owner`" //TODO TRANSLATION!!! - if (permissions.type == "admin") return lang.getTranslation("params.uppercase.role")+": 💼 `Server Admin`" //TODO TRANSLATION!!! - if (permissions.type == "moderator") return lang.getTranslation("params.uppercase.role")+": 🚔 `Moderator Team`" //TODO TRANSLATION!!! - if (permissions.type == "support") return lang.getTranslation("params.uppercase.role")+": 💬 `Support Team`" //TODO TRANSLATION!!! - else return lang.getTranslation("params.uppercase.role")+": 👤 `Member`" //TODO TRANSLATION!!! - }catch{ - return "" - } + const permissions = await opendiscord.permissions.getPermissions(scopeMember.user,channel,guild) + if (permissions.type == "developer") return lang.getTranslation("params.uppercase.role")+": 💻 `Developer`" //TODO TRANSLATION!!! + if (permissions.type == "owner") return lang.getTranslation("params.uppercase.role")+": 👑 `Server Owner`" //TODO TRANSLATION!!! + if (permissions.type == "admin") return lang.getTranslation("params.uppercase.role")+": 💼 `Server Admin`" //TODO TRANSLATION!!! + if (permissions.type == "moderator") return lang.getTranslation("params.uppercase.role")+": 🚔 `Moderator Team`" //TODO TRANSLATION!!! + if (permissions.type == "support") return lang.getTranslation("params.uppercase.role")+": 💬 `Support Team`" //TODO TRANSLATION!!! + else return lang.getTranslation("params.uppercase.role")+": 👤 `Member`" //TODO TRANSLATION!!! + })) + user.add(new api.ODBasicStat("opendiscord:tickets-created",9,lang.getTranslation("stats.properties.ticketsCreated"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-closed",8,lang.getTranslation("stats.properties.ticketsClosed"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-deleted",7,lang.getTranslation("stats.properties.ticketsDeleted"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-reopened",6,lang.getTranslation("stats.properties.ticketsReopened"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-claimed",5,lang.getTranslation("stats.properties.ticketsClaimed"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-pinned",4,lang.getTranslation("stats.properties.ticketsPinned"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-moved",3,lang.getTranslation("stats.properties.ticketsMoved"),0)) + user.add(new api.ODBasicStat("opendiscord:users-blacklisted",2,lang.getTranslation("stats.properties.usersBlacklisted"),0)) + user.add(new api.ODBasicStat("opendiscord:transcripts-created",1,lang.getTranslation("stats.properties.transcriptsCreated"),0)) + user.add(new api.ODDynamicStat("opendiscord:current-tickets",0,async (scopeId,guild,channel,user) => { + return "Current Tickets: `"+opendiscord.tickets.getFiltered((t) => t.get("opendiscord:opened-by").value === scopeId).length+"`" //TODO TRANSLATION!!! })) - user.add(new api.ODBasicStat("opendiscord:tickets-created",8,lang.getTranslation("stats.properties.ticketsCreated"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-closed",7,lang.getTranslation("stats.properties.ticketsClosed"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-deleted",6,lang.getTranslation("stats.properties.ticketsDeleted"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-reopened",5,lang.getTranslation("stats.properties.ticketsReopened"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-claimed",4,lang.getTranslation("stats.properties.ticketsClaimed"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-pinned",3,lang.getTranslation("stats.properties.ticketsPinned"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-moved",2,lang.getTranslation("stats.properties.ticketsMoved"),0)) - user.add(new api.ODBasicStat("opendiscord:users-blacklisted",1,lang.getTranslation("stats.properties.usersBlacklisted"),0)) - user.add(new api.ODBasicStat("opendiscord:transcripts-created",0,lang.getTranslation("stats.properties.transcriptsCreated"),0)) } const ticket = stats.get("opendiscord:ticket") @@ -114,6 +126,15 @@ export const loadAllStats = async () => { const creator = ticket.get("opendiscord:opened-by").value return lang.getTranslation("params.uppercase.creator")+": "+ (creator ? discord.userMention(creator) : "`unknown`") })) + ticket.add(new api.ODDynamicStat("opendiscord:ticket-age",1,async (scopeId,guild,channel,user) => { + const ticket = opendiscord.tickets.get(scopeId) + if (!ticket) return "" + + const rawDate = ticket.get("opendiscord:opened-on").value ?? new Date().getTime() + return "Ticket Age: "+discord.time(new Date(rawDate),"R") //TODO TRANSLATION!!! + })) + //TODO: opendiscord:response-time //TODO TRANSLATION!!! + //TODO: opendiscord:resolution-time //TODO TRANSLATION!!! } const participants = stats.get("opendiscord:participants") @@ -130,4 +151,37 @@ export const loadAllStats = async () => { })) } + + const messages = stats.get("opendiscord:messages") + if (messages){ + messages.add(new api.ODDynamicStat("opendiscord:count",0,async (scopeId,guild,channel,user) => { + const ticket = opendiscord.tickets.get(scopeId) + if (!ticket) return "" + + const messages = await opendiscord.transcripts.collector.collectAllMessages(ticket,{bots:true,client:true,users:true}) + if (!messages) return "" + const parsedMessages = await opendiscord.transcripts.collector.convertMessagesToTranscriptData(messages) + + let messageCount = parsedMessages.length + let embedCount = 0 + let fileCount = 0 + let componentCount = 0 + + for (const msg of parsedMessages){ + embedCount += msg.embeds.length + fileCount += msg.files.length + for (const row of msg.components){ + componentCount += row.components.length + } + } + + return [ + "Messages: `"+messageCount+"`", //TODO TRANSLATION!!! + "Embeds: `"+embedCount+"`", //TODO TRANSLATION!!! + "Files: `"+fileCount+"`", //TODO TRANSLATION!!! + "Components: `"+componentCount+"`" //TODO TRANSLATION!!! + ].join("\n") + })) + + } } \ No newline at end of file From 02d37dd177dc765e90a456b8f6b6d43c2e5da206 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:25:18 +0200 Subject: [PATCH 53/78] (API) Implemented opendiscord:reopened in database --- src/actions/closeTicket.ts | 9 +++++++-- src/actions/reopenTicket.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/actions/closeTicket.ts b/src/actions/closeTicket.ts index a37bb7d..4e79f1f 100644 --- a/src/actions/closeTicket.ts +++ b/src/actions/closeTicket.ts @@ -17,10 +17,15 @@ export const registerActions = async () => { //update ticket ticket.get("opendiscord:closed").value = true - if (source == "autoclose") ticket.get("opendiscord:autoclosed").value = true - ticket.get("opendiscord:open").value = false ticket.get("opendiscord:closed-by").value = user.id ticket.get("opendiscord:closed-on").value = new Date().getTime() + + ticket.get("opendiscord:reopened").value = false + ticket.get("opendiscord:reopened-by").value = null + ticket.get("opendiscord:reopened-on").value = null + + if (source == "autoclose") ticket.get("opendiscord:autoclosed").value = true + ticket.get("opendiscord:open").value = false ticket.get("opendiscord:busy").value = true //update stats diff --git a/src/actions/reopenTicket.ts b/src/actions/reopenTicket.ts index 84e01fb..ecfe5e9 100644 --- a/src/actions/reopenTicket.ts +++ b/src/actions/reopenTicket.ts @@ -16,11 +16,16 @@ export const registerActions = async () => { await opendiscord.events.get("onTicketReopen").emit([ticket,user,channel,reason]) //update ticket + ticket.get("opendiscord:reopened").value = true + ticket.get("opendiscord:reopened-by").value = user.id + ticket.get("opendiscord:reopened-on").value = new Date().getTime() + ticket.get("opendiscord:closed").value = false - ticket.get("opendiscord:open").value = true - ticket.get("opendiscord:autoclosed").value = false ticket.get("opendiscord:closed-by").value = null ticket.get("opendiscord:closed-on").value = null + + ticket.get("opendiscord:autoclosed").value = false + ticket.get("opendiscord:open").value = true ticket.get("opendiscord:busy").value = true //update stats From b07252151b319d1ce55efc55be3bcf666c619645 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:46:37 +0200 Subject: [PATCH 54/78] #181 : Fixed async permission registration issues --- src/data/framework/permissionLoader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/framework/permissionLoader.ts b/src/data/framework/permissionLoader.ts index 0b33ae4..7601113 100644 --- a/src/data/framework/permissionLoader.ts +++ b/src/data/framework/permissionLoader.ts @@ -20,14 +20,14 @@ export const loadAllPermissions = async () => { opendiscord.permissions.add(new api.ODPermission("opendiscord:owner-"+owner.id,"global-user","owner",owner)) //GLOBAL ADMINS - generalConfig.data.globalAdmins.forEach(async (admin) => { - const role = await mainServer.roles.fetch(admin) + for (const admin of generalConfig.data.globalAdmins){ + const role = await opendiscord.client.fetchGuildRole(mainServer,admin) if (!role) return opendiscord.log("Unable to register permission for global admin!","error",[ {key:"roleid",value:admin} ]) opendiscord.permissions.add(new api.ODPermission("opendiscord:global-admin-"+admin,"global-role","admin",role)) - }) + } //TICKET ADMINS await opendiscord.tickets.loopAll(async (ticket) => { @@ -78,8 +78,8 @@ export const removeTicketPermissions = async (ticket:api.ODTicket) => { const admins = ticket.option.exists("opendiscord:admins") ? ticket.option.get("opendiscord:admins").value : [] const readAdmins = ticket.option.exists("opendiscord:admins-readonly") ? ticket.option.get("opendiscord:admins-readonly").value : [] - admins.concat(readAdmins).forEach(async (admin) => { + for (const admin of admins.concat(readAdmins)){ if (!opendiscord.permissions.exists("opendiscord:ticket-admin_"+ticket.id.value+"_"+admin)) return opendiscord.permissions.remove("opendiscord:ticket-admin_"+ticket.id.value+"_"+admin) - }) + } } \ No newline at end of file From 21ce2921a1c9b109f38f80761a7fc2656a1082a2 Mon Sep 17 00:00:00 2001 From: Steven DUBOIS Date: Wed, 10 Sep 2025 18:32:13 +0200 Subject: [PATCH 55/78] Add user-nickname option for channel suffixe --- src/actions/createTicket.ts | 4 +-- src/core/api/defaults/config.ts | 2 +- src/core/api/openticket/option.ts | 38 ++++++++++++++++++++--------- src/data/framework/checkerLoader.ts | 4 +-- src/data/openticket/optionLoader.ts | 1 + 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index e877a90..1d05f8f 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -20,7 +20,7 @@ export const registerActions = async () => { const channelCategory = option.get("opendiscord:channel-category").value const channelBackupCategory = option.get("opendiscord:channel-category-backup").value const channelTopic = option.get("opendiscord:channel-topic").value - const channelSuffix = await opendiscord.options.suffix.getSuffixFromOption(option,user) + const channelSuffix = await opendiscord.options.suffix.getSuffixFromOption(option,user,guild) const channelName = channelPrefix+channelSuffix //handle category @@ -237,4 +237,4 @@ export const registerActions = async () => { ]) }) ]) -} \ No newline at end of file +} diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index 7a50dad..e39b98e 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -411,7 +411,7 @@ export interface ODJsonConfig_DefaultOptionTicketChannelType { /**The prefix used in the name of this ticket channel. */ prefix:string, /**The type of suffix used in the name of this ticket channel. */ - suffix:"user-name"|"user-id"|"random-number"|"random-hex"|"counter-dynamic"|"counter-fixed", + suffix:"user-name"|"user-nickname"|"user-id"|"random-number"|"random-hex"|"counter-dynamic"|"counter-fixed", /**An optional discord category id to create this ticket in. */ category:string, /**An optional discord category id to move this ticket to when closed. */ diff --git a/src/core/api/openticket/option.ts b/src/core/api/openticket/option.ts index 6741616..9bd6ad9 100644 --- a/src/core/api/openticket/option.ts +++ b/src/core/api/openticket/option.ts @@ -151,7 +151,7 @@ export interface ODTicketOptionIds { "opendiscord:questions":ODOptionData, "opendiscord:channel-prefix":ODOptionData, - "opendiscord:channel-suffix":ODOptionData<"user-name"|"user-id"|"random-number"|"random-hex"|"counter-dynamic"|"counter-fixed">, + "opendiscord:channel-suffix":ODOptionData<"user-name"|"user-nickname"|"user-id"|"random-number"|"random-hex"|"counter-dynamic"|"counter-fixed">, "opendiscord:channel-category":ODOptionData, "opendiscord:channel-category-closed":ODOptionData, "opendiscord:channel-category-backup":ODOptionData, @@ -353,10 +353,11 @@ export class ODOptionSuffixManager extends ODManager { } /**Instantly get the suffix from an `ODTicketOption`. */ - async getSuffixFromOption(option:ODTicketOption,user:discord.User): Promise { + async getSuffixFromOption(option:ODTicketOption,user:discord.User, guild: discord.Guild): Promise { const suffix = this.getAll().find((suffix) => suffix.option.id.value == option.id.value) if (!suffix) return null - return await suffix.getSuffix(user) + const member = await guild.members.fetch(user.id); + return await suffix.getSuffix(member) } } @@ -377,7 +378,7 @@ export class ODOptionSuffix extends ODManagerData { } /**Get the suffix for a new ticket. */ - async getSuffix(user:discord.User): Promise { + async getSuffix(member:discord.GuildMember): Promise { throw new ODSystemError("Tried to use an unimplemented ODOptionSuffix!") } } @@ -390,8 +391,21 @@ export class ODOptionSuffix extends ODManagerData { * Use `getSuffix()` to get the new suffix! */ export class ODOptionUserNameSuffix extends ODOptionSuffix { - async getSuffix(user:discord.User): Promise { - return user.username + async getSuffix(member:discord.GuildMember): Promise { + return member.user.username + } +} + +/**## ODOptionUserNicknameSuffix `class` + * This is an Open Ticket user-nickname option suffix. + * + * This class can generate a user-nickname suffix for a discord channel name from a specific option. + * + * Use `getSuffix()` to get the new suffix! + */ +export class ODOptionUserNicknameSuffix extends ODOptionSuffix { + async getSuffix(member:discord.GuildMember): Promise { + return member.displayName } } @@ -403,8 +417,8 @@ export class ODOptionUserNameSuffix extends ODOptionSuffix { * Use `getSuffix()` to get the new suffix! */ export class ODOptionUserIdSuffix extends ODOptionSuffix { - async getSuffix(user:discord.User): Promise { - return user.id + async getSuffix(member:discord.GuildMember): Promise { + return member.id } } @@ -429,7 +443,7 @@ export class ODOptionCounterDynamicSuffix extends ODOptionSuffix { async #init(){ if (!await this.database.exists("opendiscord:option-suffix-counter",this.option.id.value)) await this.database.set("opendiscord:option-suffix-counter",this.option.id.value,0) } - async getSuffix(user:discord.User): Promise { + async getSuffix(member:discord.GuildMember): Promise { const rawCurrentValue = await this.database.get("opendiscord:option-suffix-counter",this.option.id.value) const currentValue = (typeof rawCurrentValue != "number") ? 0 : rawCurrentValue const newValue = currentValue+1 @@ -459,7 +473,7 @@ export class ODOptionCounterFixedSuffix extends ODOptionSuffix { async #init(){ if (!await this.database.exists("opendiscord:option-suffix-counter",this.option.id.value)) await this.database.set("opendiscord:option-suffix-counter",this.option.id.value,0) } - async getSuffix(user:discord.User): Promise { + async getSuffix(member:discord.GuildMember): Promise { const rawCurrentValue = await this.database.get("opendiscord:option-suffix-counter",this.option.id.value) const currentValue = (typeof rawCurrentValue != "number") ? 0 : rawCurrentValue const newValue = (currentValue >= 9999) ? 0 : currentValue+1 @@ -504,7 +518,7 @@ export class ODOptionRandomNumberSuffix extends ODOptionSuffix { if (history.includes(number)) return this.#generateUniqueValue(history) else return number } - async getSuffix(user:discord.User): Promise { + async getSuffix(member:discord.GuildMember): Promise { const rawCurrentValues = await this.database.get("opendiscord:option-suffix-history",this.option.id.value) const currentValues = ((Array.isArray(rawCurrentValues)) ? rawCurrentValues : []) as string[] const newValue = this.#generateUniqueValue(currentValues) @@ -542,7 +556,7 @@ export class ODOptionRandomHexSuffix extends ODOptionSuffix { if (history.includes(hex)) return this.#generateUniqueValue(history) else return hex } - async getSuffix(user:discord.User): Promise { + async getSuffix(member:discord.GuildMember): Promise { const rawCurrentValues = await this.database.get("opendiscord:option-suffix-history",this.option.id.value) const currentValues = ((Array.isArray(rawCurrentValues)) ? rawCurrentValues : []) as string[] const newValue = this.#generateUniqueValue(currentValues) diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index b7eee42..ef7a3d7 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -402,7 +402,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc //TICKET CHANNEL {key:"channel",checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{cliInitSkipKeys:["backupCategory","claimedCategory"],children:[ {key:"prefix",checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-prefix",{maxLength:25,regex:/^[^\s]*$/,cliDisplayName:"Prefix",cliDisplayDescription:"The prefix of the name of the ticket channel. (e.g. 'question-')"})}, - {key:"suffix",checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-suffix",{choices:["user-name","user-id","random-number","random-hex","counter-dynamic","counter-fixed"],cliDisplayName:"Suffix",cliDisplayDescription:"The suffix mode to use. The number/text will be appended after the prefix."})}, + {key:"suffix",checker:new api.ODCheckerStringStructure("opendiscord:ticket-channel-suffix",{choices:["user-name","user-nickname","user-id","random-number","random-hex","counter-dynamic","counter-fixed"],cliDisplayName:"Suffix",cliDisplayDescription:"The suffix mode to use. The number/text will be appended after the prefix."})}, {key:"category",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-category","category",true,[],{cliDisplayName:"Category",cliDisplayDescription:"The category the ticket will be created in. Leave empty for no category."})}, {key:"closedCategory",checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-closed-category","category",true,[],{cliDisplayName:"Closed Category",cliDisplayDescription:"An additional category where the ticket will be moved to when closed."})}, @@ -700,4 +700,4 @@ export const defaultDropdownOptionsFunction = (manager:api.ODCheckerManager, fun }) return {valid:(final.length < 1),messages:final} -} \ No newline at end of file +} diff --git a/src/data/openticket/optionLoader.ts b/src/data/openticket/optionLoader.ts index 0124902..38dd5de 100644 --- a/src/data/openticket/optionLoader.ts +++ b/src/data/openticket/optionLoader.ts @@ -139,6 +139,7 @@ export const loadTicketOptionSuffix = (option:api.ODTicketOption): api.ODOptionS const mode = option.get("opendiscord:channel-suffix").value const globalDatabase = opendiscord.databases.get("opendiscord:global") if (mode == "user-name") return new api.ODOptionUserNameSuffix(option.id.value,option) + else if (mode == "user-nickname") return new api.ODOptionUserNicknameSuffix(option.id.value,option) else if (mode == "random-number") return new api.ODOptionRandomNumberSuffix(option.id.value,option,globalDatabase) else if (mode == "random-hex") return new api.ODOptionRandomHexSuffix(option.id.value,option,globalDatabase) else if (mode == "counter-fixed") return new api.ODOptionCounterFixedSuffix(option.id.value,option,globalDatabase) From 415330be2190e31ea0b89555625e071133f89870 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:04:11 +0200 Subject: [PATCH 56/78] Added priority system (Part 1, Incomplete) --- src/builders/embeds.ts | 32 +++++++- src/core/api/api.ts | 3 +- src/core/api/defaults/builder.ts | 5 ++ src/core/api/defaults/client.ts | 5 +- src/core/api/defaults/event.ts | 5 ++ src/core/api/main.ts | 4 + src/core/api/modules/defaults.ts | 5 ++ src/core/api/openticket/priority.ts | 101 ++++++++++++++++++++++++++ src/data/framework/commandLoader.ts | 67 +++++++++++++++++ src/data/framework/eventLoader.ts | 4 + src/data/framework/helpMenuLoader.ts | 8 ++ src/data/openticket/priorityLoader.ts | 11 +++ src/index.ts | 8 ++ 13 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 src/core/api/openticket/priority.ts create mode 100644 src/data/openticket/priorityLoader.ts diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index 050c670..a145298 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -20,6 +20,7 @@ export const registerAllEmbeds = async () => { roleEmbeds() clearEmbeds() autoEmbeds() + extraEmbeds() } /**Utility function to get the translated "method" from the source. Mostly used in error embeds. */ @@ -1166,8 +1167,6 @@ const roleEmbeds = () => { ) } -export default roleEmbeds - const clearEmbeds = () => { //CLEAR VERIFY MESSAGE embeds.add(new api.ODEmbed("opendiscord:clear-verify-message")) @@ -1311,4 +1310,33 @@ const autoEmbeds = () => { if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) }) ) +} + +const extraEmbeds = () => { + //PRIORITY SET + embeds.add(new api.ODEmbed("opendiscord:priority-set")) + embeds.get("opendiscord:priority-set").workers.add( + new api.ODWorker("opendiscord:priority-set",0,async (instance,params,source) => { + const {user,priority,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🚨","Priority Changed")) //TODO TRANSLATION!!! + instance.setDescription("The ticket priority has been changed to **"+priority.renderDisplayName()+"** by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) + + //PRIORITY GET + embeds.add(new api.ODEmbed("opendiscord:priority-get")) + embeds.get("opendiscord:priority-get").workers.add( + new api.ODWorker("opendiscord:priority-get",0,async (instance,params,source) => { + const {user,priority} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🚨","Priority Changed")) //TODO TRANSLATION!!! + instance.setDescription("The current priority of this ticket is **"+priority.renderDisplayName()+"**!") //TODO TRANSLATION!!! + }) + ) } \ No newline at end of file diff --git a/src/core/api/api.ts b/src/core/api/api.ts index 10e7906..7286d8f 100644 --- a/src/core/api/api.ts +++ b/src/core/api/api.ts @@ -60,4 +60,5 @@ export * from "./openticket/panel" export * from "./openticket/ticket" export * from "./openticket/blacklist" export * from "./openticket/transcript" -export * from "./openticket/role" \ No newline at end of file +export * from "./openticket/role" +export * from "./openticket/priority" \ No newline at end of file diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index 34bf3d9..7d8ff2a 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -13,6 +13,7 @@ import { ODVerifyBar } from "../modules/verifybar" import * as discord from "discord.js" import { ODTranscriptCompiler, ODTranscriptCompilerCompileResult } from "../openticket/transcript" import { ODRole, ODRoleUpdateResult } from "../openticket/role" +import { ODPriority } from "../openticket/priority" /**## ODBuilderManager_Default `default_class` * This is a special class that adds type definitions & typescript to the ODBuilderManager class. @@ -294,6 +295,8 @@ export interface ODEmbedManagerIds_Default { "opendiscord:autodelete-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"opendiscord:autodelete-enable"}, "opendiscord:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autoclose-disable"}, "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, + "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority,reason:string|null},workers:"opendiscord:priority-set"}, + "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority},workers:"opendiscord:priority-get"}, } /**## ODEmbedManager_Default `default_class` @@ -428,6 +431,8 @@ export interface ODMessageManagerIds_Default { "opendiscord:autodelete-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"opendiscord:autodelete-enable"}, "opendiscord:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autoclose-disable"}, "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, + "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority,reason:string|null},workers:"opendiscord:priority-set"}, + "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority},workers:"opendiscord:priority-get"}, } /**## ODMessageManager_Default `default_class` diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts index 0859c14..c191d09 100644 --- a/src/core/api/defaults/client.ts +++ b/src/core/api/defaults/client.ts @@ -50,6 +50,7 @@ export interface ODSlashCommandManagerIds_Default { "opendiscord:clear":ODSlashCommand, "opendiscord:autoclose":ODSlashCommand, "opendiscord:autodelete":ODSlashCommand, + "opendiscord:priority":ODSlashCommand, } /**## ODSlashCommandManager_Default `default_class` @@ -119,7 +120,9 @@ export interface ODTextCommandManagerIds_Default { "opendiscord:autoclose-disable":ODTextCommand, "opendiscord:autoclose-enable":ODTextCommand, "opendiscord:autodelete-disable":ODTextCommand, - "opendiscord:autodelete-enable":ODTextCommand + "opendiscord:autodelete-enable":ODTextCommand, + "opendiscord:priority-set":ODTextCommand, + "opendiscord:priority-get":ODTextCommand, } /**## ODTextCommandManager_Default `default_class` diff --git a/src/core/api/defaults/event.ts b/src/core/api/defaults/event.ts index f4f6d05..169c757 100644 --- a/src/core/api/defaults/event.ts +++ b/src/core/api/defaults/event.ts @@ -42,6 +42,7 @@ import { ODQuestionManager } from "../openticket/question" import { ODBlacklistManager } from "../openticket/blacklist" import { ODTranscriptManager_Default } from "../openticket/transcript" import { ODRole, ODRoleManager } from "../openticket/role" +import { ODPriorityManager_Default } from "../openticket/priority" /**## ODEventIds_Default `interface` * This interface is a list of ids available in the `ODEvent_Default` class. @@ -122,6 +123,10 @@ export interface ODEventIds_Default { "onClientActivityInit": ODEvent_Default<(activity:ODClientActivityManager, client:ODClientManager_Default) => ODPromiseVoid> "afterClientActivityInitiated": ODEvent_Default<(activity:ODClientActivityManager, client:ODClientManager_Default) => ODPromiseVoid> + //priority levels + "onPriorityLoad": ODEvent_Default<(priorities:ODPriorityManager_Default) => ODPromiseVoid> + "afterPrioritiesLoaded": ODEvent_Default<(priorities:ODPriorityManager_Default) => ODPromiseVoid> + //client slash commands "onSlashCommandLoad": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> "afterSlashCommandsLoaded": ODEvent_Default<(slash:ODSlashCommandManager_Default, client:ODClientManager_Default) => ODPromiseVoid> diff --git a/src/core/api/main.ts b/src/core/api/main.ts index cec0b11..4738986 100644 --- a/src/core/api/main.ts +++ b/src/core/api/main.ts @@ -37,6 +37,7 @@ import { ODQuestionManager } from "./openticket/question" import { ODBlacklistManager } from "./openticket/blacklist" import { ODTranscriptManager_Default } from "./openticket/transcript" import { ODRoleManager } from "./openticket/role" +import { ODPriorityManager_Default } from "./openticket/priority" /**## ODMain `class` * This is the main Open Ticket class. @@ -127,6 +128,8 @@ export class ODMain { transcripts: ODTranscriptManager_Default /**The manager that manages all reaction roles in the bot. (here, you can add additional data to roles) */ roles: ODRoleManager + /**The manager that manages all priority levels in the bot. (register/edit ticket priority levels) */ + priorities: ODPriorityManager_Default constructor(){ this.versions = new ODVersionManager_Default() @@ -175,6 +178,7 @@ export class ODMain { this.blacklist = new ODBlacklistManager(this.debug) this.transcripts = new ODTranscriptManager_Default(this.debug,this.tickets,this.client) this.roles = new ODRoleManager(this.debug) + this.priorities = new ODPriorityManager_Default(this.debug) } /**Log a message to the console. But in the Open Ticket style :) */ diff --git a/src/core/api/modules/defaults.ts b/src/core/api/modules/defaults.ts index 4b366ff..c0b4491 100644 --- a/src/core/api/modules/defaults.ts +++ b/src/core/api/modules/defaults.ts @@ -83,6 +83,9 @@ export interface ODDefaults { /**Load the default Open Ticket client activity initialization (& status refresh). */ clientActivityInitiating:boolean, + /**Load the default Open Ticket priority levels. */ + priorityLoading:boolean, + /**Load the default Open Ticket slash commands. */ slashCommandLoading:boolean, /**Load the default Open Ticket slash command registerer (register slash cmds in discord). */ @@ -284,6 +287,8 @@ export class ODDefaultsManager { clientMultiGuildWarning:true, clientActivityLoading:true, clientActivityInitiating:true, + + priorityLoading:true, slashCommandLoading:true, slashCommandRegistering:true, diff --git a/src/core/api/openticket/priority.ts b/src/core/api/openticket/priority.ts new file mode 100644 index 0000000..085cba8 --- /dev/null +++ b/src/core/api/openticket/priority.ts @@ -0,0 +1,101 @@ +/////////////////////////////////////// +//OPENTICKET PRIORITY MODULE +/////////////////////////////////////// +import { ODId, ODManager, ODValidJsonType, ODValidId, ODVersion, ODManagerData } from "../modules/base" +import { ODDebugger } from "../modules/console" +import * as discord from "discord.js" + +/**## ODPriorityManager `class` + * This is an Open Ticket priority manager. + * + * This class manages all registered priority levels in the bot. + * + * Priorities levels can be changed/updated/translated by plugins to allow for more customisability. + */ +export class ODPriorityManager extends ODManager { + /**A reference to the Open Ticket debugger. */ + #debug: ODDebugger + + constructor(debug:ODDebugger){ + super(debug,"priority") + this.#debug = debug + } +} + +/**## ODPriorityIds `type` + * This interface is a list of ids available in the `ODPriorityManager` class. + * It's used to generate typescript declarations for this class. + */ +export interface ODPriorityIds { + "opendiscord:urgent":ODPriority, + "opendiscord:very-high":ODPriority, + "opendiscord:high":ODPriority, + "opendiscord:normal":ODPriority, + "opendiscord:low":ODPriority, + "opendiscord:very-low":ODPriority, + "opendiscord:none":ODPriority, +} + +/**## ODPriorityManager_Default `default_class` + * This is a special class that adds type definitions & typescript to the ODPriorityManager class. + * It doesn't add any extra features! + * + * This default class is made for the global variable `opendiscord.priorities`! + */ +export class ODPriorityManager_Default extends ODPriorityManager { + get(id:PriorityId): ODPriorityIds[PriorityId] + get(id:ODValidId): ODPriority|null + + get(id:ODValidId): ODPriority|null { + return super.get(id) + } + + remove(id:PriorityId): ODPriorityIds[PriorityId] + remove(id:ODValidId): ODPriority|null + + remove(id:ODValidId): ODPriority|null { + return super.remove(id) + } + + exists(id:keyof ODPriorityIds): boolean + exists(id:ODValidId): boolean + + exists(id:ODValidId): boolean { + return super.exists(id) + } +} + +/**## ODPriority `class` + * This is an Open Ticket priority level. + * + * Using this class, you can register or edit a priority level for the ticket priority system. + * + * Priority levels should be registered in `opendiscord.priorities`. + * + * #### 🚨 Negative priorities are treated as `disabled/no-priority`! + */ +export class ODPriority extends ODManagerData { + /**The priority level itself. A negative number (e.g. `-1`) is treated as `disabled/no-priority`. */ + priority:number + /**The raw name of the level (used in text/slash command inputs). */ + rawName:string + /**The display name of the level (used in embeds & messages). */ + displayName:string + /**The display emoji of the level (used in embeds & messages). */ + displayEmoji:string|null + /**The emoji added to the channel name when the level is applied to a ticket. */ + channelEmoji:string|null + + constructor(id:ODValidId,priority:number,rawName:string,displayName:string,displayEmoji:string|null,channelEmoji:string|null){ + super(id) + this.priority = priority + this.rawName = rawName + this.displayName = displayName + this.displayEmoji = displayEmoji + this.channelEmoji = channelEmoji + } + /**Get the display name + emoji for rendering this priority in the UI/embeds. */ + renderDisplayName(){ + return (this.displayEmoji ? this.displayEmoji+" " : "")+this.displayName + } +} \ No newline at end of file diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index 4e10510..08133e7 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -555,6 +555,42 @@ export const loadAllSlashCommands = async () => { } ] })) + //PRIORITY + if (allowedCommands.includes("priority")) commands.add(new api.ODSlashCommand("opendiscord:priority",{ + type:act.ChatInput, + name:"priority", + description:"Set the priority of the ticket.", //TODO TRANSLATION!!! + contexts:[discord.InteractionContextType.Guild], + integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], + options:[ + { + name:"set", + description:"Set the priority of the ticket.", //TODO TRANSLATION!!! + type:acot.Subcommand, + options:[ + { + name:"priority", + description:"The priority of the channel.", + type:acot.String, + required:true, + choices:opendiscord.priorities.getAll().sort((a,b) => b.priority-a.priority).map((prio) => ({value:prio.rawName,name:prio.renderDisplayName()})) + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + }, + { + name:"get", + description:"Get the priority of the ticket.", //TODO TRANSLATION!!! + type:acot.Subcommand + }, + //TODO: list (v4.2) + ] + })) } export const loadAllTextCommands = async () => { @@ -1057,6 +1093,37 @@ export const loadAllTextCommands = async () => { } ] })) + //PRIORITY + //TODO: priority list (v4.2) + if (allowedCommands.includes("priority")) commands.add(new api.ODTextCommand("opendiscord:priority-set",{ + name:"priority set", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"priority", + type:"string", + required:true, + allowSpaces:false, + choices:opendiscord.priorities.getAll().sort((a,b) => b.priority-a.priority).map((prio) => prio.rawName) + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) + if (allowedCommands.includes("priority")) commands.add(new api.ODTextCommand("opendiscord:priority-get",{ + name:"priority get", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + })) } export const loadAllContextMenus = async () => { diff --git a/src/data/framework/eventLoader.ts b/src/data/framework/eventLoader.ts index aeda57d..c7c0dd6 100644 --- a/src/data/framework/eventLoader.ts +++ b/src/data/framework/eventLoader.ts @@ -75,6 +75,10 @@ export const loadAllEvents = () => { "afterClientActivityLoaded", "onClientActivityInit", "afterClientActivityInitiated", + + //priority levels + "onPriorityLoad", + "afterPrioritiesLoaded", //client slash commands "onSlashCommandLoad", diff --git a/src/data/framework/helpMenuLoader.ts b/src/data/framework/helpMenuLoader.ts index 0c23297..9f301b1 100644 --- a/src/data/framework/helpMenuLoader.ts +++ b/src/data/framework/helpMenuLoader.ts @@ -269,5 +269,13 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"time",optional:false},{name:"reason",optional:true}], slashOptions:[{name:"time",optional:false},{name:"reason",optional:true}] })) + if (allowedCommands.includes("priority")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:priority-set",0,{ + textName:prefix+"priority set", + textDescription:"Set the priority of the ticket channel.", //TODO TRANSLATION!!! + slashName:"/priority set", + slashDescription:"Set the priority of the ticket channel.", //TODO TRANSLATION!!! + textOptions:[{name:"priority",optional:false}], + slashOptions:[{name:"priority",optional:false}] + })) } } \ No newline at end of file diff --git a/src/data/openticket/priorityLoader.ts b/src/data/openticket/priorityLoader.ts new file mode 100644 index 0000000..e9abdd7 --- /dev/null +++ b/src/data/openticket/priorityLoader.ts @@ -0,0 +1,11 @@ +import {opendiscord, api, utilities} from "../../index" + +export const loadAllPriorities = async () => { + opendiscord.priorities.add(new api.ODPriority("opendiscord:urgent",5,"urgent","Urgent","🔴","🔴")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriority("opendiscord:very-high",4,"very-high","Very High","🟠","🟠")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriority("opendiscord:high",3,"high","High","🟡","🟡")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriority("opendiscord:normal",2,"normal","Normal","🟢","🟢")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriority("opendiscord:low",1,"low","Low","🔵","🔵")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriority("opendiscord:very-low",0,"very-low","Very Low","⚪","⚪")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriority("opendiscord:none",-1,"none","None",null,null)) //TODO TRANSLATION!!! +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9963ce9..7cd2146 100644 --- a/src/index.ts +++ b/src/index.ts @@ -424,6 +424,14 @@ const main = async () => { await opendiscord.events.get("afterClientActivityInitiated").emit([opendiscord.client.activity,opendiscord.client]) } + //load priority levels + opendiscord.log("Loading prioritiy levels...","system") + if (opendiscord.defaults.getDefault("priorityLoading")){ + await (await import("./data/openticket/priorityLoader.js")).loadAllPriorities() + } + await opendiscord.events.get("onPriorityLoad").emit([opendiscord.priorities]) + await opendiscord.events.get("afterPrioritiesLoaded").emit([opendiscord.priorities]) + //load slash commands opendiscord.log("Loading slash commands...","system") if (opendiscord.defaults.getDefault("slashCommandLoading")){ From 6ce017b4073562466e96afd6b38cdf1839a0da06 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:04:42 +0200 Subject: [PATCH 57/78] Added /topic set command (Part 1, Incomplete) --- src/builders/embeds.ts | 14 +++++++++ src/core/api/defaults/builder.ts | 4 +++ src/core/api/defaults/client.ts | 2 ++ src/data/framework/commandLoader.ts | 45 ++++++++++++++++++++++++++++ src/data/framework/helpMenuLoader.ts | 24 ++++++++++----- 5 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index a145298..52c2b12 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -1313,6 +1313,20 @@ const autoEmbeds = () => { } const extraEmbeds = () => { + //TOPIC SET + embeds.add(new api.ODEmbed("opendiscord:topic-set")) + embeds.get("opendiscord:topic-set").workers.add( + new api.ODWorker("opendiscord:topic-set",0,async (instance,params,source) => { + const {user,topic} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("ℹ️","Topic Changed")) //TODO TRANSLATION!!! + instance.setDescription("The channel topic has been changed by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! + if (topic) instance.addFields({name:"Topic"+":",value:"```"+topic+"```"}) //TODO TRANSLATION!!! + }) + ) + //PRIORITY SET embeds.add(new api.ODEmbed("opendiscord:priority-set")) embeds.get("opendiscord:priority-set").workers.add( diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index 7d8ff2a..bb84507 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -295,6 +295,8 @@ export interface ODEmbedManagerIds_Default { "opendiscord:autodelete-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"opendiscord:autodelete-enable"}, "opendiscord:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autoclose-disable"}, "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, + + "opendiscord:topic-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority,reason:string|null},workers:"opendiscord:priority-set"}, "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority},workers:"opendiscord:priority-get"}, } @@ -431,6 +433,8 @@ export interface ODMessageManagerIds_Default { "opendiscord:autodelete-enable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,time:number,reason:string|null},workers:"opendiscord:autodelete-enable"}, "opendiscord:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autoclose-disable"}, "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, + + "opendiscord:topic-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority,reason:string|null},workers:"opendiscord:priority-set"}, "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority},workers:"opendiscord:priority-get"}, } diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts index c191d09..a56ad33 100644 --- a/src/core/api/defaults/client.ts +++ b/src/core/api/defaults/client.ts @@ -50,6 +50,7 @@ export interface ODSlashCommandManagerIds_Default { "opendiscord:clear":ODSlashCommand, "opendiscord:autoclose":ODSlashCommand, "opendiscord:autodelete":ODSlashCommand, + "opendiscord:topic":ODSlashCommand, "opendiscord:priority":ODSlashCommand, } @@ -121,6 +122,7 @@ export interface ODTextCommandManagerIds_Default { "opendiscord:autoclose-enable":ODTextCommand, "opendiscord:autodelete-disable":ODTextCommand, "opendiscord:autodelete-enable":ODTextCommand, + "opendiscord:topic-set":ODTextCommand, "opendiscord:priority-set":ODTextCommand, "opendiscord:priority-get":ODTextCommand, } diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index 08133e7..d20df8f 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -555,6 +555,32 @@ export const loadAllSlashCommands = async () => { } ] })) + + //TOPIC + if (allowedCommands.includes("topic")) commands.add(new api.ODSlashCommand("opendiscord:topic",{ + type:act.ChatInput, + name:"topic", + description:"Change the topic of the ticket channel.", //TODO TRANSLATION!!! + contexts:[discord.InteractionContextType.Guild], + integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], + options:[ + { + name:"set", + description:"Set the topic of the ticket channel to a specific value.", //TODO TRANSLATION!!! + type:acot.Subcommand, + options:[ + { + name:"topic", + description:"The new topic of the channel.", + type:acot.String, + required:true + } + ] + }, + //TODO: list (v4.2) + ] + })) + //PRIORITY if (allowedCommands.includes("priority")) commands.add(new api.ODSlashCommand("opendiscord:priority",{ type:act.ChatInput, @@ -1093,6 +1119,25 @@ export const loadAllTextCommands = async () => { } ] })) + + //TOPIC + //TODO: topic list (v4.2) + if (allowedCommands.includes("topic")) commands.add(new api.ODTextCommand("opendiscord:topic-set",{ + name:"topic set", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"topic", + type:"string", + required:true, + allowSpaces:true + } + ] + })) + //PRIORITY //TODO: priority list (v4.2) if (allowedCommands.includes("priority")) commands.add(new api.ODTextCommand("opendiscord:priority-set",{ diff --git a/src/data/framework/helpMenuLoader.ts b/src/data/framework/helpMenuLoader.ts index 9f301b1..3cc1917 100644 --- a/src/data/framework/helpMenuLoader.ts +++ b/src/data/framework/helpMenuLoader.ts @@ -207,13 +207,13 @@ export const loadAllHelpMenuComponents = async () => { const advanced = helpmenu.get("opendiscord:advanced") if (advanced){ - if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-global",5,{ + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-global",9,{ textName:prefix+"stats global", textDescription:lang.getTranslation("commands.statsGlobal"), slashName:"/stats global", slashDescription:lang.getTranslation("commands.statsGlobal") })) - if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-ticket",4,{ + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-ticket",8,{ textName:prefix+"stats ticket", textDescription:lang.getTranslation("commands.statsTicket"), slashName:"/stats ticket", @@ -221,7 +221,7 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"ticket",optional:false}], slashOptions:[{name:"ticket",optional:false}] })) - if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-user",2,{ + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-user",7,{ textName:prefix+"stats user", textDescription:lang.getTranslation("commands.statsUser"), slashName:"/stats user", @@ -229,7 +229,7 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"user",optional:false}], slashOptions:[{name:"user",optional:false}] })) - if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-reset",2,{ + if (allowedCommands.includes("stats")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:stats-reset",6,{ textName:prefix+"stats reset", textDescription:lang.getTranslation("commands.statsReset"), slashName:"/stats reset", @@ -237,7 +237,7 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"reason",optional:true}], slashOptions:[{name:"reason",optional:true}] })) - if (allowedCommands.includes("autoclose")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autoclose-disable",1,{ + if (allowedCommands.includes("autoclose")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autoclose-disable",5,{ textName:prefix+"autoclose disable", textDescription:lang.getTranslation("commands.autocloseDisable"), slashName:"/autoclose disable", @@ -245,7 +245,7 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"reason",optional:true}], slashOptions:[{name:"reason",optional:true}] })) - if (allowedCommands.includes("autoclose")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autoclose-enable",0,{ + if (allowedCommands.includes("autoclose")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autoclose-enable",4,{ textName:prefix+"autoclose enable", textDescription:lang.getTranslation("commands.autocloseEnable"), slashName:"/autoclose enable", @@ -253,7 +253,7 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"time",optional:false},{name:"reason",optional:true}], slashOptions:[{name:"time",optional:false},{name:"reason",optional:true}] })) - if (allowedCommands.includes("autodelete")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autodelete-disable",1,{ + if (allowedCommands.includes("autodelete")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autodelete-disable",3,{ textName:prefix+"autodelete disable", textDescription:lang.getTranslation("commands.autodeleteDisable"), slashName:"/autodelete disable", @@ -261,7 +261,7 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"reason",optional:true}], slashOptions:[{name:"reason",optional:true}] })) - if (allowedCommands.includes("autodelete")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autodelete-enable",0,{ + if (allowedCommands.includes("autodelete")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:autodelete-enable",2,{ textName:prefix+"autodelete enable", textDescription:lang.getTranslation("commands.autodeleteEnable"), slashName:"/autodelete enable", @@ -269,6 +269,14 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"time",optional:false},{name:"reason",optional:true}], slashOptions:[{name:"time",optional:false},{name:"reason",optional:true}] })) + if (allowedCommands.includes("topic")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:topic-set",1,{ + textName:prefix+"topic set", + textDescription:"Change the topic of the ticket channel.", //TODO TRANSLATION!!! + slashName:"/topic set", + slashDescription:"Change the topic of the ticket channel.", //TODO TRANSLATION!!! + textOptions:[{name:"topic",optional:false}], + slashOptions:[{name:"topic",optional:false}] + })) if (allowedCommands.includes("priority")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:priority-set",0,{ textName:prefix+"priority set", textDescription:"Set the priority of the ticket channel.", //TODO TRANSLATION!!! From 72f84b2566416b9c637e2da38ea540ae782a176a Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:05:30 +0200 Subject: [PATCH 58/78] (API) Class & comment readability improvements --- src/core/api/openticket/role.ts | 2 +- src/core/api/openticket/transcript.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/api/openticket/role.ts b/src/core/api/openticket/role.ts index ce22481..b61c462 100644 --- a/src/core/api/openticket/role.ts +++ b/src/core/api/openticket/role.ts @@ -37,7 +37,7 @@ export interface ODRoleDataJson { value:ODValidJsonType } -/**## ODRoleDataJson `interface` +/**## ODRoleJson `interface` * The JSON representatation from a single role. */ export interface ODRoleJson { diff --git a/src/core/api/openticket/transcript.ts b/src/core/api/openticket/transcript.ts index b5e6e55..6a7d6a4 100644 --- a/src/core/api/openticket/transcript.ts +++ b/src/core/api/openticket/transcript.ts @@ -132,14 +132,14 @@ export interface ODTranscriptCompilerIds { * This default class is made for the global variable `opendiscord.transcripts`! */ export class ODTranscriptManager_Default extends ODTranscriptManager { - get(id:QuestionId): ODTranscriptCompilerIds[QuestionId] + get(id:CompilerId): ODTranscriptCompilerIds[CompilerId] get(id:ODValidId): ODTranscriptCompiler|null get(id:ODValidId): ODTranscriptCompiler|null { return super.get(id) } - remove(id:QuestionId): ODTranscriptCompilerIds[QuestionId] + remove(id:CompilerId): ODTranscriptCompilerIds[CompilerId] remove(id:ODValidId): ODTranscriptCompiler|null remove(id:ODValidId): ODTranscriptCompiler|null { From 30a0a6c503b38d5d1af856380d9473f30fb61446 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:09:09 +0200 Subject: [PATCH 59/78] (API) Renamed ODPriority --> ODPriorityLevel --- src/core/api/defaults/builder.ts | 10 +++---- src/core/api/openticket/priority.ts | 38 +++++++++++++-------------- src/data/openticket/priorityLoader.ts | 16 +++++------ src/index.ts | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index bb84507..ea1c9b3 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -13,7 +13,7 @@ import { ODVerifyBar } from "../modules/verifybar" import * as discord from "discord.js" import { ODTranscriptCompiler, ODTranscriptCompilerCompileResult } from "../openticket/transcript" import { ODRole, ODRoleUpdateResult } from "../openticket/role" -import { ODPriority } from "../openticket/priority" +import { ODPriorityLevel } from "../openticket/priority" /**## ODBuilderManager_Default `default_class` * This is a special class that adds type definitions & typescript to the ODBuilderManager class. @@ -297,8 +297,8 @@ export interface ODEmbedManagerIds_Default { "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, "opendiscord:topic-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, - "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority,reason:string|null},workers:"opendiscord:priority-set"}, - "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority},workers:"opendiscord:priority-get"}, + "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel,reason:string|null},workers:"opendiscord:priority-set"}, + "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel},workers:"opendiscord:priority-get"}, } /**## ODEmbedManager_Default `default_class` @@ -435,8 +435,8 @@ export interface ODMessageManagerIds_Default { "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, "opendiscord:topic-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, - "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority,reason:string|null},workers:"opendiscord:priority-set"}, - "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriority},workers:"opendiscord:priority-get"}, + "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel,reason:string|null},workers:"opendiscord:priority-set"}, + "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel},workers:"opendiscord:priority-get"}, } /**## ODMessageManager_Default `default_class` diff --git a/src/core/api/openticket/priority.ts b/src/core/api/openticket/priority.ts index 085cba8..300f61d 100644 --- a/src/core/api/openticket/priority.ts +++ b/src/core/api/openticket/priority.ts @@ -12,7 +12,7 @@ import * as discord from "discord.js" * * Priorities levels can be changed/updated/translated by plugins to allow for more customisability. */ -export class ODPriorityManager extends ODManager { +export class ODPriorityManager extends ODManager { /**A reference to the Open Ticket debugger. */ #debug: ODDebugger @@ -22,18 +22,18 @@ export class ODPriorityManager extends ODManager { } } -/**## ODPriorityIds `type` +/**## ODPriorityManagerIds `type` * This interface is a list of ids available in the `ODPriorityManager` class. * It's used to generate typescript declarations for this class. */ -export interface ODPriorityIds { - "opendiscord:urgent":ODPriority, - "opendiscord:very-high":ODPriority, - "opendiscord:high":ODPriority, - "opendiscord:normal":ODPriority, - "opendiscord:low":ODPriority, - "opendiscord:very-low":ODPriority, - "opendiscord:none":ODPriority, +export interface ODPriorityManagerIds { + "opendiscord:urgent":ODPriorityLevel, + "opendiscord:very-high":ODPriorityLevel, + "opendiscord:high":ODPriorityLevel, + "opendiscord:normal":ODPriorityLevel, + "opendiscord:low":ODPriorityLevel, + "opendiscord:very-low":ODPriorityLevel, + "opendiscord:none":ODPriorityLevel, } /**## ODPriorityManager_Default `default_class` @@ -43,21 +43,21 @@ export interface ODPriorityIds { * This default class is made for the global variable `opendiscord.priorities`! */ export class ODPriorityManager_Default extends ODPriorityManager { - get(id:PriorityId): ODPriorityIds[PriorityId] - get(id:ODValidId): ODPriority|null + get(id:PriorityId): ODPriorityManagerIds[PriorityId] + get(id:ODValidId): ODPriorityLevel|null - get(id:ODValidId): ODPriority|null { + get(id:ODValidId): ODPriorityLevel|null { return super.get(id) } - remove(id:PriorityId): ODPriorityIds[PriorityId] - remove(id:ODValidId): ODPriority|null + remove(id:PriorityId): ODPriorityManagerIds[PriorityId] + remove(id:ODValidId): ODPriorityLevel|null - remove(id:ODValidId): ODPriority|null { + remove(id:ODValidId): ODPriorityLevel|null { return super.remove(id) } - exists(id:keyof ODPriorityIds): boolean + exists(id:keyof ODPriorityManagerIds): boolean exists(id:ODValidId): boolean exists(id:ODValidId): boolean { @@ -65,7 +65,7 @@ export class ODPriorityManager_Default extends ODPriorityManager { } } -/**## ODPriority `class` +/**## ODPriorityLevel `class` * This is an Open Ticket priority level. * * Using this class, you can register or edit a priority level for the ticket priority system. @@ -74,7 +74,7 @@ export class ODPriorityManager_Default extends ODPriorityManager { * * #### 🚨 Negative priorities are treated as `disabled/no-priority`! */ -export class ODPriority extends ODManagerData { +export class ODPriorityLevel extends ODManagerData { /**The priority level itself. A negative number (e.g. `-1`) is treated as `disabled/no-priority`. */ priority:number /**The raw name of the level (used in text/slash command inputs). */ diff --git a/src/data/openticket/priorityLoader.ts b/src/data/openticket/priorityLoader.ts index e9abdd7..d0e85e1 100644 --- a/src/data/openticket/priorityLoader.ts +++ b/src/data/openticket/priorityLoader.ts @@ -1,11 +1,11 @@ import {opendiscord, api, utilities} from "../../index" -export const loadAllPriorities = async () => { - opendiscord.priorities.add(new api.ODPriority("opendiscord:urgent",5,"urgent","Urgent","🔴","🔴")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriority("opendiscord:very-high",4,"very-high","Very High","🟠","🟠")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriority("opendiscord:high",3,"high","High","🟡","🟡")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriority("opendiscord:normal",2,"normal","Normal","🟢","🟢")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriority("opendiscord:low",1,"low","Low","🔵","🔵")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriority("opendiscord:very-low",0,"very-low","Very Low","⚪","⚪")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriority("opendiscord:none",-1,"none","None",null,null)) //TODO TRANSLATION!!! +export const loadAllPriorityLevels = async () => { + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:urgent",5,"urgent","Urgent","🔴","🔴")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:very-high",4,"very-high","Very High","🟠","🟠")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:high",3,"high","High","🟡","🟡")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:normal",2,"normal","Normal","🟢","🟢")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:low",1,"low","Low","🔵","🔵")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:very-low",0,"very-low","Very Low","⚪","⚪")) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:none",-1,"none","None",null,null)) //TODO TRANSLATION!!! } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7cd2146..49a5134 100644 --- a/src/index.ts +++ b/src/index.ts @@ -427,7 +427,7 @@ const main = async () => { //load priority levels opendiscord.log("Loading prioritiy levels...","system") if (opendiscord.defaults.getDefault("priorityLoading")){ - await (await import("./data/openticket/priorityLoader.js")).loadAllPriorities() + await (await import("./data/openticket/priorityLoader.js")).loadAllPriorityLevels() } await opendiscord.events.get("onPriorityLoad").emit([opendiscord.priorities]) await opendiscord.events.get("afterPrioritiesLoaded").emit([opendiscord.priorities]) From d2cd6be5132c2300ddfb153a17de1b665316e8e2 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:22:09 +0200 Subject: [PATCH 60/78] Small nickname suffix fixes & discord.js try-catch --- src/core/api/openticket/option.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/api/openticket/option.ts b/src/core/api/openticket/option.ts index 9bd6ad9..f5351a7 100644 --- a/src/core/api/openticket/option.ts +++ b/src/core/api/openticket/option.ts @@ -353,11 +353,16 @@ export class ODOptionSuffixManager extends ODManager { } /**Instantly get the suffix from an `ODTicketOption`. */ - async getSuffixFromOption(option:ODTicketOption,user:discord.User, guild: discord.Guild): Promise { + async getSuffixFromOption(option:ODTicketOption,user:discord.User,guild:discord.Guild): Promise { const suffix = this.getAll().find((suffix) => suffix.option.id.value == option.id.value) if (!suffix) return null - const member = await guild.members.fetch(user.id); - return await suffix.getSuffix(member) + try{ + const member = await guild.members.fetch(user.id) + return await suffix.getSuffix(member) + }catch(err){ + process.emit("uncaughtException",err) + return null + } } } From 4aacdd1607b80ca73849496b86bd7d635196dc75 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 21 Sep 2025 20:31:47 +0200 Subject: [PATCH 61/78] fix: #122 improved windows OT plugin support Open Ticket instances with plugins are now also able to exist outside of the default C:\\ drive on windows. This has been fixed by searching for the plugins relative to the bot instead of using an absolute path. --- src/core/api/modules/plugin.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/core/api/modules/plugin.ts b/src/core/api/modules/plugin.ts index ff9d412..9a46320 100644 --- a/src/core/api/modules/plugin.ts +++ b/src/core/api/modules/plugin.ts @@ -146,11 +146,8 @@ export class ODPlugin extends ODManagerData { async execute(debug:ODDebugger,force?:boolean): Promise { if ((this.enabled && !this.crashed) || force){ try{ - //rewrite the path to make it work on windows & unix based systems - const workingDir = process.cwd().replace(/C:\\/i,"/") - const workingPath = nodepath.join("./dist/plugins/",this.getStartFile()) - const pluginPath = nodepath.join(workingDir,workingPath).split(nodepath.sep).join("/") - + //import relative plugin directory path (works on windows & unix based systems) + const pluginPath = nodepath.join("../../../../plugins/",this.getStartFile()) await import(pluginPath) debug.console.log("Plugin \""+this.id.value+"\" loaded successfully!","plugin") this.executed = true From 2aef85590252704bde12d8d4d127a1a2ea393181 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:18:38 +0200 Subject: [PATCH 62/78] Started working on channel topic again --- src/actions/createTicket.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index 1d05f8f..d3a63ac 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -19,7 +19,7 @@ export const registerActions = async () => { const channelPrefix = option.get("opendiscord:channel-prefix").value const channelCategory = option.get("opendiscord:channel-category").value const channelBackupCategory = option.get("opendiscord:channel-category-backup").value - const channelTopic = option.get("opendiscord:channel-topic").value + const channelTopicText = option.get("opendiscord:channel-topic").value const channelSuffix = await opendiscord.options.suffix.getSuffixFromOption(option,user,guild) const channelName = channelPrefix+channelSuffix @@ -58,6 +58,12 @@ export const registerActions = async () => { } } + //handle channel topic + const channelTopics: string[] = [] + if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(channelTopicText) + if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push(discord.userMention(user.id)) + //if (generalConfig.data.system.channelTopic.showClaimed) TODO TODO TODO + //handle permissions const permissions: discord.OverwriteResolvable[] = [{ type:discord.OverwriteType.Role, @@ -110,7 +116,7 @@ export const registerActions = async () => { type:discord.ChannelType.GuildText, name:channelName, nsfw:false, - topic:channelTopic, + topic:(channelTopics.length > 0) ? channelTopics.join(" | ") : undefined, parent:category, reason:"Ticket Created By "+user.displayName, permissionOverwrites:permissions, From 30b6ab4df6cb3b8c5a818f60ca116e265577a20a Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:41:56 +0100 Subject: [PATCH 63/78] Finished channel topic on ticket creation --- config/general.json | 2 +- src/actions/createTicket.ts | 51 ++++++++++++++++------------- src/core/api/defaults/config.ts | 4 +-- src/core/cli/quickSetup.ts | 2 +- src/core/startup/migration.ts | 2 +- src/data/framework/checkerLoader.ts | 2 +- src/data/framework/configLoader.ts | 2 +- 7 files changed, 36 insertions(+), 29 deletions(-) diff --git a/config/general.json b/config/general.json index f950d10..39abe86 100644 --- a/config/general.json +++ b/config/general.json @@ -72,10 +72,10 @@ "showOptionName":true, "showOptionDescription":false, "showOptionTopic":true, + "showPriority":false, "showClosed":true, "showClaimed":false, "showPinned":false, - "showPriority":false, "showCreator":false, "showParticipants":false }, diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index d3a63ac..4e79d7f 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -58,12 +58,6 @@ export const registerActions = async () => { } } - //handle channel topic - const channelTopics: string[] = [] - if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(channelTopicText) - if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push(discord.userMention(user.id)) - //if (generalConfig.data.system.channelTopic.showClaimed) TODO TODO TODO - //handle permissions const permissions: discord.OverwriteResolvable[] = [{ type:discord.OverwriteType.Role, @@ -109,22 +103,6 @@ export const registerActions = async () => { deny:[] }) - const slowMode = option.get("opendiscord:slowmode-enabled").value ? option.get("opendiscord:slowmode-seconds").value : undefined - - //create channel - const channel = await guild.channels.create({ - type:discord.ChannelType.GuildText, - name:channelName, - nsfw:false, - topic:(channelTopics.length > 0) ? channelTopics.join(" | ") : undefined, - parent:category, - reason:"Ticket Created By "+user.displayName, - permissionOverwrites:permissions, - rateLimitPerUser:slowMode - }) - - await opendiscord.events.get("afterTicketChannelCreated").emit([option,channel,user]) - //create participants const participants: {type:"role"|"user",id:string}[] = [] permissions.forEach((permission,index) => { @@ -134,6 +112,35 @@ export const registerActions = async () => { participants.push({type,id}) }) + //manage slowmode + const slowMode = option.get("opendiscord:slowmode-enabled").value ? option.get("opendiscord:slowmode-seconds").value : undefined + + //handle channel topic + const channelTopics: string[] = [] + if (generalConfig.data.system.channelTopic.showOptionName) channelTopics.push(option.get("opendiscord:name").value) + if (generalConfig.data.system.channelTopic.showOptionDescription) channelTopics.push(option.get("opendiscord:description").value) + if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(channelTopicText) + if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**Priority:** "+opendiscord.priorities.get("opendiscord:none").renderDisplayName()) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** Opened") //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** No-one") //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** No") //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**Creator:** "+discord.userMention(user.id)) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**Participants:** "+participants.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //TODO TRANSLATION!!! + + //create channel + const channel = await guild.channels.create({ + type:discord.ChannelType.GuildText, + name:channelName, + nsfw:false, + topic:(channelTopics.length > 0) ? channelTopics.join(" • ") : undefined, + parent:category, + reason:"Ticket Created By "+user.displayName, + permissionOverwrites:permissions, + rateLimitPerUser:slowMode + }) + + await opendiscord.events.get("afterTicketChannelCreated").emit([option,channel,user]) + //create ticket const ticket = new api.ODTicket(channel.id,option,[ new api.ODTicketData("opendiscord:busy",false), diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index e39b98e..d29f39d 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -143,14 +143,14 @@ export interface ODJsonConfig_DefaultSystemChannelTopic { showOptionDescription:boolean, /**Show the option topic text in the channel topic (configured in the options config). */ showOptionTopic:boolean, + /**Show the current priority in the channel topic (auto-updated). */ + showPriority:boolean, /**Show the current close/reopen status in the channel topic (auto-updated). */ showClosed:boolean, /**Show the current claim status in the channel topic (auto-updated). */ showClaimed:boolean, /**Show the current pin status in the channel topic (auto-updated). */ showPinned:boolean, - /**Show the current priority in the channel topic (auto-updated). */ - showPriority:boolean, /**Show the creator of the ticket in the channel topic (auto-updated on transfer). */ showCreator:boolean, /**Show the first 5 participants of the ticket in the channel topic (auto-updated). */ diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index 8dc6312..d09a088 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -1272,10 +1272,10 @@ async function saveQuickSetupConfig(){ showOptionName:true, showOptionDescription:false, showOptionTopic:true, + showPriority:false, showClosed:true, showClaimed:false, showPinned:false, - showPriority:false, showCreator:false, showParticipants:false }, diff --git a/src/core/startup/migration.ts b/src/core/startup/migration.ts index 64376bc..c7e3085 100644 --- a/src/core/startup/migration.ts +++ b/src/core/startup/migration.ts @@ -70,10 +70,10 @@ export const migrations = [ showOptionName:true, showOptionDescription:false, showOptionTopic:true, + showPriority:false, showClosed:true, showClaimed:false, showPinned:false, - showPriority:false, showCreator:false, showParticipants:false } diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index ef7a3d7..7ca554b 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -311,10 +311,10 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"showOptionName",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-name",{cliDisplayName:"Show Option Name",cliDisplayDescription:"Show the option name in the channel topic."})}, {key:"showOptionDescription",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-description",{cliDisplayName:"Show Option Description",cliDisplayDescription:"Show the option description in the channel topic."})}, {key:"showOptionTopic",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-option-topic",{cliDisplayName:"Show Option Topic",cliDisplayDescription:"Show the option topic text in the channel topic (configured in the options.json config)."})}, + {key:"showPriority",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-priority",{cliDisplayName:"Show Priority",cliDisplayDescription:"Show the current priority in the channel topic (auto-updated)."})}, {key:"showClosed",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-closed",{cliDisplayName:"Show Closed Status",cliDisplayDescription:"Show the current close/reopen status in the channel topic (auto-updated)."})}, {key:"showClaimed",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-claimed",{cliDisplayName:"Show Claimed Status",cliDisplayDescription:"Show the current claim status in the channel topic (auto-updated)."})}, {key:"showPinned",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-pinned",{cliDisplayName:"Show Pinned Status",cliDisplayDescription:"Show the current pin status in the channel topic (auto-updated)."})}, - {key:"showPriority",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-priority",{cliDisplayName:"Show Priority",cliDisplayDescription:"Show the current priority in the channel topic (auto-updated)."})}, {key:"showCreator",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-creator",{cliDisplayName:"Show Creator",cliDisplayDescription:"Show the creator of the ticket in the channel topic (auto-updated on transfer)."})}, {key:"showParticipants",checker:new api.ODCheckerBooleanStructure("opendiscord:topic-show-participants",{cliDisplayName:"Show Participants",cliDisplayDescription:"Show the first 5 participants of the ticket in the channel topic (auto-updated)."})}, diff --git a/src/data/framework/configLoader.ts b/src/data/framework/configLoader.ts index f902249..c39b045 100644 --- a/src/data/framework/configLoader.ts +++ b/src/data/framework/configLoader.ts @@ -107,10 +107,10 @@ export const defaultGeneralFormatter = new fjs.ObjectFormatter(null,true,[ new fjs.PropertyFormatter("showOptionName"), new fjs.PropertyFormatter("showOptionDescription"), new fjs.PropertyFormatter("showOptionTopic"), + new fjs.PropertyFormatter("showPriority"), new fjs.PropertyFormatter("showClosed"), new fjs.PropertyFormatter("showClaimed"), new fjs.PropertyFormatter("showPinned"), - new fjs.PropertyFormatter("showPriority"), new fjs.PropertyFormatter("showCreator"), new fjs.PropertyFormatter("showParticipants"), ]), From 8413d3c2b7ef123f5379fa7d97291cadacaf7958 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Mon, 27 Oct 2025 21:19:59 +0100 Subject: [PATCH 64/78] Started with channel topic auto-update action --- src/actions/updateTicketTopic.ts | 55 ++++++++++++++++++++++++++++++++ src/core/api/defaults/action.ts | 8 ++++- src/core/api/defaults/builder.ts | 4 +-- src/index.ts | 1 + 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/actions/updateTicketTopic.ts diff --git a/src/actions/updateTicketTopic.ts b/src/actions/updateTicketTopic.ts new file mode 100644 index 0000000..5a732cd --- /dev/null +++ b/src/actions/updateTicketTopic.ts @@ -0,0 +1,55 @@ +/////////////////////////////////////// +//TICKET TOPIC SYSTEM +/////////////////////////////////////// +import {opendiscord, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = opendiscord.configs.get("opendiscord:general") + +export const registerActions = async () => { + opendiscord.actions.add(new api.ODAction("opendiscord:update-ticket-topic")) + opendiscord.actions.get("opendiscord:update-ticket-topic").workers.add([ + new api.ODWorker("opendiscord:update-ticket-topic",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,newTopic} = params + if (channel.isThread() || !(channel instanceof discord.TextChannel)) throw new api.ODSystemError("Unable to set topic of ticket! Open Ticket doesn't support threads!") + + //update ticket + ticket.get("opendiscord:busy").value = true + if (newTopic) ticket.get("opendiscord:topic").value = newTopic + + //handle channel topic + const channelTopics: string[] = [] + if (generalConfig.data.system.channelTopic.showOptionName) channelTopics.push(ticket.option.get("opendiscord:name").value) + if (generalConfig.data.system.channelTopic.showOptionDescription) channelTopics.push(ticket.option.get("opendiscord:description").value) + if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(ticket.get("opendiscord:topic").value) + if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**Priority:** "+opendiscord.priorities.get("opendiscord:none").renderDisplayName()) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** Opened") //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** No-one") //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** No") //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**Creator:** "+discord.userMention(user.id)) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**Participants:** "+ticket.get("opendiscord:participants").value.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //TODO TRANSLATION!!! + + //update channel + channel.setTopic(channelTopics.join(" • "),"Topic Changed") + + //reply with new message + if (params.sendMessage && newTopic) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:topic-set").build(source,{guild,channel,user,ticket,topic:newTopic})).message) + ticket.get("opendiscord:busy").value = false + }), + new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + }), + new api.ODWorker("opendiscord:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket,newTopic} = params + + if (newTopic) opendiscord.log(user.displayName+" changed the topic of a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"topic",value:newTopic}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/core/api/defaults/action.ts b/src/core/api/defaults/action.ts index a6ec1a7..7b0d01a 100644 --- a/src/core/api/defaults/action.ts +++ b/src/core/api/defaults/action.ts @@ -111,7 +111,13 @@ export interface ODActionManagerIds_Default { params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,filter:ODTicketClearFilter,list:ODTicket[]}, result:{list:string[]}, workers:"opendiscord:clear-tickets"|"opendiscord:discord-logs"|"opendiscord:logs" - } + }, + "opendiscord:update-ticket-topic":{ + source:"slash"|"text"|"ticket-action"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,newTopic:string|null,sendMessage:boolean}, + result:{}, + workers:"opendiscord:update-ticket-topic"|"opendiscord:discord-logs"|"opendiscord:logs" + }, } /**## ODActionManager_Default `default_class` diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index ea1c9b3..1702e6f 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -296,7 +296,7 @@ export interface ODEmbedManagerIds_Default { "opendiscord:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autoclose-disable"}, "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, - "opendiscord:topic-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, + "opendiscord:topic-set":{source:"slash"|"text"|"ticket-action"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel,reason:string|null},workers:"opendiscord:priority-set"}, "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel},workers:"opendiscord:priority-get"}, } @@ -434,7 +434,7 @@ export interface ODMessageManagerIds_Default { "opendiscord:autoclose-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autoclose-disable"}, "opendiscord:autodelete-disable":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,reason:string|null},workers:"opendiscord:autodelete-disable"}, - "opendiscord:topic-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, + "opendiscord:topic-set":{source:"slash"|"text"|"ticket-action"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel,reason:string|null},workers:"opendiscord:priority-set"}, "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel},workers:"opendiscord:priority-get"}, } diff --git a/src/index.ts b/src/index.ts index 49a5134..bf29092 100644 --- a/src/index.ts +++ b/src/index.ts @@ -757,6 +757,7 @@ const main = async () => { await (await import("./actions/removeTicketUser.js")).registerActions() await (await import("./actions/reactionRole.js")).registerActions() await (await import("./actions/clearTickets.js")).registerActions() + await (await import("./actions/updateTicketTopic.js")).registerActions() } await opendiscord.events.get("onActionLoad").emit([opendiscord.actions]) await opendiscord.events.get("afterActionsLoaded").emit([opendiscord.actions]) From e8f96ef84f81329c968d74dd7c1ccca7dd791b23 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:24:08 +0100 Subject: [PATCH 65/78] Finished channel topic feature --- src/actions/addTicketUser.ts | 3 +++ src/actions/claimTicket.ts | 3 +++ src/actions/closeTicket.ts | 3 +++ src/actions/moveTicket.ts | 3 +++ src/actions/pinTicket.ts | 3 +++ src/actions/removeTicketUser.ts | 3 +++ src/actions/renameTicket.ts | 3 +++ src/actions/reopenTicket.ts | 3 +++ src/actions/unclaimTicket.ts | 3 +++ src/actions/unpinTicket.ts | 3 +++ src/actions/updateTicketTopic.ts | 11 ++++++++--- 11 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/actions/addTicketUser.ts b/src/actions/addTicketUser.ts index cf3cf0f..d595597 100644 --- a/src/actions/addTicketUser.ts +++ b/src/actions/addTicketUser.ts @@ -54,6 +54,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:add-message").build(source,{guild,channel,user,ticket,reason,data})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketUserAdded").emit([ticket,user,data,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason,data} = params diff --git a/src/actions/claimTicket.ts b/src/actions/claimTicket.ts index 6729254..502db01 100644 --- a/src/actions/claimTicket.ts +++ b/src/actions/claimTicket.ts @@ -65,6 +65,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:claim-message").build(source,{guild,channel,user,ticket,reason})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketClaimed").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason} = params diff --git a/src/actions/closeTicket.ts b/src/actions/closeTicket.ts index 4e79f1f..1db612f 100644 --- a/src/actions/closeTicket.ts +++ b/src/actions/closeTicket.ts @@ -128,6 +128,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:close-message").build(source,{guild,channel,user,ticket,reason})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketClosed").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason} = params diff --git a/src/actions/moveTicket.ts b/src/actions/moveTicket.ts index 5a9ef35..a5d3a64 100644 --- a/src/actions/moveTicket.ts +++ b/src/actions/moveTicket.ts @@ -187,6 +187,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:move-message").build(source,{guild,channel,user,ticket,reason,data})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketMoved").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason,data} = params diff --git a/src/actions/pinTicket.ts b/src/actions/pinTicket.ts index 0ac9446..ddfea98 100644 --- a/src/actions/pinTicket.ts +++ b/src/actions/pinTicket.ts @@ -61,6 +61,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:pin-message").build(source,{guild,channel,user,ticket,reason})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketPinned").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason} = params diff --git a/src/actions/removeTicketUser.ts b/src/actions/removeTicketUser.ts index 489ec8b..9a8ef9f 100644 --- a/src/actions/removeTicketUser.ts +++ b/src/actions/removeTicketUser.ts @@ -48,6 +48,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:remove-message").build(source,{guild,channel,user,ticket,reason,data})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketUserRemoved").emit([ticket,user,data,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason,data} = params diff --git a/src/actions/renameTicket.ts b/src/actions/renameTicket.ts index ee0a722..47cef4c 100644 --- a/src/actions/renameTicket.ts +++ b/src/actions/renameTicket.ts @@ -45,6 +45,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:rename-message").build(source,{guild,channel,user,ticket,reason,data})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketRenamed").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason,data} = params diff --git a/src/actions/reopenTicket.ts b/src/actions/reopenTicket.ts index ecfe5e9..511eb37 100644 --- a/src/actions/reopenTicket.ts +++ b/src/actions/reopenTicket.ts @@ -154,6 +154,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:reopen-message").build(source,{guild,channel,user,ticket,reason})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketReopened").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason} = params diff --git a/src/actions/unclaimTicket.ts b/src/actions/unclaimTicket.ts index 0d8d8d8..6a04c1e 100644 --- a/src/actions/unclaimTicket.ts +++ b/src/actions/unclaimTicket.ts @@ -94,6 +94,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:unclaim-message").build(source,{guild,channel,user,ticket,reason})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketUnclaimed").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason} = params diff --git a/src/actions/unpinTicket.ts b/src/actions/unpinTicket.ts index 4274e05..6c608fb 100644 --- a/src/actions/unpinTicket.ts +++ b/src/actions/unpinTicket.ts @@ -54,6 +54,9 @@ export const registerActions = async () => { if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:unpin-message").build(source,{guild,channel,user,ticket,reason})).message) ticket.get("opendiscord:busy").value = false await opendiscord.events.get("afterTicketUnpinned").emit([ticket,user,channel,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket,reason} = params diff --git a/src/actions/updateTicketTopic.ts b/src/actions/updateTicketTopic.ts index 5a732cd..acc5d06 100644 --- a/src/actions/updateTicketTopic.ts +++ b/src/actions/updateTicketTopic.ts @@ -17,15 +17,20 @@ export const registerActions = async () => { ticket.get("opendiscord:busy").value = true if (newTopic) ticket.get("opendiscord:topic").value = newTopic + //get ticket data + const closed = ticket.get("opendiscord:closed").value + const claimedBy = ticket.get("opendiscord:claimed-by").value + const pinned = ticket.get("opendiscord:pinned").value + //handle channel topic const channelTopics: string[] = [] if (generalConfig.data.system.channelTopic.showOptionName) channelTopics.push(ticket.option.get("opendiscord:name").value) if (generalConfig.data.system.channelTopic.showOptionDescription) channelTopics.push(ticket.option.get("opendiscord:description").value) if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(ticket.get("opendiscord:topic").value) if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**Priority:** "+opendiscord.priorities.get("opendiscord:none").renderDisplayName()) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** Opened") //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** No-one") //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** No") //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** "+(closed ? "Closed" : "Opened")) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** "+(claimedBy ? discord.userMention(claimedBy) : "No-one")) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** "+(pinned ? "Yes" : "No")) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**Creator:** "+discord.userMention(user.id)) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**Participants:** "+ticket.get("opendiscord:participants").value.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //TODO TRANSLATION!!! From a40a9b8be85f89f2998f96707ac1d371f0abece7 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:40:49 +0100 Subject: [PATCH 66/78] Added channel topic command (/topic set) --- src/builders/messages.ts | 30 +++++++++++ src/commands/topic.ts | 83 ++++++++++++++++++++++++++++++ src/core/api/defaults/responder.ts | 1 + src/index.ts | 1 + 4 files changed, 115 insertions(+) create mode 100644 src/commands/topic.ts diff --git a/src/builders/messages.ts b/src/builders/messages.ts index 8d79f82..d08f283 100644 --- a/src/builders/messages.ts +++ b/src/builders/messages.ts @@ -24,6 +24,7 @@ export const registerAllMessages = async () => { roleMessages() clearMessages() autoMessages() + extraMessages() } const verifyBarMessages = () => { @@ -1072,4 +1073,33 @@ const autoMessages = () => { instance.addEmbed(await embeds.getSafe("opendiscord:autodelete-disable").build(source,{guild,channel,user,ticket,reason})) }) ) +} + +const extraMessages = () => { + //TOPIC SET + messages.add(new api.ODMessage("opendiscord:topic-set")) + messages.get("opendiscord:topic-set").workers.add( + new api.ODWorker("opendiscord:topic-set",0,async (instance,params,source) => { + const {guild,channel,user,ticket,topic} = params + instance.addEmbed(await embeds.getSafe("opendiscord:topic-set").build(source,{guild,channel,user,ticket,topic})) + }) + ) + + //PRIORITY SET + messages.add(new api.ODMessage("opendiscord:priority-set")) + messages.get("opendiscord:priority-set").workers.add( + new api.ODWorker("opendiscord:priority-set",0,async (instance,params,source) => { + const {guild,channel,user,ticket,priority,reason} = params + instance.addEmbed(await embeds.getSafe("opendiscord:priority-set").build(source,{guild,channel,user,ticket,priority,reason})) + }) + ) + + //PRIORITY GET + messages.add(new api.ODMessage("opendiscord:priority-get")) + messages.get("opendiscord:priority-get").workers.add( + new api.ODWorker("opendiscord:priority-get",0,async (instance,params,source) => { + const {guild,channel,user,ticket,priority} = params + instance.addEmbed(await embeds.getSafe("opendiscord:priority-get").build(source,{guild,channel,user,ticket,priority})) + }) + ) } \ No newline at end of file diff --git a/src/commands/topic.ts b/src/commands/topic.ts new file mode 100644 index 0000000..f8423c7 --- /dev/null +++ b/src/commands/topic.ts @@ -0,0 +1,83 @@ +/////////////////////////////////////// +//TOPIC COMMAND +/////////////////////////////////////// +import {opendiscord, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = opendiscord.configs.get("opendiscord:general") + +export const registerCommandResponders = async () => { + //TOPIC COMMAND RESPONDER + opendiscord.responders.commands.add(new api.ODCommandResponder("opendiscord:topic",generalConfig.data.prefix,"topic")) + opendiscord.responders.commands.get("opendiscord:topic").workers.add([ + new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.topic + + if (permissionMode == "none"){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await opendiscord.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("opendiscord:topic",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = opendiscord.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("opendiscord:busy").value){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const scope = instance.options.getSubCommand() + if (!scope || (scope != "set")) return + + if (scope == "set"){ + const topic = instance.options.getString("topic",true) + //start changing ticket topic + await instance.defer(false) + await opendiscord.actions.get("opendiscord:update-ticket-topic").run(source,{guild,channel,user,ticket,newTopic:topic,sendMessage:false}) + await instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:topic-set").build(source,{guild,channel,user,ticket,topic})) + } + }), + new api.ODWorker("opendiscord:logs",-1,(instance,params,source,cancel) => { + opendiscord.log(instance.user.displayName+" used the 'topic set' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/core/api/defaults/responder.ts b/src/core/api/defaults/responder.ts index ffe8cb4..d445249 100644 --- a/src/core/api/defaults/responder.ts +++ b/src/core/api/defaults/responder.ts @@ -44,6 +44,7 @@ export interface ODCommandResponderManagerIds_Default { "opendiscord:add":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:add"|"opendiscord:logs"}, "opendiscord:remove":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:remove"|"opendiscord:logs"}, "opendiscord:clear":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:clear"|"opendiscord:logs"}, + "opendiscord:topic":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:topic"|"opendiscord:logs"}, "opendiscord:autoclose":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:autoclose"|"opendiscord:logs"}, "opendiscord:autodelete":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:autodelete"|"opendiscord:logs"}, diff --git a/src/index.ts b/src/index.ts index bf29092..0170f4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -671,6 +671,7 @@ const main = async () => { await (await import("./commands/clear.js")).registerCommandResponders() await (await import("./commands/autoclose.js")).registerCommandResponders() await (await import("./commands/autodelete.js")).registerCommandResponders() + await (await import("./commands/topic.js")).registerCommandResponders() } await opendiscord.events.get("onCommandResponderLoad").emit([opendiscord.responders.commands,opendiscord.responders,opendiscord.actions]) await opendiscord.events.get("afterCommandRespondersLoaded").emit([opendiscord.responders.commands,opendiscord.responders,opendiscord.actions]) From 34ed4edede3c87cd2f65f1450be867d08d541d7e Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:41:46 +0100 Subject: [PATCH 67/78] Added priority commands (/priority get,set) --- src/actions/pinTicket.ts | 5 +- src/actions/renameTicket.ts | 6 +- src/actions/unpinTicket.ts | 19 +++--- src/actions/updateTicketPriority.ts | 61 ++++++++++++++++++ src/actions/updateTicketTopic.ts | 6 +- src/builders/embeds.ts | 4 +- src/commands/priority.ts | 96 +++++++++++++++++++++++++++++ src/core/api/defaults/action.ts | 7 +++ src/core/api/defaults/builder.ts | 4 +- src/core/api/defaults/event.ts | 8 ++- src/core/api/defaults/responder.ts | 1 + src/core/api/openticket/priority.ts | 9 +++ src/core/startup/init.ts | 9 ++- src/data/framework/eventLoader.ts | 4 ++ src/index.ts | 2 + 15 files changed, 222 insertions(+), 19 deletions(-) create mode 100644 src/actions/updateTicketPriority.ts create mode 100644 src/commands/priority.ts diff --git a/src/actions/pinTicket.ts b/src/actions/pinTicket.ts index ddfea98..a388ae0 100644 --- a/src/actions/pinTicket.ts +++ b/src/actions/pinTicket.ts @@ -31,8 +31,11 @@ export const registerActions = async () => { } //rename channel (and give error when crashed) + const pinEmoji = ticket.get("opendiscord:pinned").value ? generalConfig.data.system.pinEmoji : "" + const priorityEmoji = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).channelEmoji ?? "" + const originalName = channel.name - const newName = "📌"+channel.name + const newName = pinEmoji+priorityEmoji+utilities.trimEmojis(channel.name) try{ await utilities.timedAwait(channel.setName(newName),2500,(err) => { opendiscord.log("Failed to rename channel on ticket pin","error") diff --git a/src/actions/renameTicket.ts b/src/actions/renameTicket.ts index 47cef4c..a110f16 100644 --- a/src/actions/renameTicket.ts +++ b/src/actions/renameTicket.ts @@ -16,9 +16,13 @@ export const registerActions = async () => { await opendiscord.events.get("onTicketRename").emit([ticket,user,channel,reason]) //rename channel (and give error when crashed) + const pinEmoji = ticket.get("opendiscord:pinned").value ? generalConfig.data.system.pinEmoji : "" + const priorityEmoji = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).channelEmoji ?? "" + const originalName = channel.name + const newName = pinEmoji+priorityEmoji+utilities.trimEmojis(data) try{ - await utilities.timedAwait(channel.setName(data),2500,(err) => { + await utilities.timedAwait(channel.setName(newName),2500,(err) => { opendiscord.log("Failed to rename channel on ticket rename","error") }) }catch(err){ diff --git a/src/actions/unpinTicket.ts b/src/actions/unpinTicket.ts index 6c608fb..931ef38 100644 --- a/src/actions/unpinTicket.ts +++ b/src/actions/unpinTicket.ts @@ -22,16 +22,17 @@ export const registerActions = async () => { ticket.get("opendiscord:busy").value = true //rename channel (and give error when crashed) + const pinEmoji = ticket.get("opendiscord:pinned").value ? generalConfig.data.system.pinEmoji : "" + const priorityEmoji = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).channelEmoji ?? "" + const originalName = channel.name - if (originalName.startsWith("📌")){ - const newName = originalName.replace("📌",""); - try{ - await utilities.timedAwait(channel.setName(newName),2500,(err) => { - opendiscord.log("Failed to rename channel on ticket unpin","error") - }) - }catch(err){ - await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:error-channel-rename").build("ticket-unpin",{guild,channel,user,originalName,newName})).message) - } + const newName = pinEmoji+priorityEmoji+utilities.trimEmojis(channel.name) + try{ + await utilities.timedAwait(channel.setName(newName),2500,(err) => { + opendiscord.log("Failed to rename channel on ticket unpin","error") + }) + }catch(err){ + await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:error-channel-rename").build("ticket-unpin",{guild,channel,user,originalName,newName})).message) } //update ticket message diff --git a/src/actions/updateTicketPriority.ts b/src/actions/updateTicketPriority.ts new file mode 100644 index 0000000..50c16d0 --- /dev/null +++ b/src/actions/updateTicketPriority.ts @@ -0,0 +1,61 @@ +/////////////////////////////////////// +//TICKET TOPIC SYSTEM +/////////////////////////////////////// +import {opendiscord, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = opendiscord.configs.get("opendiscord:general") + +export const registerActions = async () => { + opendiscord.actions.add(new api.ODAction("opendiscord:update-ticket-priority")) + opendiscord.actions.get("opendiscord:update-ticket-priority").workers.add([ + new api.ODWorker("opendiscord:update-ticket-priority",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,newPriority,reason} = params + if (channel.isThread() || !(channel instanceof discord.TextChannel)) throw new api.ODSystemError("Unable to set priority of ticket! Open Ticket doesn't support threads!") + + const oldPriority = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value) + await opendiscord.events.get("onTicketPriorityChange").emit([ticket,user,channel,oldPriority,newPriority]) + + //update ticket + ticket.get("opendiscord:busy").value = true + if (newPriority) ticket.get("opendiscord:priority").value = newPriority.priority + + //rename channel (and give error when crashed) + const pinEmoji = ticket.get("opendiscord:pinned").value ? generalConfig.data.system.pinEmoji : "" + const priorityEmoji = newPriority.channelEmoji ?? "" + + const originalName = channel.name + const newName = pinEmoji+priorityEmoji+utilities.trimEmojis(channel.name) + try{ + await utilities.timedAwait(channel.setName(newName),2500,(err) => { + opendiscord.log("Failed to rename channel on ticket priority update","error") + }) + }catch(err){ + await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:error-channel-rename").build("ticket-priority",{guild,channel,user,originalName,newName})).message) + } + + //reply with new message + if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:priority-set").build(source,{guild,channel,user,ticket,priority:newPriority,reason})).message) + ticket.get("opendiscord:busy").value = false + await opendiscord.events.get("afterTicketPriorityChanged").emit([ticket,user,channel,oldPriority,newPriority]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) + }), + new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket} = params + }), + new api.ODWorker("opendiscord:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket,newPriority} = params + + opendiscord.log(user.displayName+" changed the priority of a ticket!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"priority",value:newPriority.id.value}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/actions/updateTicketTopic.ts b/src/actions/updateTicketTopic.ts index acc5d06..f01637b 100644 --- a/src/actions/updateTicketTopic.ts +++ b/src/actions/updateTicketTopic.ts @@ -13,6 +13,9 @@ export const registerActions = async () => { const {guild,channel,user,ticket,newTopic} = params if (channel.isThread() || !(channel instanceof discord.TextChannel)) throw new api.ODSystemError("Unable to set topic of ticket! Open Ticket doesn't support threads!") + const oldTopic = ticket.get("opendiscord:topic").value + if (newTopic) await opendiscord.events.get("onTicketTopicChange").emit([ticket,user,channel,oldTopic,newTopic]) + //update ticket ticket.get("opendiscord:busy").value = true if (newTopic) ticket.get("opendiscord:topic").value = newTopic @@ -27,7 +30,7 @@ export const registerActions = async () => { if (generalConfig.data.system.channelTopic.showOptionName) channelTopics.push(ticket.option.get("opendiscord:name").value) if (generalConfig.data.system.channelTopic.showOptionDescription) channelTopics.push(ticket.option.get("opendiscord:description").value) if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(ticket.get("opendiscord:topic").value) - if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**Priority:** "+opendiscord.priorities.get("opendiscord:none").renderDisplayName()) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**Priority:** "+opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).renderDisplayName()) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** "+(closed ? "Closed" : "Opened")) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** "+(claimedBy ? discord.userMention(claimedBy) : "No-one")) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** "+(pinned ? "Yes" : "No")) //TODO TRANSLATION!!! @@ -40,6 +43,7 @@ export const registerActions = async () => { //reply with new message if (params.sendMessage && newTopic) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:topic-set").build(source,{guild,channel,user,ticket,topic:newTopic})).message) ticket.get("opendiscord:busy").value = false + if (newTopic) await opendiscord.events.get("afterTicketTopicChanged").emit([ticket,user,channel,oldTopic,newTopic]) }), new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { const {guild,channel,user,ticket} = params diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index 52c2b12..dfa5992 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -330,7 +330,7 @@ const errorEmbeds = () => { new api.ODWorker("opendiscord:error-channel-rename",0,async (instance,params,source) => { const {channel,user,originalName,newName} = params - const method = (source == "ticket-move" || source == "ticket-pin" || source == "ticket-rename" || source == "ticket-unpin") ? source : getMethodFromSource(source) + const method = (source == "ticket-move" || source == "ticket-pin" || source == "ticket-rename" || source == "ticket-unpin" || source == "ticket-priority") ? source : getMethodFromSource(source) instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.channelRename"))) @@ -1349,7 +1349,7 @@ const extraEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("🚨","Priority Changed")) //TODO TRANSLATION!!! + instance.setTitle(utilities.emojiTitle("🚨","Ticket Priority")) //TODO TRANSLATION!!! instance.setDescription("The current priority of this ticket is **"+priority.renderDisplayName()+"**!") //TODO TRANSLATION!!! }) ) diff --git a/src/commands/priority.ts b/src/commands/priority.ts new file mode 100644 index 0000000..cbf6744 --- /dev/null +++ b/src/commands/priority.ts @@ -0,0 +1,96 @@ +/////////////////////////////////////// +//PRIORITY COMMAND +/////////////////////////////////////// +import {opendiscord, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = opendiscord.configs.get("opendiscord:general") + +export const registerCommandResponders = async () => { + //PRIORITY COMMAND RESPONDER + opendiscord.responders.commands.add(new api.ODCommandResponder("opendiscord:priority",generalConfig.data.prefix,"priority")) + opendiscord.responders.commands.get("opendiscord:priority").workers.add([ + new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.priority + + if (permissionMode == "none"){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await opendiscord.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("opendiscord:priority",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = opendiscord.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("opendiscord:busy").value){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const scope = instance.options.getSubCommand() + if (!scope || (scope != "set" && scope != "get")) return + + if (scope == "set"){ + const priorityName = instance.options.getString("priority",true) + const reason = instance.options.getString("reason",false) + + const priority = opendiscord.priorities.getAll().find((lvl) => lvl.rawName === priorityName) ?? null + if (!priority){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"Please provide a valid priority level.",customTitle:"Unknown Priority Level"})) + return cancel() + } + + //start changing ticket priority + await instance.defer(false) + await opendiscord.actions.get("opendiscord:update-ticket-priority").run(source,{guild,channel,user,ticket,newPriority:priority,sendMessage:false,reason}) + await instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:priority-set").build(source,{guild,channel,user,ticket,priority,reason})) + + }else if (scope == "get"){ + const priority = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value) + await instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:priority-get").build(source,{guild,channel,user,ticket,priority})) + } + }), + new api.ODWorker("opendiscord:logs",-1,(instance,params,source,cancel) => { + const scope = instance.options.getSubCommand() + opendiscord.log(instance.user.displayName+" used the 'priority "+scope+"' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/core/api/defaults/action.ts b/src/core/api/defaults/action.ts index 7b0d01a..a3387ed 100644 --- a/src/core/api/defaults/action.ts +++ b/src/core/api/defaults/action.ts @@ -10,6 +10,7 @@ import { ODTicket, ODTicketClearFilter } from "../openticket/ticket" import { ODTranscriptCompiler, ODTranscriptCompilerCompileResult } from "../openticket/transcript" import { ODMessageBuildSentResult } from "../modules/builder" import { ODRole, ODRoleUpdateMode, ODRoleUpdateResult } from "../openticket/role" +import { ODPriorityLevel } from "../openticket/priority" /**## ODActionManagerIds_Default `interface` * This interface is a list of ids available in the `ODActionManager_Default` class. @@ -118,6 +119,12 @@ export interface ODActionManagerIds_Default { result:{}, workers:"opendiscord:update-ticket-topic"|"opendiscord:discord-logs"|"opendiscord:logs" }, + "opendiscord:update-ticket-priority":{ + source:"slash"|"text"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,newPriority:ODPriorityLevel,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"opendiscord:update-ticket-priority"|"opendiscord:discord-logs"|"opendiscord:logs" + }, } /**## ODActionManager_Default `default_class` diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index 1702e6f..39e3b02 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -239,7 +239,7 @@ export interface ODEmbedManagerIds_Default { "opendiscord:error-option-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-option-unknown"}, "opendiscord:error-panel-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-panel-unknown"}, "opendiscord:error-not-in-guild":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-not-in-guild"}, - "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, + "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"ticket-priority"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, "opendiscord:error-ticket-busy":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-ticket-busy"}, "opendiscord:help-menu":{source:"text"|"slash"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"opendiscord:help-menu"}, @@ -375,7 +375,7 @@ export interface ODMessageManagerIds_Default { "opendiscord:error-option-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-option-unknown"}, "opendiscord:error-panel-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-panel-unknown"}, "opendiscord:error-not-in-guild":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-not-in-guild"}, - "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, + "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"ticket-priority"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, "opendiscord:error-ticket-busy":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-ticket-busy"}, "opendiscord:help-menu":{source:"slash"|"text"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"opendiscord:help-menu"}, diff --git a/src/core/api/defaults/event.ts b/src/core/api/defaults/event.ts index 169c757..3315d08 100644 --- a/src/core/api/defaults/event.ts +++ b/src/core/api/defaults/event.ts @@ -42,7 +42,7 @@ import { ODQuestionManager } from "../openticket/question" import { ODBlacklistManager } from "../openticket/blacklist" import { ODTranscriptManager_Default } from "../openticket/transcript" import { ODRole, ODRoleManager } from "../openticket/role" -import { ODPriorityManager_Default } from "../openticket/priority" +import { ODPriorityLevel, ODPriorityManager_Default } from "../openticket/priority" /**## ODEventIds_Default `interface` * This interface is a list of ids available in the `ODEvent_Default` class. @@ -202,7 +202,11 @@ export interface ODEventIds_Default { "afterTicketRenamed": ODEvent_Default<(ticket:ODTicket, renamer:discord.User, channel:discord.GuildTextBasedChannel, reason:string|null) => ODPromiseVoid> "onTicketsClear": ODEvent_Default<(tickets:ODTicket[], clearer:discord.User, channel:discord.GuildTextBasedChannel, filter:ODTicketClearFilter) => ODPromiseVoid> "afterTicketsCleared": ODEvent_Default<(tickets:ODTicket[], clearer:discord.User, channel:discord.GuildTextBasedChannel, filter:ODTicketClearFilter) => ODPromiseVoid> - + "onTicketTopicChange": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldTopic:string, newTopic:string) => ODPromiseVoid> + "afterTicketTopicChanged": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldTopic:string, newTopic:string) => ODPromiseVoid> + "onTicketPriorityChange": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldPriority:ODPriorityLevel, newPriority:ODPriorityLevel) => ODPromiseVoid> + "afterTicketPriorityChanged": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldPriority:ODPriorityLevel, newPriority:ODPriorityLevel) => ODPromiseVoid> + //roles "onRoleLoad": ODEvent_Default<(roles:ODRoleManager) => ODPromiseVoid> "afterRolesLoaded": ODEvent_Default<(roles:ODRoleManager) => ODPromiseVoid> diff --git a/src/core/api/defaults/responder.ts b/src/core/api/defaults/responder.ts index d445249..6023033 100644 --- a/src/core/api/defaults/responder.ts +++ b/src/core/api/defaults/responder.ts @@ -45,6 +45,7 @@ export interface ODCommandResponderManagerIds_Default { "opendiscord:remove":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:remove"|"opendiscord:logs"}, "opendiscord:clear":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:clear"|"opendiscord:logs"}, "opendiscord:topic":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:topic"|"opendiscord:logs"}, + "opendiscord:priority":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:priority"|"opendiscord:logs"}, "opendiscord:autoclose":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:autoclose"|"opendiscord:logs"}, "opendiscord:autodelete":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:autodelete"|"opendiscord:logs"}, diff --git a/src/core/api/openticket/priority.ts b/src/core/api/openticket/priority.ts index 300f61d..90ea38a 100644 --- a/src/core/api/openticket/priority.ts +++ b/src/core/api/openticket/priority.ts @@ -20,6 +20,15 @@ export class ODPriorityManager extends ODManager { super(debug,"priority") this.#debug = debug } + + /**Get an `ODPriorityLevel` from the priority level value. Returns a dummy value when the level doesn't exist. */ + getFromPriorityLevel(level:number){ + return this.getAll().find((lvl) => lvl.priority === level) ?? new ODPriorityLevel("opendiscord:unknown",0,"unknown","UNKNOWN_PRIORITY","🚫","🚫") + } + /**List the available priority levels. */ + listAvailableLevels(){ + return this.getAll().map((lvl) => lvl.priority) + } } /**## ODPriorityManagerIds `type` diff --git a/src/core/startup/init.ts b/src/core/startup/init.ts index 6d1478d..8287218 100644 --- a/src/core/startup/init.ts +++ b/src/core/startup/init.ts @@ -122,6 +122,10 @@ export interface ODUtilities { * Get a human readable ordinal number (e.g. 1st, 2nd, 3rd, 4th, ...) from a Javascript number. */ ordinalNumber(num:number): string, + /**## trimEmojis `utility function` + * Trim/remove all emoji's from a Javascript string. + */ + trimEmojis(text:string): string, } /**## ODVersionMigration `utility class` @@ -268,5 +272,8 @@ export const utilities: ODUtilities = { if (dec === 2) return i+'nd' if (dec === 3) return i+'rd' return i+'th' - } + }, + trimEmojis(text){ + return text.replace(/(\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?)*)/gu,"") + }, } \ No newline at end of file diff --git a/src/data/framework/eventLoader.ts b/src/data/framework/eventLoader.ts index c7c0dd6..257aa5e 100644 --- a/src/data/framework/eventLoader.ts +++ b/src/data/framework/eventLoader.ts @@ -155,6 +155,10 @@ export const loadAllEvents = () => { "afterTicketRenamed", "onTicketsClear", "afterTicketsCleared", + "onTicketTopicChange", + "afterTicketTopicChanged", + "onTicketPriorityChange", + "afterTicketPriorityChanged", //roles "onRoleLoad", diff --git a/src/index.ts b/src/index.ts index 0170f4c..39f29fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -672,6 +672,7 @@ const main = async () => { await (await import("./commands/autoclose.js")).registerCommandResponders() await (await import("./commands/autodelete.js")).registerCommandResponders() await (await import("./commands/topic.js")).registerCommandResponders() + await (await import("./commands/priority.js")).registerCommandResponders() } await opendiscord.events.get("onCommandResponderLoad").emit([opendiscord.responders.commands,opendiscord.responders,opendiscord.actions]) await opendiscord.events.get("afterCommandRespondersLoaded").emit([opendiscord.responders.commands,opendiscord.responders,opendiscord.actions]) @@ -759,6 +760,7 @@ const main = async () => { await (await import("./actions/reactionRole.js")).registerActions() await (await import("./actions/clearTickets.js")).registerActions() await (await import("./actions/updateTicketTopic.js")).registerActions() + await (await import("./actions/updateTicketPriority.js")).registerActions() } await opendiscord.events.get("onActionLoad").emit([opendiscord.actions]) await opendiscord.events.get("afterActionsLoaded").emit([opendiscord.actions]) From 68a8170570165cc867889ef2e6d277c8078d0af1 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 2 Nov 2025 20:08:57 +0100 Subject: [PATCH 68/78] Minor bugfixes & improvements (priority & topic) --- src/actions/moveTicket.ts | 14 +++++++------- src/actions/updateTicketPriority.ts | 8 ++++++-- src/actions/updateTicketTopic.ts | 7 ++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/actions/moveTicket.ts b/src/actions/moveTicket.ts index a5d3a64..591aef6 100644 --- a/src/actions/moveTicket.ts +++ b/src/actions/moveTicket.ts @@ -29,7 +29,6 @@ export const registerActions = async () => { const claimCategory = (rawClaimCategory) ? rawClaimCategory.category : null const closeCategory = ticket.option.get("opendiscord:channel-category-closed").value const channelTopic = ticket.option.get("opendiscord:channel-topic").value - const channelName = channelPrefix+channelSuffix //handle category let category: string|null = null @@ -155,17 +154,18 @@ export const registerActions = async () => { ticket.get("opendiscord:participants").refreshDatabase() //rename channel (and give error when crashed) + const pinEmoji = ticket.get("opendiscord:pinned").value ? generalConfig.data.system.pinEmoji : "" + const priorityEmoji = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).channelEmoji ?? "" + const originalName = channel.name + const newName = pinEmoji+priorityEmoji+utilities.trimEmojis(channelPrefix+channelSuffix) try{ - await utilities.timedAwait(channel.setName(channelName),2500,(err) => { + await utilities.timedAwait(channel.setName(newName),2500,(err) => { opendiscord.log("Failed to rename channel on ticket move","error") }) }catch(err){ - await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:error-channel-rename").build("ticket-move",{guild,channel,user,originalName,newName:channelName})).message) + await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:error-channel-rename").build("ticket-move",{guild,channel,user,originalName,newName:newName})).message) } - try{ - if (channel.type == discord.ChannelType.GuildText) channel.setTopic(channelTopic) - }catch{} //update ticket message const ticketMessage = await opendiscord.tickets.getTicketMessage(ticket) @@ -173,7 +173,7 @@ export const registerActions = async () => { try{ ticketMessage.edit((await opendiscord.builders.messages.getSafe("opendiscord:ticket-message").build("other",{guild,channel,user,ticket})).message) }catch(e){ - opendiscord.log("Unable to edit ticket message on ticket renaming!","error",[ + opendiscord.log("Unable to edit ticket message on ticket moving!","error",[ {key:"channel",value:"#"+channel.name}, {key:"channelid",value:channel.id,hidden:true}, {key:"messageid",value:ticketMessage.id}, diff --git a/src/actions/updateTicketPriority.ts b/src/actions/updateTicketPriority.ts index 50c16d0..de328a7 100644 --- a/src/actions/updateTicketPriority.ts +++ b/src/actions/updateTicketPriority.ts @@ -14,7 +14,7 @@ export const registerActions = async () => { if (channel.isThread() || !(channel instanceof discord.TextChannel)) throw new api.ODSystemError("Unable to set priority of ticket! Open Ticket doesn't support threads!") const oldPriority = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value) - await opendiscord.events.get("onTicketPriorityChange").emit([ticket,user,channel,oldPriority,newPriority]) + await opendiscord.events.get("onTicketPriorityChange").emit([ticket,user,channel,oldPriority,newPriority,reason]) //update ticket ticket.get("opendiscord:busy").value = true @@ -37,7 +37,7 @@ export const registerActions = async () => { //reply with new message if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:priority-set").build(source,{guild,channel,user,ticket,priority:newPriority,reason})).message) ticket.get("opendiscord:busy").value = false - await opendiscord.events.get("afterTicketPriorityChanged").emit([ticket,user,channel,oldPriority,newPriority]) + await opendiscord.events.get("afterTicketPriorityChanged").emit([ticket,user,channel,oldPriority,newPriority,reason]) //update channel topic await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) @@ -58,4 +58,8 @@ export const registerActions = async () => { ]) }) ]) + opendiscord.actions.get("opendiscord:update-ticket-priority").workers.backupWorker = new api.ODWorker("opendiscord:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("opendiscord:busy").value = false + }) } \ No newline at end of file diff --git a/src/actions/updateTicketTopic.ts b/src/actions/updateTicketTopic.ts index f01637b..d974166 100644 --- a/src/actions/updateTicketTopic.ts +++ b/src/actions/updateTicketTopic.ts @@ -24,6 +24,7 @@ export const registerActions = async () => { const closed = ticket.get("opendiscord:closed").value const claimedBy = ticket.get("opendiscord:claimed-by").value const pinned = ticket.get("opendiscord:pinned").value + const creator = ticket.get("opendiscord:opened-by").value ?? opendiscord.client.client.user.id //handle channel topic const channelTopics: string[] = [] @@ -34,7 +35,7 @@ export const registerActions = async () => { if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** "+(closed ? "Closed" : "Opened")) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** "+(claimedBy ? discord.userMention(claimedBy) : "No-one")) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** "+(pinned ? "Yes" : "No")) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**Creator:** "+discord.userMention(user.id)) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**Creator:** "+discord.userMention(creator)) //TODO TRANSLATION!!! if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**Participants:** "+ticket.get("opendiscord:participants").value.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //TODO TRANSLATION!!! //update channel @@ -61,4 +62,8 @@ export const registerActions = async () => { ]) }) ]) + opendiscord.actions.get("opendiscord:update-ticket-topic").workers.backupWorker = new api.ODWorker("opendiscord:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("opendiscord:busy").value = false + }) } \ No newline at end of file From c7b44232c421ce896a7c57370c78799b14610f64 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 2 Nov 2025 20:09:38 +0100 Subject: [PATCH 69/78] Added transfer command (/transfer) + new stats With this command you're able to transfer the ticket ownership to another person in the server. --- src/actions/transferTicket.ts | 154 ++++++++++++++++++++++++++++ src/builders/embeds.ts | 16 ++- src/builders/messages.ts | 9 ++ src/commands/transfer.ts | 81 +++++++++++++++ src/core/api/defaults/action.ts | 6 ++ src/core/api/defaults/builder.ts | 6 +- src/core/api/defaults/client.ts | 2 + src/core/api/defaults/event.ts | 6 +- src/core/api/defaults/responder.ts | 1 + src/core/api/defaults/stat.ts | 2 + src/data/framework/commandLoader.ts | 45 ++++++++ src/data/framework/eventLoader.ts | 2 + src/data/framework/statLoader.ts | 34 +++--- src/index.ts | 2 + 14 files changed, 345 insertions(+), 21 deletions(-) create mode 100644 src/actions/transferTicket.ts create mode 100644 src/commands/transfer.ts diff --git a/src/actions/transferTicket.ts b/src/actions/transferTicket.ts new file mode 100644 index 0000000..85887dc --- /dev/null +++ b/src/actions/transferTicket.ts @@ -0,0 +1,154 @@ +/////////////////////////////////////// +//TICKET TRANSFER SYSTEM +/////////////////////////////////////// +import {opendiscord, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = opendiscord.configs.get("opendiscord:general") + +export const registerActions = async () => { + opendiscord.actions.add(new api.ODAction("opendiscord:transfer-ticket")) + opendiscord.actions.get("opendiscord:transfer-ticket").workers.add([ + new api.ODWorker("opendiscord:transfer-ticket",2,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,reason,newCreator} = params + if (channel.isThread()) throw new api.ODSystemError("Unable to transfer ticket! Open Ticket doesn't support threads!") + + const oldCreator = await opendiscord.tickets.getTicketUser(ticket,"creator") ?? opendiscord.client.client.user + await opendiscord.events.get("onTicketTransfer").emit([ticket,user,channel,oldCreator,newCreator,reason]) + + //update ticket + const oldCreatorId = ticket.get("opendiscord:opened-by").value + if (oldCreatorId){ + ticket.get("opendiscord:previous-creators").value.push(oldCreatorId) + ticket.get("opendiscord:previous-creators").refreshDatabase() + } + if (!ticket.get("opendiscord:participants").value.find((p) => p.type == "user" && p.id == newCreator.id)){ + ticket.get("opendiscord:participants").value.push({type:"user",id:newCreator.id}) + ticket.get("opendiscord:participants").refreshDatabase() + } + ticket.get("opendiscord:opened-by").value = newCreator.id + if (["user-name","user-nickname","user-id"].includes(ticket.option.get("opendiscord:channel-suffix").value)){ + const newSuffix = await opendiscord.options.suffix.getSuffixFromOption(ticket.option,newCreator,guild) + if (newSuffix) ticket.get("opendiscord:channel-suffix").value = newSuffix + } + + //update stats + await opendiscord.stats.get("opendiscord:global").setStat("opendiscord:tickets-transferred",1,"increase") + await opendiscord.stats.get("opendiscord:user").setStat("opendiscord:tickets-transferred",user.id,1,"increase") + + //get new channel properties + const channelPrefix = ticket.option.get("opendiscord:channel-prefix").value + const channelSuffix = ticket.get("opendiscord:channel-suffix").value + + //handle permissions + const permissions: discord.OverwriteResolvable[] = [{ + type:discord.OverwriteType.Role, + id:guild.roles.everyone.id, + allow:[], + deny:["ViewChannel","SendMessages","ReadMessageHistory"] + }] + const globalAdmins = opendiscord.configs.get("opendiscord:general").data.globalAdmins + const optionAdmins = ticket.option.get("opendiscord:admins").value + const readonlyAdmins = ticket.option.get("opendiscord:admins-readonly").value + + globalAdmins.forEach((admin) => { + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + optionAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory","ManageMessages"], + deny:[] + }) + }) + readonlyAdmins.forEach((admin) => { + if (globalAdmins.includes(admin)) return + if (optionAdmins.includes(admin)) return + permissions.push({ + type:discord.OverwriteType.Role, + id:admin, + allow:["ViewChannel","ReadMessageHistory"], + deny:["SendMessages","AddReactions","AttachFiles","SendPolls"] + }) + }) + //transfer all old user-participants over to the new ticket (creator & participants) + ticket.get("opendiscord:participants").value.forEach((p) => { + if (p.type == "user") permissions.push({ + type:discord.OverwriteType.Member, + id:p.id, + allow:["ViewChannel","SendMessages","AddReactions","AttachFiles","SendPolls","ReadMessageHistory"], + deny:[] + }) + }) + try{ + await channel.permissionOverwrites.set(permissions) + }catch{ + opendiscord.log("Failed to reset channel permissions on ticket transfer!","error") + } + + //rename channel (and give error when crashed) + const pinEmoji = ticket.get("opendiscord:pinned").value ? generalConfig.data.system.pinEmoji : "" + const priorityEmoji = opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).channelEmoji ?? "" + + const originalName = channel.name + const newName = pinEmoji+priorityEmoji+utilities.trimEmojis(channelPrefix+channelSuffix) + try{ + await utilities.timedAwait(channel.setName(newName),2500,(err) => { + opendiscord.log("Failed to rename channel on ticket transfer","error") + }) + }catch(err){ + await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:error-channel-rename").build("ticket-transfer",{guild,channel,user,originalName,newName:newName})).message) + } + + //update ticket message + const ticketMessage = await opendiscord.tickets.getTicketMessage(ticket) + if (ticketMessage){ + try{ + ticketMessage.edit((await opendiscord.builders.messages.getSafe("opendiscord:ticket-message").build("other",{guild,channel,user,ticket})).message) + }catch(e){ + opendiscord.log("Unable to edit ticket message on ticket transferring!","error",[ + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"messageid",value:ticketMessage.id}, + {key:"option",value:ticket.option.id.value} + ]) + opendiscord.debugfile.writeErrorMessage(new api.ODError(e,"uncaughtException")) + } + } + + //reply with new message + if (params.sendMessage) await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:transfer-message").build(source,{guild,channel,user,ticket,oldCreator,newCreator,reason})).message) + ticket.get("opendiscord:busy").value = false + await opendiscord.events.get("afterTicketTransferred").emit([ticket,user,channel,oldCreator,newCreator,reason]) + + //update channel topic + await opendiscord.actions.get("opendiscord:update-ticket-topic").run("ticket-action",{guild,channel,user,ticket,sendMessage:false,newTopic:null}) + }), + new api.ODWorker("opendiscord:discord-logs",1,async (instance,params,source,cancel) => { + const {guild,channel,user,ticket,newCreator,reason} = params + }), + new api.ODWorker("opendiscord:logs",0,(instance,params,source,cancel) => { + const {guild,channel,user,ticket,newCreator} = params + + opendiscord.log(user.displayName+" transferred a ticket to '"+newCreator.displayName+"'!","info",[ + {key:"user",value:user.username}, + {key:"userid",value:user.id,hidden:true}, + {key:"channel",value:"#"+channel.name}, + {key:"channelid",value:channel.id,hidden:true}, + {key:"reason",value:params.reason ?? "/"}, + {key:"method",value:source} + ]) + }) + ]) + opendiscord.actions.get("opendiscord:transfer-ticket").workers.backupWorker = new api.ODWorker("opendiscord:cancel-busy",0,(instance,params) => { + //set busy to false in case of crash or cancel + params.ticket.get("opendiscord:busy").value = false + }) +} \ No newline at end of file diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index dfa5992..efde0cf 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -330,7 +330,7 @@ const errorEmbeds = () => { new api.ODWorker("opendiscord:error-channel-rename",0,async (instance,params,source) => { const {channel,user,originalName,newName} = params - const method = (source == "ticket-move" || source == "ticket-pin" || source == "ticket-rename" || source == "ticket-unpin" || source == "ticket-priority") ? source : getMethodFromSource(source) + const method = (source == "ticket-move" || source == "ticket-pin" || source == "ticket-rename" || source == "ticket-unpin" || source == "ticket-priority" || source == "ticket-transfer") ? source : getMethodFromSource(source) instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("errors.titles.channelRename"))) @@ -1353,4 +1353,18 @@ const extraEmbeds = () => { instance.setDescription("The current priority of this ticket is **"+priority.renderDisplayName()+"**!") //TODO TRANSLATION!!! }) ) + + //TRANSFER MESSAGE + embeds.add(new api.ODEmbed("opendiscord:transfer-message")) + embeds.get("opendiscord:transfer-message").workers.add( + new api.ODWorker("opendiscord:transfer-message",0,async (instance,params,source) => { + const {user,oldCreator,newCreator,reason} = params + + instance.setAuthor(user.displayName,user.displayAvatarURL()) + instance.setColor(generalConfig.data.mainColor) + instance.setTitle(utilities.emojiTitle("🔀","Ticket Transferred")) //TODO TRANSLATION!!! + instance.setDescription("The ticket ownership has been transferred from "+discord.userMention(oldCreator.id)+" to "+discord.userMention(newCreator.id)+" by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! + if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + }) + ) } \ No newline at end of file diff --git a/src/builders/messages.ts b/src/builders/messages.ts index d08f283..0b89c1b 100644 --- a/src/builders/messages.ts +++ b/src/builders/messages.ts @@ -1102,4 +1102,13 @@ const extraMessages = () => { instance.addEmbed(await embeds.getSafe("opendiscord:priority-get").build(source,{guild,channel,user,ticket,priority})) }) ) + + //TRANSFER MESSAGE + messages.add(new api.ODMessage("opendiscord:transfer-message")) + messages.get("opendiscord:transfer-message").workers.add( + new api.ODWorker("opendiscord:transfer-message",0,async (instance,params,source) => { + const {guild,channel,user,ticket,oldCreator,newCreator,reason} = params + instance.addEmbed(await embeds.getSafe("opendiscord:transfer-message").build(source,{guild,channel,user,ticket,oldCreator,newCreator,reason})) + }) + ) } \ No newline at end of file diff --git a/src/commands/transfer.ts b/src/commands/transfer.ts new file mode 100644 index 0000000..18c8c8b --- /dev/null +++ b/src/commands/transfer.ts @@ -0,0 +1,81 @@ +/////////////////////////////////////// +//TRANSFER COMMAND +/////////////////////////////////////// +import {opendiscord, api, utilities} from "../index" +import * as discord from "discord.js" + +const generalConfig = opendiscord.configs.get("opendiscord:general") + +export const registerCommandResponders = async () => { + //TRANSFER COMMAND RESPONDER + opendiscord.responders.commands.add(new api.ODCommandResponder("opendiscord:transfer",generalConfig.data.prefix,"transfer")) + opendiscord.responders.commands.get("opendiscord:transfer").workers.add([ + new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { + const permissionMode = generalConfig.data.system.permissions.transfer + + if (permissionMode == "none"){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else if (permissionMode == "everyone") return + else if (permissionMode == "admin"){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild))){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + }else return + }else{ + if (!instance.guild || !instance.member){ + //error + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #1",layout:"advanced"})) + return cancel() + } + const role = await opendiscord.client.fetchGuildRole(instance.guild,permissionMode) + if (!role){ + //error + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Permission Error: Not in Server #2",layout:"advanced"})) + return cancel() + } + if (!role.members.has(instance.member.id)){ + //no permissions + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) + return cancel() + }else return + } + }), + new api.ODWorker("opendiscord:transfer",0,async (instance,params,source,cancel) => { + const {guild,channel,user} = instance + if (!guild){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-not-in-guild").build("button",{channel,user})) + return cancel() + } + const ticket = opendiscord.tickets.get(channel.id) + if (!ticket || channel.isDMBased()){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-unknown").build("button",{guild,channel,user})) + return cancel() + } + //return when busy + if (ticket.get("opendiscord:busy").value){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) + return cancel() + } + + const oldCreator = await opendiscord.tickets.getTicketUser(ticket,"creator") ?? opendiscord.client.client.user + const newCreator = instance.options.getUser("user",true) + const reason = instance.options.getString("reason",false) + + //start transferring ticket ownership + await instance.defer(false) + await opendiscord.actions.get("opendiscord:transfer-ticket").run(source,{guild,channel,user,ticket,reason,sendMessage:false,newCreator}) + await instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:transfer-message").build(source,{guild,channel,user,ticket,oldCreator,newCreator,reason})) + }), + new api.ODWorker("opendiscord:logs",-1,(instance,params,source,cancel) => { + opendiscord.log(instance.user.displayName+" used the 'transfer' command!","info",[ + {key:"user",value:instance.user.username}, + {key:"userid",value:instance.user.id,hidden:true}, + {key:"channelid",value:instance.channel.id,hidden:true}, + {key:"method",value:source} + ]) + }) + ]) +} \ No newline at end of file diff --git a/src/core/api/defaults/action.ts b/src/core/api/defaults/action.ts index a3387ed..c86269b 100644 --- a/src/core/api/defaults/action.ts +++ b/src/core/api/defaults/action.ts @@ -125,6 +125,12 @@ export interface ODActionManagerIds_Default { result:{}, workers:"opendiscord:update-ticket-priority"|"opendiscord:discord-logs"|"opendiscord:logs" }, + "opendiscord:transfer-ticket":{ + source:"slash"|"text"|"other", + params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,newCreator:discord.User,reason:string|null,sendMessage:boolean}, + result:{}, + workers:"opendiscord:transfer-ticket"|"opendiscord:discord-logs"|"opendiscord:logs" + }, } /**## ODActionManager_Default `default_class` diff --git a/src/core/api/defaults/builder.ts b/src/core/api/defaults/builder.ts index 39e3b02..96c652b 100644 --- a/src/core/api/defaults/builder.ts +++ b/src/core/api/defaults/builder.ts @@ -239,7 +239,7 @@ export interface ODEmbedManagerIds_Default { "opendiscord:error-option-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-option-unknown"}, "opendiscord:error-panel-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-panel-unknown"}, "opendiscord:error-not-in-guild":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-not-in-guild"}, - "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"ticket-priority"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, + "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"ticket-priority"|"ticket-transfer"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, "opendiscord:error-ticket-busy":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-ticket-busy"}, "opendiscord:help-menu":{source:"text"|"slash"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"opendiscord:help-menu"}, @@ -299,6 +299,7 @@ export interface ODEmbedManagerIds_Default { "opendiscord:topic-set":{source:"slash"|"text"|"ticket-action"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel,reason:string|null},workers:"opendiscord:priority-set"}, "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel},workers:"opendiscord:priority-get"}, + "opendiscord:transfer-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,oldCreator:discord.User,newCreator:discord.User,reason:string|null},workers:"opendiscord:transfer-message"}, } /**## ODEmbedManager_Default `default_class` @@ -375,7 +376,7 @@ export interface ODMessageManagerIds_Default { "opendiscord:error-option-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-option-unknown"}, "opendiscord:error-panel-unknown":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-panel-unknown"}, "opendiscord:error-not-in-guild":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-not-in-guild"}, - "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"ticket-priority"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, + "opendiscord:error-channel-rename":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"ticket-pin"|"ticket-unpin"|"ticket-rename"|"ticket-move"|"ticket-priority"|"ticket-transfer"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalName:string,newName:string},workers:"opendiscord:error-channel-rename"}, "opendiscord:error-ticket-busy":{source:"slash"|"text"|"button"|"dropdown"|"modal"|"other",params:{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User},workers:"opendiscord:error-ticket-busy"}, "opendiscord:help-menu":{source:"slash"|"text"|"button"|"other",params:{mode:"slash"|"text",page:number},workers:"opendiscord:help-menu"}, @@ -437,6 +438,7 @@ export interface ODMessageManagerIds_Default { "opendiscord:topic-set":{source:"slash"|"text"|"ticket-action"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,topic:string},workers:"opendiscord:topic-set"}, "opendiscord:priority-set":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel,reason:string|null},workers:"opendiscord:priority-set"}, "opendiscord:priority-get":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,priority:ODPriorityLevel},workers:"opendiscord:priority-get"}, + "opendiscord:transfer-message":{source:"slash"|"text"|"other",params:{guild:discord.Guild,channel:discord.GuildTextBasedChannel,user:discord.User,ticket:ODTicket,oldCreator:discord.User,newCreator:discord.User,reason:string|null},workers:"opendiscord:transfer-message"}, } /**## ODMessageManager_Default `default_class` diff --git a/src/core/api/defaults/client.ts b/src/core/api/defaults/client.ts index a56ad33..7b5a540 100644 --- a/src/core/api/defaults/client.ts +++ b/src/core/api/defaults/client.ts @@ -52,6 +52,7 @@ export interface ODSlashCommandManagerIds_Default { "opendiscord:autodelete":ODSlashCommand, "opendiscord:topic":ODSlashCommand, "opendiscord:priority":ODSlashCommand, + "opendiscord:transfer":ODSlashCommand, } /**## ODSlashCommandManager_Default `default_class` @@ -125,6 +126,7 @@ export interface ODTextCommandManagerIds_Default { "opendiscord:topic-set":ODTextCommand, "opendiscord:priority-set":ODTextCommand, "opendiscord:priority-get":ODTextCommand, + "opendiscord:transfer":ODTextCommand, } /**## ODTextCommandManager_Default `default_class` diff --git a/src/core/api/defaults/event.ts b/src/core/api/defaults/event.ts index 3315d08..86496fc 100644 --- a/src/core/api/defaults/event.ts +++ b/src/core/api/defaults/event.ts @@ -204,8 +204,10 @@ export interface ODEventIds_Default { "afterTicketsCleared": ODEvent_Default<(tickets:ODTicket[], clearer:discord.User, channel:discord.GuildTextBasedChannel, filter:ODTicketClearFilter) => ODPromiseVoid> "onTicketTopicChange": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldTopic:string, newTopic:string) => ODPromiseVoid> "afterTicketTopicChanged": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldTopic:string, newTopic:string) => ODPromiseVoid> - "onTicketPriorityChange": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldPriority:ODPriorityLevel, newPriority:ODPriorityLevel) => ODPromiseVoid> - "afterTicketPriorityChanged": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldPriority:ODPriorityLevel, newPriority:ODPriorityLevel) => ODPromiseVoid> + "onTicketPriorityChange": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldPriority:ODPriorityLevel, newPriority:ODPriorityLevel, reason:string|null) => ODPromiseVoid> + "afterTicketPriorityChanged": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldPriority:ODPriorityLevel, newPriority:ODPriorityLevel, reason:string|null) => ODPromiseVoid> + "onTicketTransfer": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldCreator:discord.User, newCreator:discord.User, reason:string|null) => ODPromiseVoid> + "afterTicketTransferred": ODEvent_Default<(ticket:ODTicket, changer:discord.User, channel:discord.GuildTextBasedChannel, oldCreator:discord.User, newCreator:discord.User, reason:string|null) => ODPromiseVoid> //roles "onRoleLoad": ODEvent_Default<(roles:ODRoleManager) => ODPromiseVoid> diff --git a/src/core/api/defaults/responder.ts b/src/core/api/defaults/responder.ts index 6023033..8d7b317 100644 --- a/src/core/api/defaults/responder.ts +++ b/src/core/api/defaults/responder.ts @@ -46,6 +46,7 @@ export interface ODCommandResponderManagerIds_Default { "opendiscord:clear":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:clear"|"opendiscord:logs"}, "opendiscord:topic":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:topic"|"opendiscord:logs"}, "opendiscord:priority":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:priority"|"opendiscord:logs"}, + "opendiscord:transfer":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:transfer"|"opendiscord:logs"}, "opendiscord:autoclose":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:autoclose"|"opendiscord:logs"}, "opendiscord:autodelete":{source:"slash"|"text",params:{},workers:"opendiscord:permissions"|"opendiscord:autodelete"|"opendiscord:logs"}, diff --git a/src/core/api/defaults/stat.ts b/src/core/api/defaults/stat.ts index 47e306c..dd8486e 100644 --- a/src/core/api/defaults/stat.ts +++ b/src/core/api/defaults/stat.ts @@ -60,6 +60,7 @@ export interface ODStatGlobalScopeIds_DefaultGlobal { "opendiscord:tickets-claimed":ODBasicStat, "opendiscord:tickets-pinned":ODBasicStat, "opendiscord:tickets-moved":ODBasicStat, + "opendiscord:tickets-transferred":ODBasicStat, "opendiscord:users-blacklisted":ODBasicStat, "opendiscord:transcripts-created":ODBasicStat, "opendiscord:ticket-volume":ODDynamicStat, @@ -204,6 +205,7 @@ export interface ODStatScopeIds_DefaultUser { "opendiscord:tickets-claimed":ODBasicStat, "opendiscord:tickets-pinned":ODBasicStat, "opendiscord:tickets-moved":ODBasicStat, + "opendiscord:tickets-transferred":ODBasicStat, "opendiscord:users-blacklisted":ODBasicStat, "opendiscord:transcripts-created":ODBasicStat, "opendiscord:current-tickets":ODDynamicStat, diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index d20df8f..a72d965 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -617,6 +617,29 @@ export const loadAllSlashCommands = async () => { //TODO: list (v4.2) ] })) + + //TRANSFER + if (allowedCommands.includes("transfer")) commands.add(new api.ODSlashCommand("opendiscord:transfer",{ + type:act.ChatInput, + name:"transfer", + description:"Transfer the ticket ownership from one user to another.", //TODO TRANSLATION!!! + contexts:[discord.InteractionContextType.Guild], + integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], + options:[ + { + name:"user", + description:"The user to transfer to.", //TODO TRANSLATION!!! + type:acot.User, + required:true + }, + { + name:"reason", + description:lang.getTranslation("commands.reason"), + type:acot.String, + required:false + } + ] + })) } export const loadAllTextCommands = async () => { @@ -1169,6 +1192,28 @@ export const loadAllTextCommands = async () => { guildPermission:true, allowBots:false, })) + + //TRANSFER + if (allowedCommands.includes("transfer")) commands.add(new api.ODTextCommand("opendiscord:transfer",{ + name:"transfer", + prefix, + dmPermission:false, + guildPermission:true, + allowBots:false, + options:[ + { + name:"user", + type:"user", + required:true + }, + { + name:"reason", + type:"string", + required:false, + allowSpaces:true + } + ] + })) } export const loadAllContextMenus = async () => { diff --git a/src/data/framework/eventLoader.ts b/src/data/framework/eventLoader.ts index 257aa5e..cd195c5 100644 --- a/src/data/framework/eventLoader.ts +++ b/src/data/framework/eventLoader.ts @@ -159,6 +159,8 @@ export const loadAllEvents = () => { "afterTicketTopicChanged", "onTicketPriorityChange", "afterTicketPriorityChanged", + "onTicketTransfer", + "afterTicketTransferred", //roles "onRoleLoad", diff --git a/src/data/framework/statLoader.ts b/src/data/framework/statLoader.ts index 50de840..44263d4 100644 --- a/src/data/framework/statLoader.ts +++ b/src/data/framework/statLoader.ts @@ -19,15 +19,16 @@ export const loadAllStats = async () => { const global = stats.get("opendiscord:global") if (global){ - global.add(new api.ODBasicStat("opendiscord:tickets-created",12,lang.getTranslation("stats.properties.ticketsCreated"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-closed",11,lang.getTranslation("stats.properties.ticketsClosed"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-deleted",10,lang.getTranslation("stats.properties.ticketsDeleted"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-reopened",9,lang.getTranslation("stats.properties.ticketsReopened"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-autoclosed",8,lang.getTranslation("stats.properties.ticketsAutoclosed"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-autodeleted",7,"Tickets Autodeleted",0)) //TODO TRANSLATION!!! - global.add(new api.ODBasicStat("opendiscord:tickets-claimed",6,lang.getTranslation("stats.properties.ticketsClaimed"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-pinned",5,lang.getTranslation("stats.properties.ticketsPinned"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-moved",4,lang.getTranslation("stats.properties.ticketsMoved"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-created",13,lang.getTranslation("stats.properties.ticketsCreated"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-closed",12,lang.getTranslation("stats.properties.ticketsClosed"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-deleted",11,lang.getTranslation("stats.properties.ticketsDeleted"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-reopened",10,lang.getTranslation("stats.properties.ticketsReopened"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-autoclosed",9,lang.getTranslation("stats.properties.ticketsAutoclosed"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-autodeleted",8,"Tickets Autodeleted",0)) //TODO TRANSLATION!!! + global.add(new api.ODBasicStat("opendiscord:tickets-claimed",7,lang.getTranslation("stats.properties.ticketsClaimed"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-pinned",6,lang.getTranslation("stats.properties.ticketsPinned"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-moved",5,lang.getTranslation("stats.properties.ticketsMoved"),0)) + global.add(new api.ODBasicStat("opendiscord:tickets-transferred",4,"Tickets Transferred",0)) //TODO TRANSLATION!!! global.add(new api.ODBasicStat("opendiscord:users-blacklisted",3,lang.getTranslation("stats.properties.usersBlacklisted"),0)) global.add(new api.ODBasicStat("opendiscord:transcripts-created",2,lang.getTranslation("stats.properties.transcriptsCreated"),0)) global.add(new api.ODDynamicStat("opendiscord:ticket-volume",1,() => { @@ -71,13 +72,14 @@ export const loadAllStats = async () => { if (permissions.type == "support") return lang.getTranslation("params.uppercase.role")+": 💬 `Support Team`" //TODO TRANSLATION!!! else return lang.getTranslation("params.uppercase.role")+": 👤 `Member`" //TODO TRANSLATION!!! })) - user.add(new api.ODBasicStat("opendiscord:tickets-created",9,lang.getTranslation("stats.properties.ticketsCreated"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-closed",8,lang.getTranslation("stats.properties.ticketsClosed"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-deleted",7,lang.getTranslation("stats.properties.ticketsDeleted"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-reopened",6,lang.getTranslation("stats.properties.ticketsReopened"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-claimed",5,lang.getTranslation("stats.properties.ticketsClaimed"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-pinned",4,lang.getTranslation("stats.properties.ticketsPinned"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-moved",3,lang.getTranslation("stats.properties.ticketsMoved"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-created",10,lang.getTranslation("stats.properties.ticketsCreated"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-closed",9,lang.getTranslation("stats.properties.ticketsClosed"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-deleted",8,lang.getTranslation("stats.properties.ticketsDeleted"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-reopened",7,lang.getTranslation("stats.properties.ticketsReopened"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-claimed",6,lang.getTranslation("stats.properties.ticketsClaimed"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-pinned",5,lang.getTranslation("stats.properties.ticketsPinned"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-moved",4,lang.getTranslation("stats.properties.ticketsMoved"),0)) + user.add(new api.ODBasicStat("opendiscord:tickets-transferred",3,"Tickets Transferred",0)) //TODO TRANSLATION!!! user.add(new api.ODBasicStat("opendiscord:users-blacklisted",2,lang.getTranslation("stats.properties.usersBlacklisted"),0)) user.add(new api.ODBasicStat("opendiscord:transcripts-created",1,lang.getTranslation("stats.properties.transcriptsCreated"),0)) user.add(new api.ODDynamicStat("opendiscord:current-tickets",0,async (scopeId,guild,channel,user) => { diff --git a/src/index.ts b/src/index.ts index 39f29fc..d2b5de5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -673,6 +673,7 @@ const main = async () => { await (await import("./commands/autodelete.js")).registerCommandResponders() await (await import("./commands/topic.js")).registerCommandResponders() await (await import("./commands/priority.js")).registerCommandResponders() + await (await import("./commands/transfer.js")).registerCommandResponders() } await opendiscord.events.get("onCommandResponderLoad").emit([opendiscord.responders.commands,opendiscord.responders,opendiscord.actions]) await opendiscord.events.get("afterCommandRespondersLoaded").emit([opendiscord.responders.commands,opendiscord.responders,opendiscord.actions]) @@ -761,6 +762,7 @@ const main = async () => { await (await import("./actions/clearTickets.js")).registerActions() await (await import("./actions/updateTicketTopic.js")).registerActions() await (await import("./actions/updateTicketPriority.js")).registerActions() + await (await import("./actions/transferTicket.js")).registerActions() } await opendiscord.events.get("onActionLoad").emit([opendiscord.actions]) await opendiscord.events.get("afterActionsLoaded").emit([opendiscord.actions]) From e55438cc67a428a26b04bb22117501172a4ca85f Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:39:52 +0100 Subject: [PATCH 70/78] (v4.1) Added +/-10 Minor Features & Improvements --- src/actions/closeTicket.ts | 24 ++++++++ src/actions/createTicket.ts | 3 + src/actions/deleteTicket.ts | 80 +++++++++++++++++++++++++ src/builders/buttons.ts | 4 +- src/builders/embeds.ts | 65 ++++++++++---------- src/commands/close.ts | 12 ++++ src/commands/delete.ts | 20 +++++++ src/core/api/defaults/config.ts | 4 +- src/core/api/main.ts | 2 +- src/core/api/modules/client.ts | 19 +++--- src/core/api/openticket/transcript.ts | 39 +++++++++--- src/core/cli/quickSetup.ts | 2 +- src/data/framework/startScreenLoader.ts | 2 +- src/index.ts | 2 +- 14 files changed, 223 insertions(+), 55 deletions(-) diff --git a/src/actions/closeTicket.ts b/src/actions/closeTicket.ts index 1db612f..6742e38 100644 --- a/src/actions/closeTicket.ts +++ b/src/actions/closeTicket.ts @@ -222,6 +222,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start closing ticket if (params.data == "reason"){ @@ -309,6 +321,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start closing ticket if (params.data == "reason"){ diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index 4e79d7f..918378e 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -209,6 +209,9 @@ export const registerActions = async () => { const msg = await channel.send((await opendiscord.builders.messages.getSafe("opendiscord:ticket-message").build(source,{guild,channel,user,ticket})).message) ticket.get("opendiscord:ticket-message").value = msg.id + + //pin ticket message (if required) + if (generalConfig.data.system.pinFirstTicketMessage && msg.pinnable) await msg.pin("Ticket Message") //manage stats await opendiscord.stats.get("opendiscord:ticket").setStat("opendiscord:messages-sent",ticket.id.value,1,"increase") diff --git a/src/actions/deleteTicket.ts b/src/actions/deleteTicket.ts index 4c368b7..7a50ddf 100644 --- a/src/actions/deleteTicket.ts +++ b/src/actions/deleteTicket.ts @@ -121,6 +121,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -167,6 +175,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ @@ -206,6 +226,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -252,6 +280,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ @@ -295,6 +335,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -341,6 +389,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ @@ -384,6 +444,14 @@ export const registerVerifyBars = async () => { new api.ODWorker("opendiscord:permissions",1,async (instance,params,source,cancel) => { const permissionMode = generalConfig.data.system.permissions.delete + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (params.data == "no-transcript" && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + if (permissionMode == "none"){ //no permissions instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:[]})) @@ -430,6 +498,18 @@ export const registerVerifyBars = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } //start deleting ticket if (params.data == "reason"){ diff --git a/src/builders/buttons.ts b/src/builders/buttons.ts index fcdc172..cc9cba9 100644 --- a/src/builders/buttons.ts +++ b/src/builders/buttons.ts @@ -277,7 +277,7 @@ const ticketButtons = () => { instance.setMode("button") instance.setCustomId("od:pin-ticket_"+source) instance.setColor("gray") - instance.setEmoji("📌") + instance.setEmoji(generalConfig.data.system.pinEmoji) instance.setLabel(lang.getTranslation("actions.buttons.pin")) }) ) @@ -291,7 +291,7 @@ const ticketButtons = () => { instance.setMode("button") instance.setCustomId("od:unpin-ticket_"+source) instance.setColor("gray") - instance.setEmoji("📌") + instance.setEmoji(generalConfig.data.system.pinEmoji) instance.setLabel(lang.getTranslation("actions.buttons.unpin")) }) ) diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index efde0cf..7545ce4 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -596,8 +596,11 @@ const ticketEmbeds = () => { if (embedOptions.image) instance.setImage(embedOptions.image) if (embedOptions.timestamp) instance.setTimestamp(new Date()) if (embedOptions.description) instance.setDescription(embedOptions.description) - + if (ticket.option.get("opendiscord:questions").value.length > 0){ + //show config fields if mixing is allowed + if (generalConfig.data.system.displayFieldsWithQuestions) instance.addFields(...embedOptions.fields) + const answers = ticket.get("opendiscord:answers").value answers.forEach((answer) => { if (!answer.value || answer.value.length == 0) return @@ -629,7 +632,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔒",lang.getTranslation("actions.titles.close"))) instance.setDescription(lang.getTranslation("actions.descriptions.close")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -643,7 +646,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔓",lang.getTranslation("actions.titles.reopen"))) instance.setDescription(lang.getTranslation("actions.descriptions.reopen")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -657,7 +660,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🗑️",lang.getTranslation("actions.titles.delete"))) instance.setDescription(lang.getTranslation("actions.descriptions.delete")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -671,7 +674,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("👋",lang.getTranslation("actions.titles.claim"))) instance.setDescription(lang.getTranslation("actions.descriptions.claim")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -685,7 +688,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("↩️",lang.getTranslation("actions.titles.unclaim"))) instance.setDescription(lang.getTranslation("actions.descriptions.unclaim")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -697,9 +700,9 @@ const ticketEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.pin"))) instance.setDescription(lang.getTranslation("actions.descriptions.pin")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -711,9 +714,9 @@ const ticketEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.unpin"))) instance.setDescription(lang.getTranslation("actions.descriptions.unpin")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -727,7 +730,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.rename",["`#"+data+"`"])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -741,7 +744,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔀",lang.getTranslation("actions.titles.move"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.move",["`"+data.get("opendiscord:name").value+"`"])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -755,7 +758,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.add"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.add",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -769,7 +772,7 @@ const ticketEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("👤",lang.getTranslation("actions.titles.remove"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.remove",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -801,10 +804,10 @@ const ticketEmbeds = () => { instance.setTitle(utilities.emojiTitle("↩️",lang.getTranslation("actions.titles.unclaim"))) instance.setDescription(lang.getTranslation("actions.logs.unclaimDm")) }else if (mode == "pin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.pin"))) instance.setDescription(lang.getTranslation("actions.logs.pinDm")) }else if (mode == "unpin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.unpin"))) instance.setDescription(lang.getTranslation("actions.logs.unpinDm")) }else if (mode == "rename"){ instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) @@ -838,7 +841,7 @@ const ticketEmbeds = () => { //TODO TRANSLATION!!! {name:"Option"+":",value:"```"+(ticket.option.get("opendiscord:name").value)+"```",inline:false}, ) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```",inline:false}) if (mode == "close"){ instance.setTitle(utilities.emojiTitle("🔒",lang.getTranslation("actions.titles.close"))) @@ -856,10 +859,10 @@ const ticketEmbeds = () => { instance.setTitle(utilities.emojiTitle("↩️",lang.getTranslation("actions.titles.unclaim"))) instance.setDescription(lang.getTranslationWithParams("actions.logs.unclaimLog",[discord.userMention(user.id)])) }else if (mode == "pin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.pin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.pin"))) instance.setDescription(lang.getTranslationWithParams("actions.logs.pinLog",[discord.userMention(user.id)])) }else if (mode == "unpin"){ - instance.setTitle(utilities.emojiTitle("📌",lang.getTranslation("actions.titles.unpin"))) + instance.setTitle(utilities.emojiTitle(generalConfig.data.system.pinEmoji,lang.getTranslation("actions.titles.unpin"))) instance.setDescription(lang.getTranslationWithParams("actions.logs.unpinLog",[discord.userMention(user.id)])) }else if (mode == "rename"){ instance.setTitle(utilities.emojiTitle("🔄",lang.getTranslation("actions.titles.rename"))) @@ -912,7 +915,7 @@ const blacklistEmbeds = () => { if (blacklist){ instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistGetSuccess",[discord.userMention(data.id)])) - if (blacklist.reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+blacklist.reason+"```"}) + if (blacklist.reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(blacklist.reason ?? "/")+"```"}) }else instance.setDescription("*"+lang.getTranslationWithParams("actions.descriptions.blacklistGetEmpty",[discord.userMention(data.id)])+"*") }) @@ -928,7 +931,7 @@ const blacklistEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🚫",lang.getTranslation("actions.titles.blacklistAdd"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistAdd",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -942,7 +945,7 @@ const blacklistEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🆓",lang.getTranslation("actions.titles.blacklistRemove"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.blacklistRemove",[discord.userMention(data.id)])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -959,7 +962,7 @@ const blacklistEmbeds = () => { instance.setTitle(utilities.emojiTitle((mode == "add") ? "🚫" : "🆓",title)) instance.setTimestamp(new Date()) instance.setDescription(text) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -978,7 +981,7 @@ const blacklistEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setTimestamp(new Date()) instance.setDescription(text) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } @@ -1082,7 +1085,7 @@ const transcriptEmbeds = () => { instance.setTimestamp(new Date()) instance.setDescription(lang.getTranslation("transcripts.errors.error")) instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```",inline:false}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } @@ -1265,7 +1268,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autocloseEnabled"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.autocloseEnabled",[time.toString()])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1279,7 +1282,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autodeleteEnabled"))) instance.setDescription(lang.getTranslationWithParams("actions.descriptions.autodeleteEnabled",[time.toString()])) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1293,7 +1296,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autocloseDisabled"))) instance.setDescription(lang.getTranslation("actions.descriptions.autocloseDisabled")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1307,7 +1310,7 @@ const autoEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("⏱️",lang.getTranslation("actions.titles.autodeleteDisabled"))) instance.setDescription(lang.getTranslation("actions.descriptions.autodeleteDisabled")) - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } @@ -1337,7 +1340,7 @@ const extraEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🚨","Priority Changed")) //TODO TRANSLATION!!! instance.setDescription("The ticket priority has been changed to **"+priority.renderDisplayName()+"** by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1364,7 +1367,7 @@ const extraEmbeds = () => { instance.setColor(generalConfig.data.mainColor) instance.setTitle(utilities.emojiTitle("🔀","Ticket Transferred")) //TODO TRANSLATION!!! instance.setDescription("The ticket ownership has been transferred from "+discord.userMention(oldCreator.id)+" to "+discord.userMention(newCreator.id)+" by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! - if (reason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+reason+"```"}) + if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) } \ No newline at end of file diff --git a/src/commands/close.ts b/src/commands/close.ts index ebb4616..56acc36 100644 --- a/src/commands/close.ts +++ b/src/commands/close.ts @@ -64,6 +64,18 @@ export const registerCommandResponders = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } const reason = instance.options.getString("reason",false) diff --git a/src/commands/delete.ts b/src/commands/delete.ts index 11d315f..0b08657 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -59,10 +59,30 @@ export const registerCommandResponders = async () => { instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-ticket-busy").build("button",{guild,channel,user})) return cancel() } + //return when not allowed because of missing messages + if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ + const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) + if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + return cancel() + } + } const reason = instance.options.getString("reason",false) const withoutTranscript = instance.options.getBoolean("notranscript",false) ?? false + //don't allow deleteWithoutTranscript to non-global-admins when enabled + if (withoutTranscript && generalConfig.data.system.adminOnlyDeleteWithoutTranscript){ + if (!opendiscord.permissions.hasPermissions("support",await opendiscord.permissions.getPermissions(instance.user,instance.channel,instance.guild,{allowChannelRoleScope:false,allowChannelUserScope:false,allowGlobalRoleScope:true,allowGlobalUserScope:true}))){ + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions").build("button",{guild:instance.guild,channel:instance.channel,user:instance.user,permissions:["support"]})) + return cancel() + } + } + //start deleting ticket await instance.defer(false) await instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:delete-message").build(source,{guild,channel,user,ticket,reason})) diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index d29f39d..f828f3e 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -4,7 +4,7 @@ import { ODValidButtonColor, ODValidId } from "../modules/base" import * as discord from "discord.js" import { ODConfigManager, ODConfig, ODJsonConfig } from "../modules/config" -import { ODClientActivityStatus, ODClientActivityType } from "../modules/client" +import { ODClientActivityMode, ODClientActivityType } from "../modules/client" import { ODRoleUpdateMode } from "../openticket/role" /** (CONTRIBUTOR GUIDE) HOW TO ADD NEW CONFIG VARIABLES? @@ -77,7 +77,7 @@ export interface ODJsonConfig_DefaultStatusType { /**The type of status (e.g. playing, listening, custom, ...) */ type:Exclude, /**The mode/status of the bot (e.g. online, invisible, idle, do not disturb) */ - mode:ODClientActivityStatus + mode:ODClientActivityMode /**The text for the status. */ text:string, /**Additional text for the status. (visible below 'text') */ diff --git a/src/core/api/main.ts b/src/core/api/main.ts index 4738986..811d5d6 100644 --- a/src/core/api/main.ts +++ b/src/core/api/main.ts @@ -176,7 +176,7 @@ export class ODMain { this.panels = new ODPanelManager(this.debug) this.tickets = new ODTicketManager(this.debug,this.client) this.blacklist = new ODBlacklistManager(this.debug) - this.transcripts = new ODTranscriptManager_Default(this.debug,this.tickets,this.client) + this.transcripts = new ODTranscriptManager_Default(this.debug,this.tickets,this.client,this.permissions) this.roles = new ODRoleManager(this.debug) this.priorities = new ODPriorityManager_Default(this.debug) } diff --git a/src/core/api/modules/client.ts b/src/core/api/modules/client.ts index d51423c..5638a99 100644 --- a/src/core/api/modules/client.ts +++ b/src/core/api/modules/client.ts @@ -335,10 +335,10 @@ export class ODClientManager { * Possible activity types for the bot. */ export type ODClientActivityType = ("playing"|"listening"|"watching"|"custom"|false) -/**## ODClientPermissions `type` +/**## ODClientActivityMode `type` * Possible activity statuses for the bot. */ -export type ODClientActivityStatus = ("online"|"invisible"|"idle"|"dnd") +export type ODClientActivityMode = ("online"|"invisible"|"idle"|"dnd") /**## ODClientActivityManager `class` @@ -358,8 +358,10 @@ export class ODClientActivityManager { type: ODClientActivityType = false /**The current status text */ text: string = "" - /**The current status status */ - status: ODClientActivityStatus = "online" + /**The current status mode */ + mode: ODClientActivityMode = "online" + /**Additional state text */ + state: string = "" /**The timer responsible for refreshing the status. Stop it using `clearInterval(interval)` */ interval?: NodeJS.Timeout @@ -374,10 +376,11 @@ export class ODClientActivityManager { } /**Update the status. When already initiated, it can take up to 10min to see the updated status in discord. */ - setStatus(type:ODClientActivityType, text:string, status:ODClientActivityStatus, forceUpdate?:boolean){ + setStatus(type:ODClientActivityType, text:string, mode:ODClientActivityMode, state:string, forceUpdate?:boolean){ this.type = type this.text = text - this.status = status + this.mode = mode + this.state = state if (forceUpdate) this.#updateClientActivity(this.type,this.text) } @@ -404,10 +407,10 @@ export class ODClientActivityManager { this.manager.client.user.setPresence({ activities:[{ type:this.#getStatusTypeEnum(type), - state:undefined, + state:this.state ? this.state : undefined, name:text, }], - status:this.status + status:this.mode }) } /**Get the enum that links to the correct type */ diff --git a/src/core/api/openticket/transcript.ts b/src/core/api/openticket/transcript.ts index 6a7d6a4..9234ec9 100644 --- a/src/core/api/openticket/transcript.ts +++ b/src/core/api/openticket/transcript.ts @@ -7,6 +7,7 @@ import { ODTicket, ODTicketManager } from "./ticket" import { ODMessageBuildResult } from "../modules/builder" import { ODClientManager } from "../modules/client" import * as discord from "discord.js" +import { ODPermissionManager_Default } from "#opendiscord-types" /**## ODTranscriptManager `class` * This is an Open Ticket transcript manager. @@ -21,10 +22,10 @@ export class ODTranscriptManager extends ODManager= 1000000000000) return {size:(Math.round((bytes/1000000000000)*100)/100),unit:"TB"} - if (bytes >= 1000000000) return {size:(Math.round((bytes/1000000000)*100)/100),unit:"GB"} - else if (bytes >= 1000000) return {size:(Math.round((bytes/1000000)*100)/100),unit:"MB"} - else if (bytes >= 1000) return {size:(Math.round((bytes/1000)*100)/100),unit:"KB"} - else return {size:bytes,unit:"B"} + if (bytes < 1024) return {size:Math.round(bytes),unit:"B"} + else if (bytes < 1024*1024) return {size:Math.round(bytes/1024),unit:"KB"} + else if (bytes < 1024*1024*1024) return {size:Math.round(bytes/(1024*1024)),unit:"MB"} + else if (bytes < 1024*1024*1024*1024) return {size:Math.round(bytes/(1024*1024*1024)),unit:"GB"} + else return {size:Math.round(bytes/(1024*1024*1024*1024)),unit:"TB"} } /**Get the `ODTranscriptEmojiData` from a discord.js component emoji. */ #handleComponentEmoji(message:discord.Message, rawEmoji:discord.APIMessageComponentEmoji|null): ODTranscriptEmojiData|null { @@ -439,6 +443,25 @@ export class ODTranscriptCollector { return userData } + /**Analyse the ticket for the amount of messages users & admins have sent in the ticket. */ + async ticketUserMessagesAnalysis(ticket:ODTicket,guild:discord.Guild,channel:discord.GuildTextBasedChannel){ + const messages = await this.collectAllMessages(ticket,{bots:true,client:true,users:true}) + if (!messages) return null + const parsedMessages = await this.convertMessagesToTranscriptData(messages) + let userMessages = 0 + let adminMessages = 0 + + for (const msg of parsedMessages){ + if (msg.author.tag || msg.author.id == this.#client.client.user.id) continue + const user = await this.#client.fetchUser(msg.author.id) + if (!user) continue + const isAdmin = this.#permissions.hasPermissions("support",await this.#permissions.getPermissions(user,channel,guild)) + if (isAdmin) adminMessages++ + else userMessages++ + } + + return {userMessages,adminMessages,totalMessages:userMessages+adminMessages} + } } /**## ODTranscriptCollectorIncludeSettings `interface` diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index d09a088..b26cc57 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -226,7 +226,7 @@ async function quickSetupLogin(token:string): Promise return new Promise(async (resolve) => { try{ client.readyListener = async () => { - client.activity.setStatus("custom","Configuring Open Ticket...","idle",true) + client.activity.setStatus("custom","Configuring Open Ticket...","idle","",true) resolve(client) } const success = await client.login(true) diff --git a/src/data/framework/startScreenLoader.ts b/src/data/framework/startScreenLoader.ts index b80d7dc..328e01b 100644 --- a/src/data/framework/startScreenLoader.ts +++ b/src/data/framework/startScreenLoader.ts @@ -31,7 +31,7 @@ export const loadAllStartScreenComponents = async () => { //STATS opendiscord.startscreen.add(new api.ODStartScreenPropertiesCategoryComponent("opendiscord:stats",2,"startup info",[ - {key:"status",value:ansis.bold(opendiscord.client.activity.getStatusType())+opendiscord.client.activity.text+" ("+opendiscord.client.activity.status+")"}, + {key:"status",value:ansis.bold(opendiscord.client.activity.getStatusType())+opendiscord.client.activity.text+" ("+opendiscord.client.activity.mode+")"}, {key:"options",value:"loaded "+ansis.bold(opendiscord.options.getLength().toString())+" options!"}, {key:"panels",value:"loaded "+ansis.bold(opendiscord.panels.getLength().toString())+" panels!"}, {key:"tickets",value:"loaded "+ansis.bold(opendiscord.tickets.getLength().toString())+" tickets!"}, diff --git a/src/index.ts b/src/index.ts index d2b5de5..d9fd98e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -412,7 +412,7 @@ const main = async () => { opendiscord.log("Loading client activity...","system") if (opendiscord.defaults.getDefault("clientActivityLoading")){ //load config status - if (generalConfig.data.status && generalConfig.data.status.enabled) opendiscord.client.activity.setStatus(generalConfig.data.status.type,generalConfig.data.status.text,generalConfig.data.status.mode) + if (generalConfig.data.status && generalConfig.data.status.enabled) opendiscord.client.activity.setStatus(generalConfig.data.status.type,generalConfig.data.status.text,generalConfig.data.status.mode,generalConfig.data.status.state) } await opendiscord.events.get("onClientActivityLoad").emit([opendiscord.client.activity,opendiscord.client]) await opendiscord.events.get("afterClientActivityLoaded").emit([opendiscord.client.activity,opendiscord.client]) From 4b485adf82bff717e8fa0e38d7582fe3f27bcc50 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:53:40 +0100 Subject: [PATCH 71/78] (v4.1) Additional autoclose/autodelete features --- config/general.json | 1 - src/actions/reopenTicket.ts | 6 ++++++ src/core/api/defaults/config.ts | 2 -- src/core/cli/quickSetup.ts | 1 - src/core/startup/migration.ts | 1 - src/data/framework/checkerLoader.ts | 1 - src/data/framework/codeLoader.ts | 7 +++++-- src/data/framework/configLoader.ts | 1 - 8 files changed, 11 insertions(+), 9 deletions(-) diff --git a/config/general.json b/config/general.json index 39abe86..221ecda 100644 --- a/config/general.json +++ b/config/general.json @@ -39,7 +39,6 @@ "replyOnTicketCreation":true, "replyOnReactionRole":true, - "showPreAutocloseWarning":false, "askPriorityOnTicketCreation":false, "removeParticipantsOnClose":false, "disableAutocloseAfterReopen":true, diff --git a/src/actions/reopenTicket.ts b/src/actions/reopenTicket.ts index 511eb37..5112f66 100644 --- a/src/actions/reopenTicket.ts +++ b/src/actions/reopenTicket.ts @@ -28,6 +28,12 @@ export const registerActions = async () => { ticket.get("opendiscord:open").value = true ticket.get("opendiscord:busy").value = true + if (generalConfig.data.system.disableAutocloseAfterReopen){ + //disable autoclose after reopen + ticket.get("opendiscord:autoclose-enabled").value = false + ticket.get("opendiscord:autoclose-hours").value = 0 + } + //update stats await opendiscord.stats.get("opendiscord:global").setStat("opendiscord:tickets-reopened",1,"increase") await opendiscord.stats.get("opendiscord:user").setStat("opendiscord:tickets-reopened",user.id,1,"increase") diff --git a/src/core/api/defaults/config.ts b/src/core/api/defaults/config.ts index f828f3e..69a1850 100644 --- a/src/core/api/defaults/config.ts +++ b/src/core/api/defaults/config.ts @@ -235,8 +235,6 @@ export interface ODJsonConfig_DefaultSystem { replyOnTicketCreation:boolean, /**Reply with an ephemeral message when reaction roles are changed. */ replyOnReactionRole:boolean, - /**Show a warning message before the ticket gets autoclosed. This will happen when only 1/4th of the autoclose time remains. */ - showPreAutocloseWarning:boolean, /**Ask for the priority of this ticket on ticket creation. This will happen in a dropdown in the ticket message. */ askPriorityOnTicketCreation:boolean, /**Remove all participants (except admins) from the ticket when it's closed. */ diff --git a/src/core/cli/quickSetup.ts b/src/core/cli/quickSetup.ts index b26cc57..ef1bdf4 100644 --- a/src/core/cli/quickSetup.ts +++ b/src/core/cli/quickSetup.ts @@ -1239,7 +1239,6 @@ async function saveQuickSetupConfig(){ replyOnTicketCreation:false, replyOnReactionRole:true, - showPreAutocloseWarning:false, askPriorityOnTicketCreation:false, removeParticipantsOnClose:quickSetupStorage.removeParticipantsOnClose ?? false, disableAutocloseAfterReopen:true, diff --git a/src/core/startup/migration.ts b/src/core/startup/migration.ts index c7e3085..2bbc38a 100644 --- a/src/core/startup/migration.ts +++ b/src/core/startup/migration.ts @@ -57,7 +57,6 @@ export const migrations = [ generalConfig.data.system.showGlobalAdminsInPanelRoles = false generalConfig.data.system.alwaysShowReason = false generalConfig.data.system.pinEmoji = "📌" - generalConfig.data.system.showPreAutocloseWarning = false generalConfig.data.system.askPriorityOnTicketCreation = false generalConfig.data.system.disableAutocloseAfterReopen = true generalConfig.data.system.autodeleteRequiresClosedTicket = true diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 7ca554b..c34c907 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -278,7 +278,6 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"replyOnTicketCreation",checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-ticket-creation",{cliDisplayName:"Reply On Ticket Creation",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when creating a ticket."})}, {key:"replyOnReactionRole",checker:new api.ODCheckerBooleanStructure("opendiscord:reply-on-reaction-role",{cliDisplayName:"Reply On Reaction Role",cliDisplayDescription:"When enabled, the bot will send an ephemeral reply in the channel of the panel when using a role button."})}, - {key:"showPreAutocloseWarning",checker:new api.ODCheckerBooleanStructure("opendiscord:show-pre-autoclose-warning",{cliDisplayName:"Show Pre-Autoclose Warning",cliDisplayDescription:"Show a warning message before the ticket gets autoclosed. This will happen when only 1/4th of the autoclose time remains."})}, {key:"askPriorityOnTicketCreation",checker:new api.ODCheckerBooleanStructure("opendiscord:ask-priority-creation",{cliDisplayName:"Ask Priority On Ticket Creation",cliDisplayDescription:"Ask for the priority of this ticket on ticket creation. This will happen in a dropdown in the ticket message."})}, {key:"removeParticipantsOnClose",checker:new api.ODCheckerBooleanStructure("opendiscord:remove-participants-on-close",{cliDisplayName:"Remove Participants On Close",cliDisplayDescription:"When enabled, all participants except admins will be removed from the ticket."})}, {key:"disableAutocloseAfterReopen",checker:new api.ODCheckerBooleanStructure("opendiscord:disable-autoclose-reopen",{cliDisplayName:"Disable Autoclose On Reopen",cliDisplayDescription:"Disable autoclose for a ticket when it has been closed and re-opened."})}, diff --git a/src/data/framework/codeLoader.ts b/src/data/framework/codeLoader.ts index da79601..08b4cd0 100644 --- a/src/data/framework/codeLoader.ts +++ b/src/data/framework/codeLoader.ts @@ -446,7 +446,9 @@ const loadAutoCode = () => { if (lastMessage){ //ticket has last message const disableOnClaim = ticket.option.get("opendiscord:autodelete-disable-claim").value && ticket.get("opendiscord:claimed").value - const enabled = (disableOnClaim) ? false : ticket.get("opendiscord:autodelete-enabled").value + const disableWhenNotClosed = generalConfig.data.system.autodeleteRequiresClosedTicket && !ticket.get("opendiscord:closed").value + + const enabled = (disableOnClaim || disableWhenNotClosed) ? false : ticket.get("opendiscord:autodelete-enabled").value const days = ticket.get("opendiscord:autodelete-days").value const time = days*24*60*60*1000 //days in milliseconds @@ -475,7 +477,8 @@ const loadAutoCode = () => { if (!channel) return //ticket has been created by this user const disableOnClaim = ticket.option.get("opendiscord:autodelete-disable-claim").value && ticket.get("opendiscord:claimed").value - const enabled = (disableOnClaim || !ticket.get("opendiscord:autodelete-enabled").value) ? false : ticket.option.get("opendiscord:autodelete-enable-leave") + const disableWhenNotClosed = generalConfig.data.system.autodeleteRequiresClosedTicket && !ticket.get("opendiscord:closed").value + const enabled = (disableOnClaim || disableWhenNotClosed || !ticket.get("opendiscord:autodelete-enabled").value) ? false : ticket.option.get("opendiscord:autodelete-enable-leave") if (enabled){ //autodelete ticket diff --git a/src/data/framework/configLoader.ts b/src/data/framework/configLoader.ts index c39b045..98945d0 100644 --- a/src/data/framework/configLoader.ts +++ b/src/data/framework/configLoader.ts @@ -74,7 +74,6 @@ export const defaultGeneralFormatter = new fjs.ObjectFormatter(null,true,[ new fjs.TextFormatter(""), new fjs.PropertyFormatter("replyOnTicketCreation"), new fjs.PropertyFormatter("replyOnReactionRole"), - new fjs.PropertyFormatter("showPreAutocloseWarning"), new fjs.PropertyFormatter("askPriorityOnTicketCreation"), new fjs.PropertyFormatter("removeParticipantsOnClose"), new fjs.PropertyFormatter("disableAutocloseAfterReopen"), From f7cda48ed6c6da93f994ff6db24fe1a51eb677be Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:07:18 +0100 Subject: [PATCH 72/78] Improved LICENSE Terminology --- LICENSE.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index a79bc68..c40fc44 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,12 @@ + # Open Ticket (License) © 2025 - DJj123dj & Contributors +#### Table Of Contents +- [General License](#general-license) +- [Additional Terms for Open Ticket](#additional-terms-for-open-ticket) + +### General License ``` GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 @@ -679,11 +685,28 @@ Public License instead of this License. But first, please read ``` ### Additional Terms for Open Ticket +#### Terminology: +1. **“Software”** +Refers to the Open Ticket source code, documentation, and any associated files distributed under this license, including any modifications or derivative works thereof. + +1. **“Public Discord Bot** +A Discord bot that is made publicly available for use by multiple, unrelated Discord servers or users, where the bot’s functionality is provided as a service rather than for private or personal use. + +1. **“Monetized Service”** +Refers to any service or deployment that generates direct or indirect revenue through the use of the Software. This includes, but is not limited to, subscriptions, premium features, paid hosting, advertisements. + +1. **“For-Profit Discord Server”** +Means a Discord server that generates income, revenue, or other material benefits, whether through memberships, donations, or other monetization strategies. + +1. **“Private Use”** +Refers to using the Software on a limited number of Discord servers controlled or operated by a single individual or organization, not made available as a public service to multiple unrelated users. + +#### Additional Terms: The following additional terms apply to the use of this software: -1. This software may not be used as part of a public Discord bot service that is monetized (e.g., generating revenue through subscriptions, donations, or other forms of payment) without the prior written permission of the original author. +1. This software may not be used as (or part of) a public Discord bot that is monetized (e.g., generating revenue through subscriptions, donations, or other forms of payment) without the prior written permission of the original author. -2. This software may not be used as part of a public Discord bot service on multiple Discord servers without attribution to the original author. +2. This software may not be used as (or part of) a public Discord bot on multiple Discord servers without attribution to the original author. 3. You are allowed to use this bot in a Discord server that makes money or is for-profit, provided that the income is not generated directly from the ticket bot or its functionality. From 4147658575e1ad98e07a5ebab44e2a1fca8d7663 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:42:26 +0100 Subject: [PATCH 73/78] README Updates --- .eggs/README.md | 4 +- .github/CONTRIBUTING.md | 4 +- .github/SECURITY.md | 4 +- README.md | 114 +++++++++++++++++++++------------------- 4 files changed, 66 insertions(+), 60 deletions(-) diff --git a/.eggs/README.md b/.eggs/README.md index 576d737..11f0ae8 100644 --- a/.eggs/README.md +++ b/.eggs/README.md @@ -57,6 +57,6 @@ It's recommended to provide at least `1GB` of **Memory/RAM** and `5GB` of **disk Open Ticket Logo **Pterodactyl Eggs**
-[changelog](https://otgithub.dj-dj.be/releases) - [documentation](https://otdocs.dj-dj.be) - [tutorial](https://www.youtube.com/watch?v=2jK9kAf6ASU) - [website](https://openticket.dj-dj.be) - [discord](https://discord.dj-dj.be)
+[Changelog](https://otgithub.dj-dj.be/releases) - [Documentation](https://otdocs.dj-dj.be) - [Website](https://openticket.dj-dj.be) - [Support Server](https://discord.dj-dj.be) - [License](./LICENSE.md)
-© 2025 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms#terms) - [Privacy Policy](https://www.dj-dj.be/terms#privacy) \ No newline at end of file +© 2025 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms) - [Privacy Policy](https://www.dj-dj.be/privacy) - [Support Us](https://github.com/sponsors/DJj123dj) \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5b8629a..c2241d5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -87,6 +87,6 @@ you are **REQUIRED to send the bug privately via one of the following methods:** Open Ticket Logo **Contributing Guidelines**
-[changelog](https://otgithub.dj-dj.be/releases) - [documentation](https://otdocs.dj-dj.be) - [tutorial](https://www.youtube.com/watch?v=2jK9kAf6ASU) - [website](https://openticket.dj-dj.be) - [discord](https://discord.dj-dj.be)
+[Changelog](https://otgithub.dj-dj.be/releases) - [Documentation](https://otdocs.dj-dj.be) - [Website](https://openticket.dj-dj.be) - [Support Server](https://discord.dj-dj.be) - [License](./LICENSE.md)
-© 2025 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms#terms) - [Privacy Policy](https://www.dj-dj.be/terms#privacy) \ No newline at end of file +© 2025 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms) - [Privacy Policy](https://www.dj-dj.be/privacy) - [Support Us](https://github.com/sponsors/DJj123dj) \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md index e4ae1aa..36e9aaf 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -47,6 +47,6 @@ If possible, try to provide screenshots! Open Ticket Logo **Security Policy**
-[changelog](https://otgithub.dj-dj.be/releases) - [documentation](https://otdocs.dj-dj.be) - [tutorial](https://www.youtube.com/watch?v=2jK9kAf6ASU) - [website](https://openticket.dj-dj.be) - [discord](https://discord.dj-dj.be)
+[Changelog](https://otgithub.dj-dj.be/releases) - [Documentation](https://otdocs.dj-dj.be) - [Website](https://openticket.dj-dj.be) - [Support Server](https://discord.dj-dj.be) - [License](./LICENSE.md)
-© 2025 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms#terms) - [Privacy Policy](https://www.dj-dj.be/terms#privacy) \ No newline at end of file +© 2025 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms) - [Privacy Policy](https://www.dj-dj.be/privacy) - [Support Us](https://github.com/sponsors/DJj123dj) \ No newline at end of file diff --git a/README.md b/README.md index c4f5e31..aaaf611 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -> ### 🎥 Content Creators 🎥 -> Hey there! We're searching for content creators that would want to create a tutorial or setup guide for Open Ticket!
+> ### 🎥 Content Creators +> Hey there! We're searching for content creators that would like to create a tutorial or setup guide for Open Ticket!
> [📌 More Information](.github/CONTENT_CREATORS.md) --- @@ -26,40 +26,42 @@ The bot is translated in more than 36 Languages and has been battle

⭐️ Help us grow by giving a star! ⭐️

### 📌 Features -- **🦇 pterodactyl support** - Open Ticket works perfect on Pterodactyl based panels! [(Download official eggs)](.eggs/README.md) -- **💩 no bloat** - Your Open Ticket bot doesn't contain any form of bloat or credits! -- **🔒 tested & secure** - Open Ticket has been battletested by thousands of servers and is very secure! -- **📈 scalable** - Open Ticket is made to handle huge servers! (Already tested in **servers with 100k members**) -- **📄 HTML transcripts** - Make use of the most customisable, beautiful and easy-to-use HTML Transcripts! -- **✅ ticket actions** - Close, Reopen, Delete, Claim, Pin, Rename & Move all your tickets! -- **🇬🇧 translation** - Open Ticket has been translated in more than **36 languages** by our community! -- **🎨 customisation** - Open Ticket has been created around customisation, everything can be customised! -- **🖥️ interactions** - The bot has full support for Buttons, Dropdowns, Slash Commands and Modals! -- **∞ unlimited** - Create an infinite amount of tickets & panels! -- **📝 advanced plugins** - Create advanced plugins or use pre-made ones by our community! -- **👥 user management** - Add & Remove users from all tickets! -- **📊 detailed stats** - Open Ticket has ticket, user & global staticstics available for everyone! -- **🚫 blacklist** - Blacklist users to prevent them from creating a ticket! -- **❓ questions** - Let users answer questions in a modal before the ticket is created! -- **📦 slash & text** - Open Ticket supports both slash & text commands! -- **📥 extra buttons** - The bot also supports Reaction Roles & Url Buttons, because why not ¯\\_(ツ)_/¯ -- **✨ commands** - The bot contains more than 25 commands! +- **⏳ Quick Setup** - Using the interactive Quick Setup CLI, you can **configure Open Ticket in less than 5min!** +- **🦇 Pterodactyl Support** - Open Ticket works perfect on Pterodactyl based panels. [(Download official eggs)](.eggs/README.md) +- **💩 No Credits** - Your bot won't contain any form of bloat or credits. It's all yours! +- **🔒 Private & Secure** - It has been battletested by thousands of servers and **respects security & privacy.** +- **📈 Scalable** - Made to handle huge servers and has already been **tested in servers with 100k members.** +- **📄 HTML Transcripts** - The **built-in HTML Transcripts Service** provides beautiful & easy-to-use transcripts. +- **✅ Ticket Status** - Close, reopen, delete, claim, pin, rename or move tickets in your server. +- **🇬🇧 Translation** - Every message has been translated in more than **36 languages** by our community. +- **🎨 Customisation** - More than **200+ settings** are related to customisation & advanced features. +- **🖥️ Interactions** - The bot has full support for buttons, dropdowns, slash/text commands & modals. +- **∞ Unlimited Possibilities** - Create an infinite amount of tickets, questions & panels. +- **📝 Advanced Plugins** - Create advanced plugins or use [**pre-made plugins**](#-plugins) by our community. +- **👥 Participants** - Add or remove participants & transfer ownership from one user to another. +- **📊 Detailed Statistics** - With more than **50+ statistics** for tickets, users & the server. +- **🚫 Blacklist** - Blacklist users to prevent them from creating new tickets. +- **🚨 Priorities** - Assign different **priority levels** to tickets to mark them as important. +- **❓ Modal Questions** - Give users the ability to **answer questions** in a modal before their ticket is created. +- **✨ Commands** - Manage all your tickets with more than 28+ commands. +- **🤖 Automation** - Automate ticket handling with **autoclose, autodelete** & slow mode. +- **😎 Additional Features** - For some weird reason, the bot also supports Reaction Role & URL Buttons. -#### And more using plugins! - - **💬 reviews** - Create & customise your own review system! - - **📢 feedback** - Collect feedback & create forms for people to answer! - - **🏷️ tags** - Create tags & answer questions automatically using keywords! - - **📝 forms** - Create advanced forms and ask people for additional details! - - **🔄 rotating status** - Create a rotating bot status & use dynamic variables from the bot! - - **💾 sqlite database** - Use an `sqlite` database for increased performances! - - **🎉 custom embeds** - Create your own embeds and use them in your server! - - **⏰ reminders** - Create & manage customisable reminders in your server! - - **🎨 customisation** - Yep, you heard it right! Even more customisation! +#### And even more using [pre-made community plugins](#-plugins)! + - **💬 Reviews** - Create & manage a support review system. + - **📢 Feedback** - Collect feedback & create forms for users to answer. + - **⏰ Reminders** - Create & manage customisable reminders. + - **🏷️ Tags** - Create tags & answer questions automatically using keywords. + - **📝 Forms** - Create advanced forms and automatically ask for repetitive questions. + - **🔄 Channel Display** - Create a voice channel with realtime statistics from the ticket system. + - **💾 SQLite Database** - Use an `SQLite` database for increased performance. + - **🎉 Custom Embeds** - Create your own embeds and send them using a command. + - **🎨 Customisation** - Yep, you heard it right. Even more customisation! - **😁 And so much more...** > ### 📦 Resources -> Not all resources are accurate yet! We are working on this.
-> Open Ticket Tutorial +> These resources will help with configuration and usage of the bot:
+> > Open Ticket Docs > Open Ticket Plugins @@ -83,19 +85,19 @@ A big thanks to all our sponsors! Without them, it wouldn't be possible to creat BENZORICH -### ⏱️ Quick Setup +### ⏱️ Quick Setup (Using CLI) > 1. Download the latest version of Open Ticket on [Github](https://github.com/open-discord-bots/open-ticket). -> 2. Make sure node.js & npm are installed using `node -v` (minimum `v18`). +> 2. Make sure node.js & npm are installed using `node -v` (minimum `v20`). > 3. Install any required dependencies using `npm install`. -> 4. Configure the bot in the `./config/` directory. -> 5. Start the bot using `npm start` or `node index.js` +> 4. Start the **Quick Setup CLI** using `npm run setup`. +> 5. Click on `> ⏱️ Quick Setup` and follow the instructions. +> 6. Start the bot using `npm start` or `node index.js` > - The bot will let you know any existing config errors. > - Fix these errors and restart the bot. -> 6. Enjoy using Open Ticket! +> 7. Enjoy using Open Ticket! > ### [📔 Visit Documentation](https://otdocs.dj-dj.be) > > ### 🖥️ Recommended Hostings -> - [⭐ **Peakhosting.nl**](https://peakhosting.nl/) - Official support for Open Ticket pterodactyl eggs. > - **Any Pterodactyl-Based Panel** - Easy installation & configuration. > - **A Virtual Private Server (VPS)** - Extra customisation & more stability. Recommended for large servers. > @@ -127,9 +129,13 @@ A list of people that contributed or provided the most support for Open Ticket. ### 💬 Translators -With the amazing support of our translators, we've been able to translate Open Ticket in more than **36 languages**! -As a result, you're able to enjoy using Open Ticket in your own native language. -- **Categories: 🟢 Available - 🤖 Made Using AI - 🔴 Unavailable/Outdated** +With the amazing support of our translators, we've been able to translate Open Ticket in more than **36 languages**! +#### Categories: +- **🟢 Available** +- **⏳ In Progress (Incomplete)** +- **🤖 Made Using AI** +- **🟠 Incomplete** +- **🔴 Unavailable/Outdated** |🔍 |Languages (36) |Maintainer (Github/Discord) | |---|---------------------|--------------------------------| @@ -172,7 +178,7 @@ As a result, you're able to enjoy using Open Ticket in your own native language. |🔴 |🇨🇳 Traditional Chinese|[⭐ Contribute!](.github/CONTRIBUTING.md)| ## ⭐️ Star History -Please give this repository a star if you like it. +If you enjoy using Open ticket, **consider starring** this repository. This will help us grow and reach even more people! @@ -184,13 +190,21 @@ This will help us grow and reach even more people! ## 🧩 Plugins -**Download all plugins in our [Official Plugin Repository](https://github.com/open-discord-bots/plugins)!**
+**Download all plugins from our [Official Plugin Repository](https://github.com/open-discord-bots/plugins)!**
> #### ⭐ Featured Plugins (Top 5 Most Used) > **[`ot-sqlite-database`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-sqlite-database/), -> [`ot-migrate-v3`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-migrate-v3/), > [`ot-reviews`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-reviews/), > [`ot-feedback`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-feedback/), -> [`ot-tags`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-tags/)** +> [`ot-tags`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-tags/), +> [`ot-config-reload`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-config-reload/)** + +#### Categories: +- **📄 Example** - These plugins serve as an example or starting template. +- **📢 Command** - These plugins add new commands to the bot. +- **⚙️ Utility** - These plugins help with utility systems. You might not notice them as a ticket user/admin directly. +- **🎨 Customisation** - These plugins add even more customisation to the bot. +- **💼 Management** - These plugins add features that help you manage your server or ticket system. +- **🤖 Client** - These plugins add features affecting the Discord Client or bot itself. ### 📦 Official *(made by DJdj Development)* |Name |Category |Description | @@ -227,18 +241,10 @@ This will help us grow and reach even more people! |[`od-reminders`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/od-reminders/) |guillee.3 |💼 Management |Set reminders that will be sent to a channel every specified time. | |[`ot-translate-cmds`](https://github.com/open-discord-bots/plugins/tree/main/open-ticket/ot-translate-cmds/) |guillee.3 |🤖 Client |Translate all built-in command names, descriptions & options. | -### 📢 Categories -- **📄 Example** - These plugins serve as an example or starting template. -- **📢 Command** - These plugins add new commands to the bot. -- **⚙️ Utility** - These plugins help with backend & systems. You won't notice it in discord itself. -- **🎨 Customisation** - These plugins help you customise the bot even further. -- **💼 Management** - These plugins add features that help you manage your server. -- **🤖 Client** - These plugins add features affecting the discord client or bot itself. - --- Open Ticket Logo **README.md**
-[changelog](https://otgithub.dj-dj.be/releases) - [documentation](https://otdocs.dj-dj.be) - [tutorial](https://www.youtube.com/watch?v=2jK9kAf6ASU) - [website](https://openticket.dj-dj.be) - [discord server](https://discord.dj-dj.be) - [license](./LICENSE.md)
+[Changelog](https://otgithub.dj-dj.be/releases) - [Documentation](https://otdocs.dj-dj.be) - [Website](https://openticket.dj-dj.be) - [Support Server](https://discord.dj-dj.be) - [License](./LICENSE.md)
© 2025 - [DJdj Development](https://www.dj-dj.be) - [Terms](https://www.dj-dj.be/terms) - [Privacy Policy](https://www.dj-dj.be/privacy) - [Support Us](https://github.com/sponsors/DJj123dj) From 5494c1ad8fa88935d1d8cac9c9fcfd8ce95003db Mon Sep 17 00:00:00 2001 From: JasperAtSchool Date: Wed, 5 Nov 2025 14:53:05 +0100 Subject: [PATCH 74/78] (v4.1) Minor Bugfixes --- src/core/api/defaults/helpmenu.ts | 9 +++++++-- src/core/api/modules/checker.ts | 2 +- src/data/framework/checkerLoader.ts | 2 +- src/data/framework/commandLoader.ts | 4 ++-- src/data/framework/helpMenuLoader.ts | 22 +++++++++++++++------- src/data/framework/statLoader.ts | 2 +- 6 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/core/api/defaults/helpmenu.ts b/src/core/api/defaults/helpmenu.ts index d8a3e84..9864f18 100644 --- a/src/core/api/defaults/helpmenu.ts +++ b/src/core/api/defaults/helpmenu.ts @@ -182,7 +182,8 @@ export interface ODHelpMenuManagerCategoryIds_DefaultTicketUser { "opendiscord:claim":ODHelpMenuCommandComponent, "opendiscord:unclaim":ODHelpMenuCommandComponent, "opendiscord:add":ODHelpMenuCommandComponent, - "opendiscord:remove":ODHelpMenuCommandComponent + "opendiscord:remove":ODHelpMenuCommandComponent, + "opendiscord:transfer":ODHelpMenuCommandComponent, } /**## ODHelpMenuCategory_DefaultTicketUser `default_class` @@ -265,7 +266,11 @@ export interface ODHelpMenuManagerCategoryIds_DefaultAdvanced { "opendiscord:stats-ticket":ODHelpMenuCommandComponent, "opendiscord:stats-user":ODHelpMenuCommandComponent, "opendiscord:autoclose-disable":ODHelpMenuCommandComponent, - "opendiscord:autoclose-enable":ODHelpMenuCommandComponent + "opendiscord:autoclose-enable":ODHelpMenuCommandComponent, + "opendiscord:autodelete-disable":ODHelpMenuCommandComponent, + "opendiscord:autodelete-enable":ODHelpMenuCommandComponent, + "opendiscord:topic-set":ODHelpMenuCommandComponent, + "opendiscord:priority-set":ODHelpMenuCommandComponent, } /**## ODHelpMenuCategory_DefaultAdvanced `default_class` diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index d776f79..ceb2e84 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -697,7 +697,7 @@ export class ODCheckerNumberStructure extends ODCheckerStructure { checker.createMessage("opendiscord:invalid-type","error","This property needs to be the type: number!",lt,null,["number"],this.id,(this.options.docs ?? null)) return false }else if (!this.options.nanAllowed && isNaN(value)){ - checker.createMessage("opendiscord:number-nan","error",`This number can't NaN (Not A Number)!`,lt,null,[],this.id,(this.options.docs ?? null)) + checker.createMessage("opendiscord:number-nan","error",`This number can't be NaN (Not A Number)!`,lt,null,[],this.id,(this.options.docs ?? null)) return false }else if (typeof this.options.minLength != "undefined" && value.toString().length < this.options.minLength){ checker.createMessage("opendiscord:number-too-short","error",`This number can't be shorter than ${this.options.minLength} characters!`,lt,null,[this.options.minLength.toString()],this.id,(this.options.docs ?? null)) diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index c34c907..979f663 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -89,7 +89,7 @@ export const registerDefaultCheckerMessageTranslations = (tm:api.ODCheckerTransl tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-capital-sentence") // It looks like some sentences in this string don't start with a capital letter! tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-punctuation") // It looks like the sentence in this string doesn't end with a punctuation mark! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:number-nan") // This number can't NaN (Not A Number)! + tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:number-nan") // This number can't be NaN (Not A Number)! tm.quickTranslate(lm,"checker.messages.numberTooShort","message","opendiscord:number-too-short") // This number can't be shorter than {0} characters! tm.quickTranslate(lm,"checker.messages.numberTooLong","message","opendiscord:number-too-long") // This number can't be longer than {0} characters! tm.quickTranslate(lm,"checker.messages.numberLengthInvalid","message","opendiscord:number-length-invalid") // This number needs to be {0} characters long! diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index a72d965..bae7f65 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -560,7 +560,7 @@ export const loadAllSlashCommands = async () => { if (allowedCommands.includes("topic")) commands.add(new api.ODSlashCommand("opendiscord:topic",{ type:act.ChatInput, name:"topic", - description:"Change the topic of the ticket channel.", //TODO TRANSLATION!!! + description:"Manage the topic of the ticket channel.", //TODO TRANSLATION!!! contexts:[discord.InteractionContextType.Guild], integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], options:[ @@ -585,7 +585,7 @@ export const loadAllSlashCommands = async () => { if (allowedCommands.includes("priority")) commands.add(new api.ODSlashCommand("opendiscord:priority",{ type:act.ChatInput, name:"priority", - description:"Set the priority of the ticket.", //TODO TRANSLATION!!! + description:"Manage the priority of the ticket.", //TODO TRANSLATION!!! contexts:[discord.InteractionContextType.Guild], integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], options:[ diff --git a/src/data/framework/helpMenuLoader.ts b/src/data/framework/helpMenuLoader.ts index 3cc1917..65c1668 100644 --- a/src/data/framework/helpMenuLoader.ts +++ b/src/data/framework/helpMenuLoader.ts @@ -127,7 +127,7 @@ export const loadAllHelpMenuComponents = async () => { })) } - const ticketUser = helpmenu.get("opendiscord:ticket-channel") + const ticketUser = helpmenu.get("opendiscord:ticket-user") if (ticketUser){ if (allowedCommands.includes("claim")) ticketUser.add(new api.ODHelpMenuCommandComponent("opendiscord:claim",7,{ textName:prefix+"claim", @@ -161,6 +161,14 @@ export const loadAllHelpMenuComponents = async () => { textOptions:[{name:"user",optional:false},{name:"reason",optional:true}], slashOptions:[{name:"user",optional:false},{name:"reason",optional:true}] })) + if (allowedCommands.includes("transfer")) ticketUser.add(new api.ODHelpMenuCommandComponent("opendiscord:transfer",-1,{ + textName:prefix+"transfer", + textDescription:"Transfer the ticket ownership from one user to another.", //TODO TRANSLATION!!! + slashName:"/transfer", + slashDescription:"Transfer the ticket ownership from one user to another.", //TODO TRANSLATION!!! + textOptions:[{name:"user",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"user",optional:false},{name:"reason",optional:true}] + })) } const admin = helpmenu.get("opendiscord:admin") @@ -271,19 +279,19 @@ export const loadAllHelpMenuComponents = async () => { })) if (allowedCommands.includes("topic")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:topic-set",1,{ textName:prefix+"topic set", - textDescription:"Change the topic of the ticket channel.", //TODO TRANSLATION!!! + textDescription:"Set the topic of the ticket channel.", //TODO TRANSLATION!!! slashName:"/topic set", - slashDescription:"Change the topic of the ticket channel.", //TODO TRANSLATION!!! + slashDescription:"Manage the topic of the ticket channel.", //TODO TRANSLATION!!! textOptions:[{name:"topic",optional:false}], slashOptions:[{name:"topic",optional:false}] })) if (allowedCommands.includes("priority")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:priority-set",0,{ textName:prefix+"priority set", - textDescription:"Set the priority of the ticket channel.", //TODO TRANSLATION!!! + textDescription:"Set the priority of the ticket.", //TODO TRANSLATION!!! slashName:"/priority set", - slashDescription:"Set the priority of the ticket channel.", //TODO TRANSLATION!!! - textOptions:[{name:"priority",optional:false}], - slashOptions:[{name:"priority",optional:false}] + slashDescription:"Manage the priority of the ticket.", //TODO TRANSLATION!!! + textOptions:[{name:"priority",optional:false},{name:"reason",optional:true}], + slashOptions:[{name:"priority",optional:false},{name:"reason",optional:true}] })) } } \ No newline at end of file diff --git a/src/data/framework/statLoader.ts b/src/data/framework/statLoader.ts index 44263d4..a9b6b03 100644 --- a/src/data/framework/statLoader.ts +++ b/src/data/framework/statLoader.ts @@ -128,7 +128,7 @@ export const loadAllStats = async () => { const creator = ticket.get("opendiscord:opened-by").value return lang.getTranslation("params.uppercase.creator")+": "+ (creator ? discord.userMention(creator) : "`unknown`") })) - ticket.add(new api.ODDynamicStat("opendiscord:ticket-age",1,async (scopeId,guild,channel,user) => { + ticket.add(new api.ODDynamicStat("opendiscord:ticket-age",-1,async (scopeId,guild,channel,user) => { const ticket = opendiscord.tickets.get(scopeId) if (!ticket) return "" From f835850b3f007b4cc5f6a1649f130c061a7061fb Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:50:57 +0100 Subject: [PATCH 75/78] Translated code strings using language manager --- src/actions/closeTicket.ts | 13 +-- src/actions/createTicket.ts | 13 +-- src/actions/deleteTicket.ts | 17 +-- src/actions/updateTicketTopic.ts | 13 +-- src/builders/embeds.ts | 33 +++--- src/commands/close.ts | 5 +- src/commands/delete.ts | 5 +- src/commands/ticket.ts | 7 +- src/core/api/defaults/language.ts | 133 +++++++++++++++++++++++- src/data/framework/checkerLoader.ts | 24 ++--- src/data/framework/commandLoader.ts | 18 ++-- src/data/framework/helpMenuLoader.ts | 26 ++--- src/data/framework/statLoader.ts | 49 ++++----- src/data/openticket/panelLoader.ts | 31 ++---- src/data/openticket/priorityLoader.ts | 16 +-- src/data/openticket/transcriptLoader.ts | 69 ++++++------ 16 files changed, 293 insertions(+), 179 deletions(-) diff --git a/src/actions/closeTicket.ts b/src/actions/closeTicket.ts index 6742e38..083a903 100644 --- a/src/actions/closeTicket.ts +++ b/src/actions/closeTicket.ts @@ -5,6 +5,7 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" const generalConfig = opendiscord.configs.get("opendiscord:general") +const lang = opendiscord.languages export const registerActions = async () => { opendiscord.actions.add(new api.ODAction("opendiscord:close-ticket")) @@ -214,7 +215,7 @@ export const registerVerifyBars = async () => { } //return when already closed if (ticket.get("opendiscord:closed").value){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,error:opendiscord.languages.getTranslation("errors.actionInvalid.close"),layout:"simple"})) + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,error:lang.getTranslation("errors.actionInvalid.close"),layout:"simple"})) return cancel() } //return when busy @@ -226,11 +227,11 @@ export const registerVerifyBars = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } @@ -313,7 +314,7 @@ export const registerVerifyBars = async () => { } //return when already closed if (ticket.get("opendiscord:closed").value){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,error:opendiscord.languages.getTranslation("errors.actionInvalid.close"),layout:"simple"})) + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,error:lang.getTranslation("errors.actionInvalid.close"),layout:"simple"})) return cancel() } //return when busy @@ -325,11 +326,11 @@ export const registerVerifyBars = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } diff --git a/src/actions/createTicket.ts b/src/actions/createTicket.ts index 918378e..2e62699 100644 --- a/src/actions/createTicket.ts +++ b/src/actions/createTicket.ts @@ -5,6 +5,7 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" const generalConfig = opendiscord.configs.get("opendiscord:general") +const lang = opendiscord.languages export const registerActions = async () => { opendiscord.actions.add(new api.ODAction("opendiscord:create-ticket")) @@ -120,12 +121,12 @@ export const registerActions = async () => { if (generalConfig.data.system.channelTopic.showOptionName) channelTopics.push(option.get("opendiscord:name").value) if (generalConfig.data.system.channelTopic.showOptionDescription) channelTopics.push(option.get("opendiscord:description").value) if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(channelTopicText) - if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**Priority:** "+opendiscord.priorities.get("opendiscord:none").renderDisplayName()) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** Opened") //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** No-one") //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** No") //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**Creator:** "+discord.userMention(user.id)) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**Participants:** "+participants.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**"+lang.getTranslation("params.uppercase.priority")+":** "+opendiscord.priorities.get("opendiscord:none").renderDisplayName()) + if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**"+lang.getTranslation("params.uppercase.status")+":** "+lang.getTranslation("params.uppercase.open")) + if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**"+lang.getTranslation("stats.properties.claimedBy")+":** "+lang.getTranslation("params.uppercase.noone")) + if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**"+lang.getTranslation("params.uppercase.pinned")+":** "+lang.getTranslation("params.uppercase.no")) + if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**"+lang.getTranslation("params.uppercase.creator")+":** "+discord.userMention(user.id)) + if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**"+lang.getTranslation("params.uppercase.participants")+":** "+participants.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //create channel const channel = await guild.channels.create({ diff --git a/src/actions/deleteTicket.ts b/src/actions/deleteTicket.ts index 7a50ddf..07d6996 100644 --- a/src/actions/deleteTicket.ts +++ b/src/actions/deleteTicket.ts @@ -5,6 +5,7 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" const generalConfig = opendiscord.configs.get("opendiscord:general") +const lang = opendiscord.languages export const registerActions = async () => { opendiscord.actions.add(new api.ODAction("opendiscord:delete-ticket")) @@ -179,11 +180,11 @@ export const registerVerifyBars = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } @@ -284,11 +285,11 @@ export const registerVerifyBars = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } @@ -393,11 +394,11 @@ export const registerVerifyBars = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } @@ -502,11 +503,11 @@ export const registerVerifyBars = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } diff --git a/src/actions/updateTicketTopic.ts b/src/actions/updateTicketTopic.ts index d974166..1b544b0 100644 --- a/src/actions/updateTicketTopic.ts +++ b/src/actions/updateTicketTopic.ts @@ -5,6 +5,7 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" const generalConfig = opendiscord.configs.get("opendiscord:general") +const lang = opendiscord.languages export const registerActions = async () => { opendiscord.actions.add(new api.ODAction("opendiscord:update-ticket-topic")) @@ -31,12 +32,12 @@ export const registerActions = async () => { if (generalConfig.data.system.channelTopic.showOptionName) channelTopics.push(ticket.option.get("opendiscord:name").value) if (generalConfig.data.system.channelTopic.showOptionDescription) channelTopics.push(ticket.option.get("opendiscord:description").value) if (generalConfig.data.system.channelTopic.showOptionTopic) channelTopics.push(ticket.get("opendiscord:topic").value) - if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**Priority:** "+opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).renderDisplayName()) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**Status:** "+(closed ? "Closed" : "Opened")) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**Claimed By:** "+(claimedBy ? discord.userMention(claimedBy) : "No-one")) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**Pinned:** "+(pinned ? "Yes" : "No")) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**Creator:** "+discord.userMention(creator)) //TODO TRANSLATION!!! - if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**Participants:** "+ticket.get("opendiscord:participants").value.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //TODO TRANSLATION!!! + if (generalConfig.data.system.channelTopic.showPriority) channelTopics.push("**"+lang.getTranslation("params.uppercase.priority")+":** "+opendiscord.priorities.getFromPriorityLevel(ticket.get("opendiscord:priority").value).renderDisplayName()) + if (generalConfig.data.system.channelTopic.showClosed) channelTopics.push("**"+lang.getTranslation("params.uppercase.status")+":** "+(closed ? lang.getTranslation("params.uppercase.closed") : lang.getTranslation("params.uppercase.open"))) + if (generalConfig.data.system.channelTopic.showClaimed) channelTopics.push("**"+lang.getTranslation("stats.properties.claimedBy")+":** "+(claimedBy ? discord.userMention(claimedBy) : lang.getTranslation("params.uppercase.noone"))) + if (generalConfig.data.system.channelTopic.showPinned) channelTopics.push("**"+lang.getTranslation("params.uppercase.pinned")+":** "+(pinned ? lang.getTranslation("params.uppercase.yes") : lang.getTranslation("params.uppercase.no"))) + if (generalConfig.data.system.channelTopic.showCreator) channelTopics.push("**"+lang.getTranslation("params.uppercase.creator")+":** "+discord.userMention(creator)) + if (generalConfig.data.system.channelTopic.showParticipants) channelTopics.push("**"+lang.getTranslation("params.uppercase.participants")+":** "+ticket.get("opendiscord:participants").value.map((p) => (p.type == "user") ? discord.userMention(p.id) : discord.roleMention(p.id)).join(", ")) //update channel channel.setTopic(channelTopics.join(" • "),"Topic Changed") diff --git a/src/builders/embeds.ts b/src/builders/embeds.ts index 7545ce4..5d7cc2c 100644 --- a/src/builders/embeds.ts +++ b/src/builders/embeds.ts @@ -617,7 +617,7 @@ const ticketEmbeds = () => { if (ticket.get("opendiscord:claimed").value){ const claimUser = await opendiscord.tickets.getTicketUser(ticket,"claimer") if (!claimUser) return - instance.setAuthor(lang.getTranslationWithParams("params.uppercase.claimedBy",[claimUser.displayName]),claimUser.displayAvatarURL()) + instance.setAuthor(lang.getTranslation("stats.properties.claimedBy")+" "+claimUser.displayName,claimUser.displayAvatarURL()) } }) ) @@ -838,8 +838,7 @@ const ticketEmbeds = () => { instance.setTimestamp(new Date()) instance.addFields( {name:lang.getTranslation("params.uppercase.ticket")+":",value:"```#"+(channel ? channel.name : "")+"```",inline:false}, - //TODO TRANSLATION!!! - {name:"Option"+":",value:"```"+(ticket.option.get("opendiscord:name").value)+"```",inline:false}, + {name:lang.getTranslation("params.uppercase.option")+":",value:"```"+(ticket.option.get("opendiscord:name").value)+"```",inline:false}, ) if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```",inline:false}) @@ -1081,7 +1080,7 @@ const transcriptEmbeds = () => { const {guild,channel,user,ticket,compiler,reason} = params instance.setColor(generalConfig.data.system.useRedErrorEmbeds ? "Red" : generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("❌","Transcript Error")) //TODO TRANSLATION!!! + instance.setTitle(utilities.emojiTitle("❌",lang.getTranslation("transcripts.errors.title"))) instance.setTimestamp(new Date()) instance.setDescription(lang.getTranslation("transcripts.errors.error")) instance.setFooter(lang.getTranslation("errors.descriptions.askForInfo")) @@ -1134,8 +1133,7 @@ const roleEmbeds = () => { return (r.action == "added") ? "🟢 "+lang.getTranslation("params.uppercase.added")+" @"+r.role.name : "🔴 "+lang.getTranslation("params.uppercase.removed")+" @"+r.role.name }) - //TODO TRANSLATION!!! - const baseDescription = ("Your roles in our server have been updated!") + const baseDescription = lang.getTranslation("actions.logs.roleUpdateDm") if (newResult.length > 0) instance.setDescription(baseDescription+"\n\n"+newResult.join("\n")) else instance.setDescription(baseDescription+"\n"+lang.getTranslation("actions.descriptions.rolesEmpty")) @@ -1161,8 +1159,7 @@ const roleEmbeds = () => { return (r.action == "added") ? "🟢 "+lang.getTranslation("params.uppercase.added")+" "+discord.roleMention(r.role.id) : "🔴 "+lang.getTranslation("params.uppercase.removed")+" "+discord.roleMention(r.role.id) }) - //TODO TRANSLATION!!! - const baseDescription = ("{0} has updated their roles!").replace("{0}",discord.userMention(user.id)) + const baseDescription = lang.getTranslationWithParams("actions.logs.roleUpdateLog",[discord.userMention(user.id)]) if (newResult.length > 0) instance.setDescription(baseDescription+"\n\n"+newResult.join("\n")) else instance.setDescription(baseDescription+"\n"+lang.getTranslation("actions.descriptions.rolesEmpty")) @@ -1178,7 +1175,7 @@ const clearEmbeds = () => { const {guild,channel,user,filter,list} = params instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("⚠️","Clear Tickets")) //TODO TRANSLATION!!! + instance.setTitle(utilities.emojiTitle("⚠️",lang.getTranslation("actions.titles.clearTickets"))) instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setTimestamp(new Date()) instance.setDescription(lang.getTranslation("actions.descriptions.clearVerify")) @@ -1324,9 +1321,9 @@ const extraEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("ℹ️","Topic Changed")) //TODO TRANSLATION!!! - instance.setDescription("The channel topic has been changed by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! - if (topic) instance.addFields({name:"Topic"+":",value:"```"+topic+"```"}) //TODO TRANSLATION!!! + instance.setTitle(utilities.emojiTitle("ℹ️",lang.getTranslation("actions.titles.topicSet"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.topicSet",[discord.userMention(user.id)])) + if (topic) instance.addFields({name:lang.getTranslation("params.uppercase.topic")+":",value:"```"+topic+"```"}) }) ) @@ -1338,8 +1335,8 @@ const extraEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("🚨","Priority Changed")) //TODO TRANSLATION!!! - instance.setDescription("The ticket priority has been changed to **"+priority.renderDisplayName()+"** by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! + instance.setTitle(utilities.emojiTitle("🚨",lang.getTranslation("actions.titles.prioritySet"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.prioritySet",["**"+priority.renderDisplayName()+"**",discord.userMention(user.id)])) if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) @@ -1352,8 +1349,8 @@ const extraEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("🚨","Ticket Priority")) //TODO TRANSLATION!!! - instance.setDescription("The current priority of this ticket is **"+priority.renderDisplayName()+"**!") //TODO TRANSLATION!!! + instance.setTitle(utilities.emojiTitle("🚨",lang.getTranslation("actions.titles.priorityGet"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.priorityGet",["**"+priority.renderDisplayName()+"**"])) }) ) @@ -1365,8 +1362,8 @@ const extraEmbeds = () => { instance.setAuthor(user.displayName,user.displayAvatarURL()) instance.setColor(generalConfig.data.mainColor) - instance.setTitle(utilities.emojiTitle("🔀","Ticket Transferred")) //TODO TRANSLATION!!! - instance.setDescription("The ticket ownership has been transferred from "+discord.userMention(oldCreator.id)+" to "+discord.userMention(newCreator.id)+" by "+discord.userMention(user.id)+" successfully!") //TODO TRANSLATION!!! + instance.setTitle(utilities.emojiTitle("🔀",lang.getTranslation("actions.titles.transfer"))) + instance.setDescription(lang.getTranslationWithParams("actions.descriptions.transfer",[discord.userMention(oldCreator.id),discord.userMention(newCreator.id),discord.userMention(user.id)])) if (reason || generalConfig.data.system.alwaysShowReason) instance.addFields({name:lang.getTranslation("params.uppercase.reason")+":",value:"```"+(reason ?? "/")+"```"}) }) ) diff --git a/src/commands/close.ts b/src/commands/close.ts index 56acc36..8aac86a 100644 --- a/src/commands/close.ts +++ b/src/commands/close.ts @@ -5,6 +5,7 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" const generalConfig = opendiscord.configs.get("opendiscord:general") +const lang = opendiscord.languages export const registerCommandResponders = async () => { //CLOSE COMMAND RESPONDER @@ -68,11 +69,11 @@ export const registerCommandResponders = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } diff --git a/src/commands/delete.ts b/src/commands/delete.ts index 0b08657..24ea2ec 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -5,6 +5,7 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" const generalConfig = opendiscord.configs.get("opendiscord:general") +const lang = opendiscord.languages export const registerCommandResponders = async () => { //DELETE COMMAND RESPONDER @@ -63,11 +64,11 @@ export const registerCommandResponders = async () => { if (!generalConfig.data.system.allowCloseBeforeMessage || !generalConfig.data.system.allowCloseBeforeAdminMessage){ const analysis = await opendiscord.transcripts.collector.ticketUserMessagesAnalysis(ticket,guild,channel) if (analysis && !generalConfig.data.system.allowCloseBeforeMessage && analysis.totalMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a user.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } if (analysis && !generalConfig.data.system.allowCloseBeforeAdminMessage && analysis.adminMessages < 1){ - instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.",customTitle:opendiscord.languages.getTranslation("errors.titles.noPermissions")})) //TODO TRANSLATION!!! + instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build("button",{guild,channel,user,layout:"simple",error:lang.getTranslation("errors.descriptions.closeBeforeAdminMessage"),customTitle:lang.getTranslation("errors.titles.noPermissions")})) return cancel() } } diff --git a/src/commands/ticket.ts b/src/commands/ticket.ts index 3de8693..45fcddb 100644 --- a/src/commands/ticket.ts +++ b/src/commands/ticket.ts @@ -5,6 +5,7 @@ import {opendiscord, api, utilities} from "../index" import * as discord from "discord.js" const generalConfig = opendiscord.configs.get("opendiscord:general") +const lang = opendiscord.languages export const registerCommandResponders = async () => { //TICKET COMMAND RESPONDER @@ -70,7 +71,7 @@ export const registerCommandResponders = async () => { else if (res.reason == "global-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) else if (res.reason == "option-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) else if (res.reason == "option-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) - else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? "You are unable to create a ticket. `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:"Permission Error"})) //TODO TRANSLATION!!! + else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? lang.getTranslation("errors.descriptions.unableToCreateTicket")+" `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:lang.getTranslation("errors.titles.permissionError")})) else instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) return cancel() } @@ -133,7 +134,7 @@ export const registerButtonResponders = async () => { else if (res.reason == "global-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) else if (res.reason == "option-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) else if (res.reason == "option-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) - else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? "You are unable to create a ticket. `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:"Permission Error"})) //TODO TRANSLATION!!! + else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? lang.getTranslation("errors.descriptions.unableToCreateTicket")+" `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:lang.getTranslation("errors.titles.permissionError")})) else instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) return cancel() } @@ -189,7 +190,7 @@ export const registerDropdownResponders = async () => { else if (res.reason == "global-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"global-user"})) else if (res.reason == "option-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option"})) else if (res.reason == "option-user-limit") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error-no-permissions-limits").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,limit:"option-user"})) - else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? "You are unable to create a ticket. `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:"Permission Error"})) //TODO TRANSLATION!!! + else if (res.reason == "custom") instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,layout:"simple",error:res.customReason ?? lang.getTranslation("errors.descriptions.unableToCreateTicket")+" `Unknown invalid_permission_reason => no reason specified by plugin`",customTitle:lang.getTranslation("errors.titles.permissionError")})) else instance.reply(await opendiscord.builders.messages.getSafe("opendiscord:error").build(source,{guild:instance.guild,channel:instance.channel,user:instance.user,error:"Unknown invalid_permission reason => calculation failed #1",layout:"advanced"})) return cancel() } diff --git a/src/core/api/defaults/language.ts b/src/core/api/defaults/language.ts index 65573e6..64270e2 100644 --- a/src/core/api/defaults/language.ts +++ b/src/core/api/defaults/language.ts @@ -76,6 +76,7 @@ export type ODLanguageManagerTranslations_Default = ( "checker.system.dataPath"| "checker.system.dataDocs"| "checker.system.dataMessages"| + "checker.messages.invalidType"| "checker.messages.propertyMissing"| "checker.messages.propertyOptional"| @@ -83,6 +84,7 @@ export type ODLanguageManagerTranslations_Default = ( "checker.messages.nullInvalid"| "checker.messages.switchInvalidType"| "checker.messages.objectSwitchInvalid"| + "checker.messages.stringTooShort"| "checker.messages.stringTooLong"| "checker.messages.stringLengthInvalid"| @@ -91,6 +93,15 @@ export type ODLanguageManagerTranslations_Default = ( "checker.messages.stringContains"| "checker.messages.stringChoices"| "checker.messages.stringRegex"| + "checker.messages.stringInvertedContains"| + "checker.messages.stringLowercase"| + "checker.messages.stringUppercase"| + "checker.messages.stringSpecialCharacters"| + "checker.messages.stringNoSpaces"| + "checker.messages.stringCapitalWord"| + "checker.messages.stringCapitalSentence"| + "checker.messages.stringPunctuation"| + "checker.messages.numberTooShort"| "checker.messages.numberTooLong"| "checker.messages.numberLengthInvalid"| @@ -107,8 +118,12 @@ export type ODLanguageManagerTranslations_Default = ( "checker.messages.numberNegative"| "checker.messages.numberPositive"| "checker.messages.numberZero"| + "checker.messages.numberNan"| + "checker.messages.numberInvertedContains"| + "checker.messages.booleanTrue"| "checker.messages.booleanFalse"| + "checker.messages.arrayEmptyDisabled"| "checker.messages.arrayEmptyRequired"| "checker.messages.arrayTooShort"| @@ -116,6 +131,7 @@ export type ODLanguageManagerTranslations_Default = ( "checker.messages.arrayLengthInvalid"| "checker.messages.arrayInvalidTypes"| "checker.messages.arrayDouble"| + "checker.messages.discordInvalidId"| "checker.messages.discordInvalidIdOptions"| "checker.messages.discordInvalidToken"| @@ -137,6 +153,8 @@ export type ODLanguageManagerTranslations_Default = ( "checker.messages.unusedOption"| "checker.messages.unusedQuestion"| "checker.messages.dropdownOption"| + "checker.messages.customInvalidVersion"| + "actions.buttons.create"| "actions.buttons.close"| "actions.buttons.delete"| @@ -174,6 +192,7 @@ export type ODLanguageManagerTranslations_Default = ( "actions.titles.blacklistAddDm"| "actions.titles.blacklistRemoveDm"| "actions.titles.clear"| + "actions.titles.clearTickets"| "actions.titles.roles"| "actions.titles.autoclose"| @@ -183,6 +202,11 @@ export type ODLanguageManagerTranslations_Default = ( "actions.titles.autodeleteEnabled"| "actions.titles.autodeleteDisabled"| + "actions.titles.topicSet"| + "actions.titles.prioritySet"| + "actions.titles.priorityGet"| + "actions.titles.transfer"| + "actions.descriptions.create"| "actions.descriptions.close"| "actions.descriptions.delete"| @@ -223,6 +247,11 @@ export type ODLanguageManagerTranslations_Default = ( "actions.descriptions.ticketMessageAutodelete"| "actions.descriptions.panelReady"| + "actions.descriptions.topicSet"| + "actions.descriptions.prioritySet"| + "actions.descriptions.priorityGet"| + "actions.descriptions.transfer"| + "actions.modal.closePlaceholder"| "actions.modal.deletePlaceholder"| "actions.modal.reopenPlaceholder"| @@ -261,6 +290,13 @@ export type ODLanguageManagerTranslations_Default = ( "actions.logs.blacklistRemoveDm"| "actions.logs.clearLog"| + "actions.logs.transferLog"| + "actions.logs.transferDm"| + "actions.logs.prioritySetLog"| + "actions.logs.prioritySetDm"| + "actions.logs.roleUpdateLog"| + "actions.logs.roleUpdateDm"| + "transcripts.success.visit"| "transcripts.success.ready"| "transcripts.success.textFileDescription"| @@ -277,6 +313,17 @@ export type ODLanguageManagerTranslations_Default = ( "transcripts.errors.continue"| "transcripts.errors.backup"| "transcripts.errors.error"| + "transcripts.errors.title"| + + "transcripts.text.messagesTitle"| + "transcripts.text.embedTitle"| + "transcripts.text.fileTitle"| + "transcripts.text.fieldsTitle"| + "transcripts.text.reactionsTitle"| + "transcripts.text.statsTitle"| + "transcripts.text.emptyContent"| + "transcripts.text.noTitle"| + "transcripts.text.noDesc"| "errors.titles.internalError"| "errors.titles.optionMissing"| @@ -290,6 +337,7 @@ export type ODLanguageManagerTranslations_Default = ( "errors.titles.notInGuild"| "errors.titles.channelRename"| "errors.titles.busy"| + "errors.titles.permissionError"| "errors.descriptions.askForInfo"| "errors.descriptions.askForInfoResolve"| @@ -312,6 +360,9 @@ export type ODLanguageManagerTranslations_Default = ( "errors.descriptions.channelRename"| "errors.descriptions.channelRenameSource"| "errors.descriptions.busy"| + "errors.descriptions.closeBeforeMessage"| + "errors.descriptions.closeBeforeAdminMessage"| + "errors.descriptions.unableToCreateTicket"| "errors.optionInvalidReasons.stringRegex"| "errors.optionInvalidReasons.stringMinLength"| @@ -356,7 +407,6 @@ export type ODLanguageManagerTranslations_Default = ( "params.uppercase.added"| "params.uppercase.removed"| "params.uppercase.filter"| - "params.uppercase.claimedBy"| "params.uppercase.method"| "params.uppercase.type"| "params.uppercase.blacklisted"| @@ -382,6 +432,27 @@ export type ODLanguageManagerTranslations_Default = ( "params.uppercase.pinned"| "params.uppercase.creationDate"| + "params.uppercase.noone"| + "params.uppercase.open"| + "params.uppercase.closed"| + "params.uppercase.priority"| + "params.uppercase.status"| + "params.uppercase.participants"| + "params.uppercase.yes"| + "params.uppercase.no"| + "params.uppercase.option"| + "params.uppercase.topic"| + "params.uppercase.uptime"| + "params.uppercase.messages"| + "params.uppercase.embeds"| + "params.uppercase.files"| + "params.uppercase.components"| + "params.uppercase.cooldown"| + "params.uppercase.maxTickets"| + "params.uppercase.admins"| + "params.uppercase.roles"| + "params.uppercase.size"| + "params.lowercase.text"| "params.lowercase.html"| "params.lowercase.command"| @@ -448,6 +519,18 @@ export type ODLanguageManagerTranslations_Default = ( "commands.autodeleteEnable"| "commands.autodeleteEnableTime"| + "commands.topic"| + "commands.topicSet"| + "commands.topicValue"| + "commands.topicList"| + "commands.priority"| + "commands.prioritySet"| + "commands.priorityValue"| + "commands.priorityGet"| + "commands.priorityList"| + "commands.transfer"| + "commands.transferUser"| + "helpMenu.help"| "helpMenu.ticket"| "helpMenu.close"| @@ -475,11 +558,20 @@ export type ODLanguageManagerTranslations_Default = ( "helpMenu.autodeleteDisable"| "helpMenu.autodeleteEnable"| + "helpMenu.categories.general"| + "helpMenu.categories.basicTicket"| + "helpMenu.categories.advancedTicket"| + "helpMenu.categories.userTicket"| + "helpMenu.categories.admin"| + "helpMenu.categories.advanced"| + "helpMenu.categories.extra"| + "stats.scopes.global"| "stats.scopes.system"| "stats.scopes.user"| "stats.scopes.ticket"| "stats.scopes.participants"| + "stats.scopes.messages"| "stats.properties.ticketsCreated"| "stats.properties.ticketsClosed"| @@ -490,7 +582,44 @@ export type ODLanguageManagerTranslations_Default = ( "stats.properties.ticketsPinned"| "stats.properties.ticketsMoved"| "stats.properties.usersBlacklisted"| - "stats.properties.transcriptsCreated" + "stats.properties.transcriptsCreated"| + "stats.properties.ticketsAutodeleted"| + "stats.properties.ticketsTransferred"| + "stats.properties.ticketVolume"| + "stats.properties.averageTickets"| + "stats.properties.currentTickets"| + "stats.properties.age"| + "stats.properties.responseTime"| + "stats.properties.resolutionTime"| + "stats.properties.createdOn"| + "stats.properties.createdBy"| + "stats.properties.closedOn"| + "stats.properties.closedBy"| + "stats.properties.claimedOn"| + "stats.properties.claimedBy"| + "stats.properties.pinnedOn"| + "stats.properties.pinnedBy"| + "stats.properties.deletedOn"| + "stats.properties.deletedBy"| + + "stats.roles.developer"| + "stats.roles.serverOwner"| + "stats.roles.serverAdmin"| + "stats.roles.moderator"| + "stats.roles.support"| + "stats.roles.member"| + + "panel.selectTicket"| + "panel.selectRole"| + "panel.selectOption"| + + "priorities.urgent"| + "priorities.veryHigh"| + "priorities.high"| + "priorities.normal"| + "priorities.low"| + "priorities.veryLow"| + "priorities.none" ) /**## ODLanguageManager_Default `default_class` diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 979f663..38e1145 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -78,18 +78,18 @@ export const registerDefaultCheckerMessageTranslations = (tm:api.ODCheckerTransl tm.quickTranslate(lm,"checker.messages.stringStartsWith","message","opendiscord:string-starts-with") // This string needs to start with {0}! tm.quickTranslate(lm,"checker.messages.stringEndsWith","message","opendiscord:string-ends-with") // This string needs to end with {0}! tm.quickTranslate(lm,"checker.messages.stringContains","message","opendiscord:string-contains") // This string needs to contain {0}! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-inverted-contains") // This string is not allowed to contain {0}! + tm.quickTranslate(lm,"checker.messages.stringInvertedContains","message","opendiscord:string-inverted-contains") // This string is not allowed to contain {0}! tm.quickTranslate(lm,"checker.messages.stringChoices","message","opendiscord:string-choices") // This string can only be one of the following values: {0}! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-lowercase") // This string must be written in lowercase only! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-uppercase") // This string must be written in uppercase only! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-special-characters") // This string is not allowed to contain any special characters! (a-z, 0-9 & space only) - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-no-spaces") // This string is not allowed to contain spaces! + tm.quickTranslate(lm,"checker.messages.stringLowercase","message","opendiscord:string-lowercase") // This string must be written in lowercase only! + tm.quickTranslate(lm,"checker.messages.stringUppercase","message","opendiscord:string-uppercase") // This string must be written in uppercase only! + tm.quickTranslate(lm,"checker.messages.stringSpecialCharacters","message","opendiscord:string-special-characters") // This string is not allowed to contain any special characters! (a-z, 0-9 & space only) + tm.quickTranslate(lm,"checker.messages.stringNoSpaces","message","opendiscord:string-no-spaces") // This string is not allowed to contain spaces! tm.quickTranslate(lm,"checker.messages.stringRegex","message","opendiscord:string-regex") // This string is invalid! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-capital-word") // It's recommended that each word in this string starts with a capital letter! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-capital-sentence") // It looks like some sentences in this string don't start with a capital letter! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:string-punctuation") // It looks like the sentence in this string doesn't end with a punctuation mark! + tm.quickTranslate(lm,"checker.messages.stringCapitalWord","message","opendiscord:string-capital-word") // It's recommended that each word in this string starts with a capital letter! + tm.quickTranslate(lm,"checker.messages.stringCapitalSentence","message","opendiscord:string-capital-sentence") // It looks like some sentences in this string don't start with a capital letter! + tm.quickTranslate(lm,"checker.messages.stringPunctuation","message","opendiscord:string-punctuation") // It looks like the sentence in this string doesn't end with a punctuation mark! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:number-nan") // This number can't be NaN (Not A Number)! + tm.quickTranslate(lm,"checker.messages.numberNan","message","opendiscord:number-nan") // This number can't be NaN (Not A Number)! tm.quickTranslate(lm,"checker.messages.numberTooShort","message","opendiscord:number-too-short") // This number can't be shorter than {0} characters! tm.quickTranslate(lm,"checker.messages.numberTooLong","message","opendiscord:number-too-long") // This number can't be longer than {0} characters! tm.quickTranslate(lm,"checker.messages.numberLengthInvalid","message","opendiscord:number-length-invalid") // This number needs to be {0} characters long! @@ -101,7 +101,7 @@ export const registerDefaultCheckerMessageTranslations = (tm:api.ODCheckerTransl tm.quickTranslate(lm,"checker.messages.numberStartsWith","message","opendiscord:number-starts-with") // This number needs to start with {0}! tm.quickTranslate(lm,"checker.messages.numberEndsWith","message","opendiscord:number-ends-with") // This number needs to end with {0}! tm.quickTranslate(lm,"checker.messages.numberContains","message","opendiscord:number-contains") // This number needs to contain {0}! - tm.quickTranslate(lm,"//TODO TRANSLATION!!!","message","opendiscord:number-inverted-contains") // This number is not allowed to contain {0}! + tm.quickTranslate(lm,"checker.messages.numberInvertedContains","message","opendiscord:number-inverted-contains") // This number is not allowed to contain {0}! tm.quickTranslate(lm,"checker.messages.numberChoices","message","opendiscord:number-choices") // This number can only be one of the following values: {0}! tm.quickTranslate(lm,"checker.messages.numberFloat","message","opendiscord:number-float") // This number can't be a decimal! tm.quickTranslate(lm,"checker.messages.numberNegative","message","opendiscord:number-negative") // This number can't be negative! @@ -144,9 +144,7 @@ export const registerDefaultCheckerCustomTranslations = (tm:api.ODCheckerTransla tm.quickTranslate(lm,"checker.messages.unusedOption","message","opendiscord:unused-option") // The option {0} isn't used anywhere! tm.quickTranslate(lm,"checker.messages.unusedQuestion","message","opendiscord:unused-question") // The question {0} isn't used anywhere! tm.quickTranslate(lm,"checker.messages.dropdownOption","message","opendiscord:dropdown-option") // A panel with dropdown enabled can only contain options of the 'ticket' type! - - //TODO TRANSLATION!!! - //tm.quickTranslate(lm,"checker.messages.TODO","message","opendiscord:invalid-version") // The version specified in your config does not match! Make sure you have updated the config to the latest version! + tm.quickTranslate(lm,"checker.messages.customInvalidVersion","message","opendiscord:invalid-version") // The version specified in your config does not match! Make sure you have updated the config to the latest version! } //UTILITY FUNCTIONS diff --git a/src/data/framework/commandLoader.ts b/src/data/framework/commandLoader.ts index bae7f65..b598e79 100644 --- a/src/data/framework/commandLoader.ts +++ b/src/data/framework/commandLoader.ts @@ -560,18 +560,18 @@ export const loadAllSlashCommands = async () => { if (allowedCommands.includes("topic")) commands.add(new api.ODSlashCommand("opendiscord:topic",{ type:act.ChatInput, name:"topic", - description:"Manage the topic of the ticket channel.", //TODO TRANSLATION!!! + description:lang.getTranslation("commands.topic"), contexts:[discord.InteractionContextType.Guild], integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], options:[ { name:"set", - description:"Set the topic of the ticket channel to a specific value.", //TODO TRANSLATION!!! + description:lang.getTranslation("commands.topicSet"), type:acot.Subcommand, options:[ { name:"topic", - description:"The new topic of the channel.", + description:lang.getTranslation("commands.topicValue"), type:acot.String, required:true } @@ -585,18 +585,18 @@ export const loadAllSlashCommands = async () => { if (allowedCommands.includes("priority")) commands.add(new api.ODSlashCommand("opendiscord:priority",{ type:act.ChatInput, name:"priority", - description:"Manage the priority of the ticket.", //TODO TRANSLATION!!! + description:lang.getTranslation("commands.priority"), contexts:[discord.InteractionContextType.Guild], integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], options:[ { name:"set", - description:"Set the priority of the ticket.", //TODO TRANSLATION!!! + description:lang.getTranslation("commands.prioritySet"), type:acot.Subcommand, options:[ { name:"priority", - description:"The priority of the channel.", + description:lang.getTranslation("commands.priorityValue"), type:acot.String, required:true, choices:opendiscord.priorities.getAll().sort((a,b) => b.priority-a.priority).map((prio) => ({value:prio.rawName,name:prio.renderDisplayName()})) @@ -611,7 +611,7 @@ export const loadAllSlashCommands = async () => { }, { name:"get", - description:"Get the priority of the ticket.", //TODO TRANSLATION!!! + description:lang.getTranslation("commands.priorityGet"), type:acot.Subcommand }, //TODO: list (v4.2) @@ -622,13 +622,13 @@ export const loadAllSlashCommands = async () => { if (allowedCommands.includes("transfer")) commands.add(new api.ODSlashCommand("opendiscord:transfer",{ type:act.ChatInput, name:"transfer", - description:"Transfer the ticket ownership from one user to another.", //TODO TRANSLATION!!! + description:lang.getTranslation("commands.transfer"), contexts:[discord.InteractionContextType.Guild], integrationTypes:[discord.ApplicationIntegrationType.GuildInstall], options:[ { name:"user", - description:"The user to transfer to.", //TODO TRANSLATION!!! + description:lang.getTranslation("commands.transferUser"), type:acot.User, required:true }, diff --git a/src/data/framework/helpMenuLoader.ts b/src/data/framework/helpMenuLoader.ts index 65c1668..92c96bb 100644 --- a/src/data/framework/helpMenuLoader.ts +++ b/src/data/framework/helpMenuLoader.ts @@ -15,13 +15,13 @@ const lang = opendiscord.languages export const loadAllHelpMenuCategories = async () => { const helpmenu = opendiscord.helpmenu - helpmenu.add(new api.ODHelpMenuCategory("opendiscord:general",5,utilities.emojiTitle("📎","General Commands"))) //TODO TRANSLATION!!! - helpmenu.add(new api.ODHelpMenuCategory("opendiscord:ticket-basic",4,utilities.emojiTitle("🎫","Basic Ticket Commands"))) //TODO TRANSLATION!!! - helpmenu.add(new api.ODHelpMenuCategory("opendiscord:ticket-advanced",4,utilities.emojiTitle("💡","Advanced Ticket Commands"))) //TODO TRANSLATION!!! - helpmenu.add(new api.ODHelpMenuCategory("opendiscord:ticket-user",3,utilities.emojiTitle("👤","User Ticket Commands"))) //TODO TRANSLATION!!! - helpmenu.add(new api.ODHelpMenuCategory("opendiscord:admin",2,utilities.emojiTitle("🚨","Admin Commands"))) //TODO TRANSLATION!!! - helpmenu.add(new api.ODHelpMenuCategory("opendiscord:advanced",1,utilities.emojiTitle("🚧","Advanced Commands"))) //TODO TRANSLATION!!! - helpmenu.add(new api.ODHelpMenuCategory("opendiscord:extra",0,utilities.emojiTitle("✨","Extra Commands"))) //TODO TRANSLATION!!! + helpmenu.add(new api.ODHelpMenuCategory("opendiscord:general",5,utilities.emojiTitle("📎",lang.getTranslation("helpMenu.categories.general")))) + helpmenu.add(new api.ODHelpMenuCategory("opendiscord:ticket-basic",4,utilities.emojiTitle("🎫",lang.getTranslation("helpMenu.categories.basicTicket")))) + helpmenu.add(new api.ODHelpMenuCategory("opendiscord:ticket-advanced",4,utilities.emojiTitle("💡",lang.getTranslation("helpMenu.categories.advancedTicket")))) + helpmenu.add(new api.ODHelpMenuCategory("opendiscord:ticket-user",3,utilities.emojiTitle("👤",lang.getTranslation("helpMenu.categories.userTicket")))) + helpmenu.add(new api.ODHelpMenuCategory("opendiscord:admin",2,utilities.emojiTitle("🚨",lang.getTranslation("helpMenu.categories.admin")))) + helpmenu.add(new api.ODHelpMenuCategory("opendiscord:advanced",1,utilities.emojiTitle("🚧",lang.getTranslation("helpMenu.categories.advanced")))) + helpmenu.add(new api.ODHelpMenuCategory("opendiscord:extra",0,utilities.emojiTitle("✨",lang.getTranslation("helpMenu.categories.extra")))) } export const loadAllHelpMenuComponents = async () => { @@ -163,9 +163,9 @@ export const loadAllHelpMenuComponents = async () => { })) if (allowedCommands.includes("transfer")) ticketUser.add(new api.ODHelpMenuCommandComponent("opendiscord:transfer",-1,{ textName:prefix+"transfer", - textDescription:"Transfer the ticket ownership from one user to another.", //TODO TRANSLATION!!! + textDescription:lang.getTranslation("commands.transfer"), slashName:"/transfer", - slashDescription:"Transfer the ticket ownership from one user to another.", //TODO TRANSLATION!!! + slashDescription:lang.getTranslation("commands.transfer"), textOptions:[{name:"user",optional:false},{name:"reason",optional:true}], slashOptions:[{name:"user",optional:false},{name:"reason",optional:true}] })) @@ -279,17 +279,17 @@ export const loadAllHelpMenuComponents = async () => { })) if (allowedCommands.includes("topic")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:topic-set",1,{ textName:prefix+"topic set", - textDescription:"Set the topic of the ticket channel.", //TODO TRANSLATION!!! + textDescription:lang.getTranslation("commands.topicSet"), slashName:"/topic set", - slashDescription:"Manage the topic of the ticket channel.", //TODO TRANSLATION!!! + slashDescription:lang.getTranslation("commands.topicSet"), textOptions:[{name:"topic",optional:false}], slashOptions:[{name:"topic",optional:false}] })) if (allowedCommands.includes("priority")) advanced.add(new api.ODHelpMenuCommandComponent("opendiscord:priority-set",0,{ textName:prefix+"priority set", - textDescription:"Set the priority of the ticket.", //TODO TRANSLATION!!! + textDescription:lang.getTranslation("commands.prioritySet"), slashName:"/priority set", - slashDescription:"Manage the priority of the ticket.", //TODO TRANSLATION!!! + slashDescription:lang.getTranslation("commands.prioritySet"), textOptions:[{name:"priority",optional:false},{name:"reason",optional:true}], slashOptions:[{name:"priority",optional:false},{name:"reason",optional:true}] })) diff --git a/src/data/framework/statLoader.ts b/src/data/framework/statLoader.ts index a9b6b03..9392c5f 100644 --- a/src/data/framework/statLoader.ts +++ b/src/data/framework/statLoader.ts @@ -10,7 +10,7 @@ export const loadAllStatScopes = async () => { stats.add(new api.ODStatScope("opendiscord:user",utilities.emojiTitle("📊",lang.getTranslation("stats.scopes.user")))) stats.add(new api.ODStatScope("opendiscord:ticket",utilities.emojiTitle("📊",lang.getTranslation("stats.scopes.ticket")))) stats.add(new api.ODStatScope("opendiscord:participants",utilities.emojiTitle("👥",lang.getTranslation("stats.scopes.participants")))) - stats.add(new api.ODStatScope("opendiscord:messages",utilities.emojiTitle("💬","Messages"))) //TODO TRANSLATION!!! + stats.add(new api.ODStatScope("opendiscord:messages",utilities.emojiTitle("💬",lang.getTranslation("stats.scopes.messages")))) } export const loadAllStats = async () => { @@ -24,21 +24,21 @@ export const loadAllStats = async () => { global.add(new api.ODBasicStat("opendiscord:tickets-deleted",11,lang.getTranslation("stats.properties.ticketsDeleted"),0)) global.add(new api.ODBasicStat("opendiscord:tickets-reopened",10,lang.getTranslation("stats.properties.ticketsReopened"),0)) global.add(new api.ODBasicStat("opendiscord:tickets-autoclosed",9,lang.getTranslation("stats.properties.ticketsAutoclosed"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-autodeleted",8,"Tickets Autodeleted",0)) //TODO TRANSLATION!!! + global.add(new api.ODBasicStat("opendiscord:tickets-autodeleted",8,lang.getTranslation("stats.properties.ticketsAutodeleted"),0)) global.add(new api.ODBasicStat("opendiscord:tickets-claimed",7,lang.getTranslation("stats.properties.ticketsClaimed"),0)) global.add(new api.ODBasicStat("opendiscord:tickets-pinned",6,lang.getTranslation("stats.properties.ticketsPinned"),0)) global.add(new api.ODBasicStat("opendiscord:tickets-moved",5,lang.getTranslation("stats.properties.ticketsMoved"),0)) - global.add(new api.ODBasicStat("opendiscord:tickets-transferred",4,"Tickets Transferred",0)) //TODO TRANSLATION!!! + global.add(new api.ODBasicStat("opendiscord:tickets-transferred",4,lang.getTranslation("stats.properties.ticketsTransferred"),0)) global.add(new api.ODBasicStat("opendiscord:users-blacklisted",3,lang.getTranslation("stats.properties.usersBlacklisted"),0)) global.add(new api.ODBasicStat("opendiscord:transcripts-created",2,lang.getTranslation("stats.properties.transcriptsCreated"),0)) global.add(new api.ODDynamicStat("opendiscord:ticket-volume",1,() => { - return "Ticket Volume: `"+opendiscord.tickets.getLength()+"`" //TODO TRANSLATION!!! + return lang.getTranslation("stats.properties.ticketVolume")+": `"+opendiscord.tickets.getLength()+"`" })) global.add(new api.ODDynamicStat("opendiscord:average-tickets",0,async () => { const userTicketsCreated = await opendiscord.stats.get("opendiscord:user").getAllStats("opendiscord:tickets-created") const average = userTicketsCreated.map((s) => s.value as number).filter((t) => t > 0).reduce((prev,curr) => prev+curr,0)/userTicketsCreated.length const roundedAverage = Math.round(average*1000)/1000 - return "Average Tickets/User: `"+roundedAverage+"`" //TODO TRANSLATION!!! + return lang.getTranslation("stats.properties.averageTickets")+": `"+roundedAverage+"`" })) } @@ -48,7 +48,7 @@ export const loadAllStats = async () => { return lang.getTranslation("params.uppercase.startupDate")+": "+discord.time(opendiscord.processStartupDate,"f") })) system.add(new api.ODDynamicStat("opendiscord:system-uptime",1,() => { - return "System Uptime: "+discord.time(opendiscord.processStartupDate,"R") //TODO TRANSLATION!!! + return lang.getTranslation("params.uppercase.uptime")+": "+discord.time(opendiscord.processStartupDate,"R") })) system.add(new api.ODDynamicStat("opendiscord:version",0,() => { return lang.getTranslation("params.uppercase.version")+": `"+opendiscord.versions.get("opendiscord:version").toString()+"`" @@ -65,12 +65,12 @@ export const loadAllStats = async () => { if (!scopeMember) return "" const permissions = await opendiscord.permissions.getPermissions(scopeMember.user,channel,guild) - if (permissions.type == "developer") return lang.getTranslation("params.uppercase.role")+": 💻 `Developer`" //TODO TRANSLATION!!! - if (permissions.type == "owner") return lang.getTranslation("params.uppercase.role")+": 👑 `Server Owner`" //TODO TRANSLATION!!! - if (permissions.type == "admin") return lang.getTranslation("params.uppercase.role")+": 💼 `Server Admin`" //TODO TRANSLATION!!! - if (permissions.type == "moderator") return lang.getTranslation("params.uppercase.role")+": 🚔 `Moderator Team`" //TODO TRANSLATION!!! - if (permissions.type == "support") return lang.getTranslation("params.uppercase.role")+": 💬 `Support Team`" //TODO TRANSLATION!!! - else return lang.getTranslation("params.uppercase.role")+": 👤 `Member`" //TODO TRANSLATION!!! + if (permissions.type == "developer") return lang.getTranslation("params.uppercase.role")+": 💻 `"+lang.getTranslation("stats.roles.developer")+"`" + if (permissions.type == "owner") return lang.getTranslation("params.uppercase.role")+": 👑 `"+lang.getTranslation("stats.roles.serverOwner")+"`" + if (permissions.type == "admin") return lang.getTranslation("params.uppercase.role")+": 💼 `"+lang.getTranslation("stats.roles.serverAdmin")+"`" + if (permissions.type == "moderator") return lang.getTranslation("params.uppercase.role")+": 🚔 `"+lang.getTranslation("stats.roles.moderator")+"`" + if (permissions.type == "support") return lang.getTranslation("params.uppercase.role")+": 💬 `"+lang.getTranslation("stats.roles.support")+"`" + else return lang.getTranslation("params.uppercase.role")+": 👤 `"+lang.getTranslation("stats.roles.member")+"`" })) user.add(new api.ODBasicStat("opendiscord:tickets-created",10,lang.getTranslation("stats.properties.ticketsCreated"),0)) user.add(new api.ODBasicStat("opendiscord:tickets-closed",9,lang.getTranslation("stats.properties.ticketsClosed"),0)) @@ -79,11 +79,11 @@ export const loadAllStats = async () => { user.add(new api.ODBasicStat("opendiscord:tickets-claimed",6,lang.getTranslation("stats.properties.ticketsClaimed"),0)) user.add(new api.ODBasicStat("opendiscord:tickets-pinned",5,lang.getTranslation("stats.properties.ticketsPinned"),0)) user.add(new api.ODBasicStat("opendiscord:tickets-moved",4,lang.getTranslation("stats.properties.ticketsMoved"),0)) - user.add(new api.ODBasicStat("opendiscord:tickets-transferred",3,"Tickets Transferred",0)) //TODO TRANSLATION!!! + user.add(new api.ODBasicStat("opendiscord:tickets-transferred",3,lang.getTranslation("stats.properties.ticketsTransferred"),0)) user.add(new api.ODBasicStat("opendiscord:users-blacklisted",2,lang.getTranslation("stats.properties.usersBlacklisted"),0)) user.add(new api.ODBasicStat("opendiscord:transcripts-created",1,lang.getTranslation("stats.properties.transcriptsCreated"),0)) user.add(new api.ODDynamicStat("opendiscord:current-tickets",0,async (scopeId,guild,channel,user) => { - return "Current Tickets: `"+opendiscord.tickets.getFiltered((t) => t.get("opendiscord:opened-by").value === scopeId).length+"`" //TODO TRANSLATION!!! + return lang.getTranslation("stats.properties.currentTickets")+": `"+opendiscord.tickets.getFiltered((t) => t.get("opendiscord:opened-by").value === scopeId).length+"`" })) } @@ -98,21 +98,21 @@ export const loadAllStats = async () => { const closed = ticket.exists("opendiscord:closed") ? ticket.get("opendiscord:closed").value : false - return closed ? lang.getTranslation("params.uppercase.status")+": 🔒 `Closed`" : lang.getTranslation("params.uppercase.status")+": 🔓 `Open`" //TODO TRANSLATION!!! + return closed ? lang.getTranslation("params.uppercase.status")+": 🔒 `"+lang.getTranslation("params.uppercase.closed")+"`" : lang.getTranslation("params.uppercase.status")+": 🔓 `"+lang.getTranslation("params.uppercase.open")+"`" })) ticket.add(new api.ODDynamicStat("opendiscord:claimed",3,async (scopeId,guild,channel,user) => { const ticket = opendiscord.tickets.get(scopeId) if (!ticket) return "" const claimed = ticket.exists("opendiscord:claimed") ? ticket.get("opendiscord:claimed").value : false - return claimed ? lang.getTranslation("params.uppercase.claimed")+": 🟢 `Yes`" : lang.getTranslation("params.uppercase.claimed")+": 🔴 `No`" //TODO TRANSLATION!!! + return claimed ? lang.getTranslation("params.uppercase.claimed")+": 🟢 `"+lang.getTranslation("params.uppercase.yes")+"`" : lang.getTranslation("params.uppercase.claimed")+": 🔴 `"+lang.getTranslation("params.uppercase.no")+"`" })) ticket.add(new api.ODDynamicStat("opendiscord:pinned",2,async (scopeId,guild,channel,user) => { const ticket = opendiscord.tickets.get(scopeId) if (!ticket) return "" const pinned = ticket.exists("opendiscord:pinned") ? ticket.get("opendiscord:pinned").value : false - return pinned ? lang.getTranslation("params.uppercase.pinned")+": 🟢 `Yes`" : lang.getTranslation("params.uppercase.pinned")+": 🔴 `No`" //TODO TRANSLATION!!! + return pinned ? lang.getTranslation("params.uppercase.pinned")+": 🟢 `"+lang.getTranslation("params.uppercase.yes")+"`" : lang.getTranslation("params.uppercase.pinned")+": 🔴 `"+lang.getTranslation("params.uppercase.no")+"`" })) ticket.add(new api.ODDynamicStat("opendiscord:creation-date",1,async (scopeId,guild,channel,user) => { const ticket = opendiscord.tickets.get(scopeId) @@ -133,10 +133,11 @@ export const loadAllStats = async () => { if (!ticket) return "" const rawDate = ticket.get("opendiscord:opened-on").value ?? new Date().getTime() - return "Ticket Age: "+discord.time(new Date(rawDate),"R") //TODO TRANSLATION!!! + return lang.getTranslation("stats.properties.age")+": "+discord.time(new Date(rawDate),"R") })) - //TODO: opendiscord:response-time //TODO TRANSLATION!!! - //TODO: opendiscord:resolution-time //TODO TRANSLATION!!! + + //TODO: opendiscord:response-time --> lang.getTranslation("stats.properties.responseTime") + //TODO: opendiscord:resolution-time --> lang.getTranslation("stats.properties.resolutionTime") } const participants = stats.get("opendiscord:participants") @@ -178,10 +179,10 @@ export const loadAllStats = async () => { } return [ - "Messages: `"+messageCount+"`", //TODO TRANSLATION!!! - "Embeds: `"+embedCount+"`", //TODO TRANSLATION!!! - "Files: `"+fileCount+"`", //TODO TRANSLATION!!! - "Components: `"+componentCount+"`" //TODO TRANSLATION!!! + lang.getTranslation("params.uppercase.messages")+": `"+messageCount+"`", + lang.getTranslation("params.uppercase.embeds")+": `"+embedCount+"`", + lang.getTranslation("params.uppercase.files")+": `"+fileCount+"`", + lang.getTranslation("params.uppercase.components")+": `"+componentCount+"`" ].join("\n") })) diff --git a/src/data/openticket/panelLoader.ts b/src/data/openticket/panelLoader.ts index e522669..7d902cf 100644 --- a/src/data/openticket/panelLoader.ts +++ b/src/data/openticket/panelLoader.ts @@ -1,6 +1,8 @@ import {opendiscord, api, utilities} from "../../index" import * as discord from "discord.js" +const lang = opendiscord.languages + export const loadAllPanels = async () => { const panelConfig = opendiscord.configs.get("opendiscord:panels") if (!panelConfig) return @@ -75,8 +77,7 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { } }) - //TODO TRANSLATION!!! - const autotitle = (hasTicket && ticketOnly) ? "Select your ticket:" : ((hasRole && roleOnly) ? "Select your role:" : "Select your option:") + const autotitle = (hasTicket && ticketOnly) ? lang.getTranslation("panel.selectTicket")+":" : ((hasRole && roleOnly) ? lang.getTranslation("panel.selectRole")+":" : lang.getTranslation("panel.selectOption")+":") const title = (panel.get("opendiscord:describe-options-custom-title").value.length < 1) ? "__"+autotitle+"__\n" : "__"+panel.get("opendiscord:describe-options-custom-title").value+"__\n" if (mode == "fields") return options.map((opt) => { @@ -87,10 +88,8 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { let description = opt.exists("opendiscord:description") ? opt.get("opendiscord:description").value : "``" if (layout == "normal" || layout == "detailed"){ - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:cooldown-enabled") && opt.get("opendiscord:cooldown-enabled").value) description = description + "\nCooldown: `"+opt.get("opendiscord:cooldown-minutes").value+" min`" - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:limits-enabled") && opt.get("opendiscord:limits-enabled").value) description = description + "\nMax Tickets: `"+opt.get("opendiscord:limits-maximum-user").value+"`" + if (opt.exists("opendiscord:cooldown-enabled") && opt.get("opendiscord:cooldown-enabled").value) description = description + "\n"+lang.getTranslation("params.uppercase.cooldown")+": `"+opt.get("opendiscord:cooldown-minutes").value+" min`" + if (opt.exists("opendiscord:limits-enabled") && opt.get("opendiscord:limits-enabled").value) description = description + "\n"+lang.getTranslation("params.uppercase.maxTickets")+": `"+opt.get("opendiscord:limits-maximum-user").value+"`" } if (layout == "detailed"){ const optionAdmins = [...opt.get("opendiscord:admins").value] @@ -99,9 +98,7 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { if (!optionAdmins.includes(admin)) optionAdmins.push(admin) } } - - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:admins")) description = description + "\nAdmins: "+optionAdmins.map((admin) => discord.roleMention(admin)).join(", ") + if (opt.exists("opendiscord:admins")) description = description + "\n"+lang.getTranslation("params.uppercase.admins")+": "+optionAdmins.map((admin) => discord.roleMention(admin)).join(", ") } if (description == "") description = "``" @@ -123,8 +120,7 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { let description = opt.exists("opendiscord:description") ? opt.get("opendiscord:description").value : "``" if (layout == "normal" || layout == "detailed"){ - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:roles")) description = description + "\nRoles: "+opt.get("opendiscord:roles").value.map((admin) => discord.roleMention(admin)).join(", ") + if (opt.exists("opendiscord:roles")) description = description + "\n"+lang.getTranslation("params.uppercase.roles")+": "+opt.get("opendiscord:roles").value.map((admin) => discord.roleMention(admin)).join(", ") } if (description == "") description = "``" @@ -146,10 +142,8 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { let description = opt.exists("opendiscord:description") ? opt.get("opendiscord:description").value : "``" if (layout == "normal" || layout == "detailed"){ - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:cooldown-enabled") && opt.get("opendiscord:cooldown-enabled").value) description = description + "\nCooldown: `"+opt.get("opendiscord:cooldown-minutes").value+" min`" - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:limits-enabled") && opt.get("opendiscord:limits-enabled").value) description = description + "\nMax Tickets: `"+opt.get("opendiscord:limits-maximum-user").value+"`" + if (opt.exists("opendiscord:cooldown-enabled") && opt.get("opendiscord:cooldown-enabled").value) description = description + "\n"+lang.getTranslation("params.uppercase.cooldown")+": `"+opt.get("opendiscord:cooldown-minutes").value+" min`" + if (opt.exists("opendiscord:limits-enabled") && opt.get("opendiscord:limits-enabled").value) description = description + "\n"+lang.getTranslation("params.uppercase.maxTickets")+": `"+opt.get("opendiscord:limits-maximum-user").value+"`" } if (layout == "detailed"){ const optionAdmins = [...opt.get("opendiscord:admins").value] @@ -158,9 +152,7 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { if (!optionAdmins.includes(admin)) optionAdmins.push(admin) } } - - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:admins")) description = description + "\nAdmins: "+optionAdmins.map((admin) => discord.roleMention(admin)).join(", ") + if (opt.exists("opendiscord:admins")) description = description + "\n"+lang.getTranslation("params.uppercase.admins")+": "+optionAdmins.map((admin) => discord.roleMention(admin)).join(", ") } if (layout == "simple") return "**"+utilities.emojiTitle(emoji,name)+":** "+description @@ -182,8 +174,7 @@ export function describePanelOptions(mode:"fields"|"text", panel:api.ODPanel): { let description = opt.exists("opendiscord:description") ? opt.get("opendiscord:description").value : "``" if (layout == "normal" || layout == "detailed"){ - //TODO TRANSLATION!!! - if (opt.exists("opendiscord:roles")) description = description + "\nRoles: "+opt.get("opendiscord:roles").value.map((admin) => discord.roleMention(admin)).join(", ") + if (opt.exists("opendiscord:roles")) description = description + "\n"+lang.getTranslation("params.uppercase.roles")+": "+opt.get("opendiscord:roles").value.map((admin) => discord.roleMention(admin)).join(", ") } if (layout == "simple") return "**"+utilities.emojiTitle(emoji,name)+":** "+description diff --git a/src/data/openticket/priorityLoader.ts b/src/data/openticket/priorityLoader.ts index d0e85e1..c568fca 100644 --- a/src/data/openticket/priorityLoader.ts +++ b/src/data/openticket/priorityLoader.ts @@ -1,11 +1,13 @@ import {opendiscord, api, utilities} from "../../index" +const lang = opendiscord.languages + export const loadAllPriorityLevels = async () => { - opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:urgent",5,"urgent","Urgent","🔴","🔴")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:very-high",4,"very-high","Very High","🟠","🟠")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:high",3,"high","High","🟡","🟡")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:normal",2,"normal","Normal","🟢","🟢")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:low",1,"low","Low","🔵","🔵")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:very-low",0,"very-low","Very Low","⚪","⚪")) //TODO TRANSLATION!!! - opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:none",-1,"none","None",null,null)) //TODO TRANSLATION!!! + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:urgent",5,"urgent",lang.getTranslation("priorities.urgent"),"🔴","🔴")) + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:very-high",4,"very-high",lang.getTranslation("priorities.veryHigh"),"🟠","🟠")) + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:high",3,"high",lang.getTranslation("priorities.high"),"🟡","🟡")) + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:normal",2,"normal",lang.getTranslation("priorities.normal"),"🟢","🟢")) + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:low",1,"low",lang.getTranslation("priorities.low"),"🔵","🔵")) + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:very-low",0,"very-low",lang.getTranslation("priorities.veryLow"),"⚪","⚪")) + opendiscord.priorities.add(new api.ODPriorityLevel("opendiscord:none",-1,"none",lang.getTranslation("priorities.none"),null,null)) } \ No newline at end of file diff --git a/src/data/openticket/transcriptLoader.ts b/src/data/openticket/transcriptLoader.ts index 3d7148f..6d6afd2 100644 --- a/src/data/openticket/transcriptLoader.ts +++ b/src/data/openticket/transcriptLoader.ts @@ -7,6 +7,7 @@ const transcriptConfig = opendiscord.configs.get("opendiscord:transcripts") const textConfig = transcriptConfig.data.textTranscriptStyle const htmlVersion = Buffer.from("eW91LXNob3VsZG50LWJlLWxvb2tpbmctYXQtdGhpcy0tLWZvci1tb3JlLWluZm8tY29tbWEtc2VuZC1hLW1lc3NhZ2UtdG8tZGpqMTIzZGo=","base64").toString("utf8") const htmlDomain = atob("dC5kai1kai5iZQ==") +const lang = opendiscord.languages export const replaceHtmlTranscriptMentions = async (text:string) => { const mainServer = opendiscord.client.mainServer @@ -65,8 +66,7 @@ export const loadAllTranscriptCompilers = async () => { const messages = await collector.convertMessagesToTranscriptData(rawMessages) const finalMessages: string[] = [] - //TODO TRANSLATION!!! - finalMessages.push("=============== MESSAGES ===============") + finalMessages.push("=============== "+lang.getTranslation("transcripts.text.messagesTitle")+" ===============") messages.filter((msg) => textConfig.includeBotMessages || !msg.author.tag).forEach((msg) => { const timestamp = utilities.dateString(new Date(msg.timestamp)) @@ -77,23 +77,19 @@ export const loadAllTranscriptCompilers = async () => { if (textConfig.layout == "simple"){ //SIMPLE LAYOUT const header = "["+timestamp+" | "+msg.author.displayname+authorId+"]"+edited+msgId - //TODO TRANSLATION!!! - const embeds = (textConfig.includeEmbeds) ? "\nEmbeds: "+msg.embeds.length : "" - const files = (textConfig.includeFiles) ? "\nFiles: "+msg.files.length : "" - //TODO TRANSLATION!!! - const content = (msg.content) ? msg.content : (""+embeds+files) + const embeds = (textConfig.includeEmbeds) ? "\n"+lang.getTranslation("params.uppercase.embeds")+": "+msg.embeds.length : "" + const files = (textConfig.includeFiles) ? "\n"+lang.getTranslation("params.uppercase.files")+": "+msg.files.length : "" + const content = (msg.content) ? msg.content : (lang.getTranslation("transcripts.text.emptyContent")+embeds+files) finalMessages.push(header+"\n "+content.split("\n").join("\n ")) }else if (textConfig.layout == "normal"){ //NORMAL LAYOUT const header = "["+timestamp+" | "+msg.author.displayname+authorId+"]"+edited+msgId const embeds = (textConfig.includeEmbeds && msg.embeds.length > 0) ? "\n"+msg.embeds.map((embed) => { - //TODO TRANSLATION!!! - return "==== (EMBED) "+(embed.title ?? "")+" ====\n"+(embed.description ?? "") + return "==== ("+lang.getTranslation("transcripts.text.embedTitle")+") "+(embed.title ?? lang.getTranslation("transcripts.text.noTitle"))+" ====\n"+(embed.description ?? lang.getTranslation("transcripts.text.noDesc")) }) : "" const files = (textConfig.includeFiles && msg.files.length > 0) ? "\n"+msg.files.map((file) => { - //TODO TRANSLATION!!! - return "==== (FILE) "+(file.name)+" ====\nSize: "+(file.size+" "+file.unit)+"\nUrl: "+file.url + return "==== ("+lang.getTranslation("transcripts.text.fileTitle")+") "+(file.name)+" ====\n"+lang.getTranslation("params.uppercase.size")+": "+(file.size+" "+file.unit)+"\nUrl: "+file.url }) : "" const content = (msg.content) ? msg.content : "" finalMessages.push(header+"\n "+(content+embeds+files).split("\n").join("\n ")) @@ -102,15 +98,12 @@ export const loadAllTranscriptCompilers = async () => { //ADVANCED LAYOUT const header = "["+timestamp+" | "+msg.author.displayname+authorId+"]"+edited+msgId const embeds = (textConfig.includeEmbeds && msg.embeds.length > 0) ? "\n"+msg.embeds.map((embed) => { - //TODO TRANSLATION!!! - return "\n==== (EMBED) "+(embed.title ?? "")+" ====\n"+(embed.description ?? "")+(embed.fields.length > 0 ? "\n\n== (FIELDS) ==\n"+embed.fields.map((field) => field.name+": "+field.value).join("\n") : "") + return "\n==== ("+lang.getTranslation("transcripts.text.embedTitle")+") "+(embed.title ?? lang.getTranslation("transcripts.text.noTitle"))+" ====\n"+(embed.description ?? lang.getTranslation("transcripts.text.noDesc"))+(embed.fields.length > 0 ? "\n\n== ("+lang.getTranslation("transcripts.text.fieldsTitle")+") ==\n"+embed.fields.map((field) => field.name+": "+field.value).join("\n") : "") }) : "" const files = (textConfig.includeFiles && msg.files.length > 0) ? "\n"+msg.files.map((file) => { - //TODO TRANSLATION!!! - return "\n==== (FILE) "+(file.name)+" ====\nSize: "+(file.size+" "+file.unit)+"\nUrl: "+file.url+"\nAlt: "+(file.alt ?? "/") + return "\n==== ("+lang.getTranslation("transcripts.text.fileTitle")+") "+(file.name)+" ====\n"+lang.getTranslation("params.uppercase.size")+": "+(file.size+" "+file.unit)+"\nUrl: "+file.url+"\nAlt: "+(file.alt ?? "/") }) : "" - //TODO TRANSLATION!!! - const reactions = (msg.reactions.filter((r) => !r.custom).length > 0) ? "\n==== (REACTIONS) ====\n"+msg.reactions.filter((r) => !r.custom).map((r) => r.amount+" "+r.emoji).join(" - ") : "" + const reactions = (msg.reactions.filter((r) => !r.custom).length > 0) ? "\n==== ("+lang.getTranslation("transcripts.text.reactionsTitle")+") ====\n"+msg.reactions.filter((r) => !r.custom).map((r) => r.amount+" "+r.emoji).join(" - ") : "" const content = (msg.content) ? msg.content : "" finalMessages.push(header+"\n "+(content+embeds+files+reactions).split("\n").join("\n ")) } @@ -128,44 +121,40 @@ export const loadAllTranscriptCompilers = async () => { const pinner = await opendiscord.tickets.getTicketUser(ticket,"pinner") if (textConfig.includeStats){ - //TODO TRANSLATION!!! - finalStats.push("=============== STATS ===============") + finalStats.push("=============== "+lang.getTranslation("transcripts.text.statsTitle")+" ===============") if (textConfig.layout == "simple"){ //SIMPLE LAYOUT - //TODO TRANSLATION!!! - if (creationDate) finalStats.push("Created On: "+utilities.dateString(new Date(creationDate))) - if (creator) finalStats.push("Created By: "+creator.displayName) + if (creationDate) finalStats.push(lang.getTranslation("stats.properties.createdOn")+": "+utilities.dateString(new Date(creationDate))) + if (creator) finalStats.push(lang.getTranslation("stats.properties.createdBy")+": "+creator.displayName) finalStats.push("\n") }else if (textConfig.layout == "normal"){ //NORMAL LAYOUT - //TODO TRANSLATION!!! - if (creationDate) finalStats.push("Created On: "+utilities.dateString(new Date(creationDate))) - if (creator) finalStats.push("Created By: "+creator.displayName) + if (creationDate) finalStats.push(lang.getTranslation("stats.properties.createdOn")+": "+utilities.dateString(new Date(creationDate))) + if (creator) finalStats.push(lang.getTranslation("stats.properties.createdBy")+": "+creator.displayName) if (closer || claimer || pinner) finalStats.push("") - if (closer) finalStats.push("Closed By: "+closer.displayName) - if (claimer) finalStats.push("Claimed By: "+claimer.displayName) - if (pinner) finalStats.push("Pinned By: "+pinner.displayName) - finalStats.push("Deleted By: "+user.displayName) + if (closer) finalStats.push(lang.getTranslation("stats.properties.closedBy")+": "+closer.displayName) + if (claimer) finalStats.push(lang.getTranslation("stats.properties.claimedBy")+": "+claimer.displayName) + if (pinner) finalStats.push(lang.getTranslation("stats.properties.pinnedBy")+": "+pinner.displayName) + finalStats.push(lang.getTranslation("stats.properties.deletedBy")+": "+user.displayName) finalStats.push("\n") }else if (textConfig.layout == "detailed"){ //ADVANCED LAYOUT - //TODO TRANSLATION!!! - if (creationDate) finalStats.push("Created On: "+utilities.dateString(new Date(creationDate))) - if (creator) finalStats.push("Created By: "+creator.displayName) + if (creationDate) finalStats.push(lang.getTranslation("stats.properties.createdOn")+": "+utilities.dateString(new Date(creationDate))) + if (creator) finalStats.push(lang.getTranslation("stats.properties.createdBy")+": "+creator.displayName) if (closer || closeDate) finalStats.push("") - if (closeDate) finalStats.push("Closed On: "+utilities.dateString(new Date(closeDate))) - if (closer) finalStats.push("Closed By: "+closer.displayName) + if (closeDate) finalStats.push(lang.getTranslation("stats.properties.closedOn")+": "+utilities.dateString(new Date(closeDate))) + if (closer) finalStats.push(lang.getTranslation("stats.properties.closedBy")+": "+closer.displayName) if (claimer || claimDate) finalStats.push("") - if (claimDate) finalStats.push("Claimed On: "+utilities.dateString(new Date(claimDate))) - if (claimer) finalStats.push("Claimed By: "+claimer.displayName) + if (claimDate) finalStats.push(lang.getTranslation("stats.properties.claimedOn")+": "+utilities.dateString(new Date(claimDate))) + if (claimer) finalStats.push(lang.getTranslation("stats.properties.claimedBy")+": "+claimer.displayName) if (pinner || pinDate) finalStats.push("") - if (pinDate) finalStats.push("Pinned On: "+utilities.dateString(new Date(pinDate))) - if (pinner) finalStats.push("Pinned By: "+pinner.displayName) + if (pinDate) finalStats.push(lang.getTranslation("stats.properties.pinnedOn")+": "+utilities.dateString(new Date(pinDate))) + if (pinner) finalStats.push(lang.getTranslation("stats.properties.pinnedBy")+": "+pinner.displayName) if (closer || claimer || pinner) finalStats.push("") - finalStats.push("Deleted On: "+utilities.dateString(new Date())) - finalStats.push("Deleted By: "+user.displayName) + finalStats.push(lang.getTranslation("stats.properties.deletedOn")+": "+utilities.dateString(new Date())) + finalStats.push(lang.getTranslation("stats.properties.deletedBy")+": "+user.displayName) finalStats.push("\n") } } From 68d6a2df3f025efd1771453366bb6bea18e7aa12 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:51:18 +0100 Subject: [PATCH 76/78] Added language merge utility tool --- .docs/mergeTranslations.js | 649 +++++++++++++++++++++++++++++++++++++ .gitignore | 1 + package.json | 3 +- 3 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 .docs/mergeTranslations.js diff --git a/.docs/mergeTranslations.js b/.docs/mergeTranslations.js new file mode 100644 index 0000000..7025dee --- /dev/null +++ b/.docs/mergeTranslations.js @@ -0,0 +1,649 @@ +//@ts-check +const fjs = require("formatted-json-stringify") +const fs = require("fs") +const formatter = new fjs.ObjectFormatter(null,true,[ + new fjs.ObjectFormatter("_TRANSLATION",true,[ + new fjs.PropertyFormatter("otversion"), + new fjs.ArrayFormatter("translators",false,new fjs.PropertyFormatter(null)), + new fjs.PropertyFormatter("lastedited"), + new fjs.PropertyFormatter("language"), + new fjs.PropertyFormatter("automated"), + ]), + new fjs.ObjectFormatter("checker",true,[ + new fjs.ObjectFormatter("system",true,[ + new fjs.PropertyFormatter("typeError"), + new fjs.PropertyFormatter("headerOpenTicket"), + new fjs.PropertyFormatter("typeWarning"), + new fjs.PropertyFormatter("typeInfo"), + new fjs.PropertyFormatter("headerConfigChecker"), + new fjs.PropertyFormatter("headerDescription"), + new fjs.PropertyFormatter("footerError"), + new fjs.PropertyFormatter("footerWarning"), + new fjs.PropertyFormatter("footerSupport"), + new fjs.PropertyFormatter("compactInformation"), + new fjs.PropertyFormatter("dataPath"), + new fjs.PropertyFormatter("dataDocs"), + new fjs.PropertyFormatter("dataMessages"), + ]), + new fjs.ObjectFormatter("messages",true,[ + new fjs.PropertyFormatter("stringTooShort"), + new fjs.PropertyFormatter("stringTooLong"), + new fjs.PropertyFormatter("stringLengthInvalid"), + new fjs.PropertyFormatter("stringStartsWith"), + new fjs.PropertyFormatter("stringEndsWith"), + new fjs.PropertyFormatter("stringContains"), + new fjs.PropertyFormatter("stringChoices"), + new fjs.PropertyFormatter("stringRegex"), + + new fjs.PropertyFormatter("stringInvertedContains"), + new fjs.PropertyFormatter("stringLowercase"), + new fjs.PropertyFormatter("stringUppercase"), + new fjs.PropertyFormatter("stringSpecialCharacters"), + new fjs.PropertyFormatter("stringNoSpaces"), + new fjs.PropertyFormatter("stringCapitalWord"), + new fjs.PropertyFormatter("stringCapitalSentence"), + new fjs.PropertyFormatter("stringPunctuation"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("numberTooShort"), + new fjs.PropertyFormatter("numberTooLong"), + new fjs.PropertyFormatter("numberLengthInvalid"), + new fjs.PropertyFormatter("numberTooSmall"), + new fjs.PropertyFormatter("numberTooLarge"), + new fjs.PropertyFormatter("numberNotEqual"), + new fjs.PropertyFormatter("numberStep"), + new fjs.PropertyFormatter("numberStepOffset"), + new fjs.PropertyFormatter("numberStartsWith"), + new fjs.PropertyFormatter("numberEndsWith"), + new fjs.PropertyFormatter("numberContains"), + new fjs.PropertyFormatter("numberChoices"), + new fjs.PropertyFormatter("numberFloat"), + new fjs.PropertyFormatter("numberNegative"), + new fjs.PropertyFormatter("numberPositive"), + new fjs.PropertyFormatter("numberZero"), + new fjs.PropertyFormatter("numberNan"), + new fjs.PropertyFormatter("numberInvertedContains"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("booleanTrue"), + new fjs.PropertyFormatter("booleanFalse"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("arrayEmptyDisabled"), + new fjs.PropertyFormatter("arrayEmptyRequired"), + new fjs.PropertyFormatter("arrayTooShort"), + new fjs.PropertyFormatter("arrayTooLong"), + new fjs.PropertyFormatter("arrayLengthInvalid"), + new fjs.PropertyFormatter("arrayInvalidTypes"), + new fjs.PropertyFormatter("arrayDouble"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("discordInvalidId"), + new fjs.PropertyFormatter("discordInvalidIdOptions"), + new fjs.PropertyFormatter("discordInvalidToken"), + new fjs.PropertyFormatter("colorInvalid"), + new fjs.PropertyFormatter("emojiTooShort"), + new fjs.PropertyFormatter("emojiTooLong"), + new fjs.PropertyFormatter("emojiCustom"), + new fjs.PropertyFormatter("emojiInvalid"), + new fjs.PropertyFormatter("urlInvalid"), + new fjs.PropertyFormatter("urlInvalidHttp"), + new fjs.PropertyFormatter("urlInvalidProtocol"), + new fjs.PropertyFormatter("urlInvalidHostname"), + new fjs.PropertyFormatter("urlInvalidExtension"), + new fjs.PropertyFormatter("urlInvalidPath"), + new fjs.PropertyFormatter("idNotUnique"), + new fjs.PropertyFormatter("idNonExistent"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("invalidType"), + new fjs.PropertyFormatter("propertyMissing"), + new fjs.PropertyFormatter("propertyOptional"), + new fjs.PropertyFormatter("objectDisabled"), + new fjs.PropertyFormatter("nullInvalid"), + new fjs.PropertyFormatter("switchInvalidType"), + new fjs.PropertyFormatter("objectSwitchInvalid"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("invalidLanguage"), + new fjs.PropertyFormatter("invalidButton"), + new fjs.PropertyFormatter("unusedOption"), + new fjs.PropertyFormatter("unusedQuestion"), + new fjs.PropertyFormatter("dropdownOption"), + new fjs.PropertyFormatter("customInvalidVersion"), + ]), + ]), + new fjs.ObjectFormatter("actions",true,[ + new fjs.ObjectFormatter("buttons",true,[ + new fjs.PropertyFormatter("create"), + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("delete"), + new fjs.PropertyFormatter("reopen"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.PropertyFormatter("clear"), + new fjs.PropertyFormatter("helpSwitchSlash"), + new fjs.PropertyFormatter("helpSwitchText"), + new fjs.PropertyFormatter("helpPage"), + new fjs.PropertyFormatter("withReason"), + new fjs.PropertyFormatter("withoutTranscript"), + ]), + new fjs.ObjectFormatter("titles",true,[ + new fjs.PropertyFormatter("created"), + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("delete"), + new fjs.PropertyFormatter("reopen"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.PropertyFormatter("rename"), + new fjs.PropertyFormatter("move"), + new fjs.PropertyFormatter("add"), + new fjs.PropertyFormatter("remove"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("help"), + new fjs.PropertyFormatter("statsReset"), + new fjs.PropertyFormatter("blacklistAdd"), + new fjs.PropertyFormatter("blacklistRemove"), + new fjs.PropertyFormatter("blacklistGet"), + new fjs.PropertyFormatter("blacklistView"), + new fjs.PropertyFormatter("blacklistAddDm"), + new fjs.PropertyFormatter("blacklistRemoveDm"), + new fjs.PropertyFormatter("clear"), + new fjs.PropertyFormatter("clearTickets"), + new fjs.PropertyFormatter("roles"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("autoclose"), + new fjs.PropertyFormatter("autocloseEnabled"), + new fjs.PropertyFormatter("autocloseDisabled"), + new fjs.PropertyFormatter("autodelete"), + new fjs.PropertyFormatter("autodeleteEnabled"), + new fjs.PropertyFormatter("autodeleteDisabled"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("topicSet"), + new fjs.PropertyFormatter("prioritySet"), + new fjs.PropertyFormatter("priorityGet"), + new fjs.PropertyFormatter("transfer"), + ]), + new fjs.ObjectFormatter("descriptions",true,[ + new fjs.PropertyFormatter("create"), + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("delete"), + new fjs.PropertyFormatter("reopen"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.PropertyFormatter("rename"), + new fjs.PropertyFormatter("move"), + new fjs.PropertyFormatter("add"), + new fjs.PropertyFormatter("remove"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("helpExplanation"), + new fjs.PropertyFormatter("statsReset"), + new fjs.PropertyFormatter("statsError"), + new fjs.PropertyFormatter("blacklistAdd"), + new fjs.PropertyFormatter("blacklistRemove"), + new fjs.PropertyFormatter("blacklistGetSuccess"), + new fjs.PropertyFormatter("blacklistGetEmpty"), + new fjs.PropertyFormatter("blacklistViewEmpty"), + new fjs.PropertyFormatter("blacklistViewTip"), + new fjs.PropertyFormatter("clearVerify"), + new fjs.PropertyFormatter("clearReady"), + new fjs.PropertyFormatter("rolesEmpty"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("autocloseLeave"), + new fjs.PropertyFormatter("autocloseTimeout"), + new fjs.PropertyFormatter("autodeleteLeave"), + new fjs.PropertyFormatter("autodeleteTimeout"), + new fjs.PropertyFormatter("autocloseEnabled"), + new fjs.PropertyFormatter("autocloseDisabled"), + new fjs.PropertyFormatter("autodeleteEnabled"), + new fjs.PropertyFormatter("autodeleteDisabled"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("ticketMessageLimit"), + new fjs.PropertyFormatter("ticketMessageAutoclose"), + new fjs.PropertyFormatter("ticketMessageAutodelete"), + new fjs.PropertyFormatter("panelReady"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("topicSet"), + new fjs.PropertyFormatter("prioritySet"), + new fjs.PropertyFormatter("priorityGet"), + new fjs.PropertyFormatter("transfer"), + ]), + new fjs.ObjectFormatter("modal",true,[ + new fjs.PropertyFormatter("closePlaceholder"), + new fjs.PropertyFormatter("deletePlaceholder"), + new fjs.PropertyFormatter("reopenPlaceholder"), + new fjs.PropertyFormatter("claimPlaceholder"), + new fjs.PropertyFormatter("unclaimPlaceholder"), + new fjs.PropertyFormatter("pinPlaceholder"), + new fjs.PropertyFormatter("unpinPlaceholder"), + ]), + new fjs.ObjectFormatter("logs",true,[ + new fjs.PropertyFormatter("createLog"), + new fjs.PropertyFormatter("closeLog"), + new fjs.PropertyFormatter("closeDm"), + new fjs.PropertyFormatter("deleteLog"), + new fjs.PropertyFormatter("deleteDm"), + new fjs.PropertyFormatter("reopenLog"), + new fjs.PropertyFormatter("reopenDm"), + new fjs.PropertyFormatter("claimLog"), + new fjs.PropertyFormatter("claimDm"), + new fjs.PropertyFormatter("unclaimLog"), + new fjs.PropertyFormatter("unclaimDm"), + new fjs.PropertyFormatter("pinLog"), + new fjs.PropertyFormatter("pinDm"), + new fjs.PropertyFormatter("unpinLog"), + new fjs.PropertyFormatter("unpinDm"), + new fjs.PropertyFormatter("renameLog"), + new fjs.PropertyFormatter("renameDm"), + new fjs.PropertyFormatter("moveLog"), + new fjs.PropertyFormatter("moveDm"), + new fjs.PropertyFormatter("addLog"), + new fjs.PropertyFormatter("addDm"), + new fjs.PropertyFormatter("removeLog"), + new fjs.PropertyFormatter("removeDm"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("blacklistAddLog"), + new fjs.PropertyFormatter("blacklistRemoveLog"), + new fjs.PropertyFormatter("blacklistAddDm"), + new fjs.PropertyFormatter("blacklistRemoveDm"), + new fjs.PropertyFormatter("clearLog"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("transferLog"), + new fjs.PropertyFormatter("transferDm"), + new fjs.PropertyFormatter("prioritySetLog"), + new fjs.PropertyFormatter("prioritySetDm"), + new fjs.PropertyFormatter("roleUpdateLog"), + new fjs.PropertyFormatter("roleUpdateDm"), + ]), + ]), + new fjs.ObjectFormatter("transcripts",true,[ + new fjs.ObjectFormatter("success",true,[ + new fjs.PropertyFormatter("visit"), + new fjs.PropertyFormatter("ready"), + new fjs.PropertyFormatter("textFileDescription"), + new fjs.PropertyFormatter("htmlProgress"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("createdChannel"), + new fjs.PropertyFormatter("createdCreator"), + new fjs.PropertyFormatter("createdParticipant"), + new fjs.PropertyFormatter("createdActiveAdmin"), + new fjs.PropertyFormatter("createdEveryAdmin"), + new fjs.PropertyFormatter("createdOther"), + ]), + new fjs.ObjectFormatter("errors",true,[ + new fjs.PropertyFormatter("retry"), + new fjs.PropertyFormatter("continue"), + new fjs.PropertyFormatter("backup"), + new fjs.PropertyFormatter("error"), + new fjs.PropertyFormatter("title"), + ]), + new fjs.ObjectFormatter("text",true,[ + new fjs.PropertyFormatter("messagesTitle"), + new fjs.PropertyFormatter("embedTitle"), + new fjs.PropertyFormatter("fileTitle"), + new fjs.PropertyFormatter("fieldsTitle"), + new fjs.PropertyFormatter("reactionsTitle"), + new fjs.PropertyFormatter("statsTitle"), + new fjs.PropertyFormatter("emptyContent"), + new fjs.PropertyFormatter("noTitle"), + new fjs.PropertyFormatter("noDesc"), + ]), + ]), + new fjs.ObjectFormatter("errors",true,[ + new fjs.ObjectFormatter("titles",true,[ + new fjs.PropertyFormatter("internalError"), + new fjs.PropertyFormatter("optionMissing"), + new fjs.PropertyFormatter("optionInvalid"), + new fjs.PropertyFormatter("unknownCommand"), + new fjs.PropertyFormatter("noPermissions"), + new fjs.PropertyFormatter("unknownTicket"), + new fjs.PropertyFormatter("deprecatedTicket"), + new fjs.PropertyFormatter("unknownOption"), + new fjs.PropertyFormatter("unknownPanel"), + new fjs.PropertyFormatter("notInGuild"), + new fjs.PropertyFormatter("channelRename"), + new fjs.PropertyFormatter("busy"), + new fjs.PropertyFormatter("permissionError"), + ]), + new fjs.ObjectFormatter("descriptions",true,[ + new fjs.PropertyFormatter("askForInfo"), + new fjs.PropertyFormatter("askForInfoResolve"), + new fjs.PropertyFormatter("internalError"), + new fjs.PropertyFormatter("optionMissing"), + new fjs.PropertyFormatter("optionInvalid"), + new fjs.PropertyFormatter("optionInvalidChoose"), + new fjs.PropertyFormatter("unknownCommand"), + new fjs.PropertyFormatter("noPermissions"), + new fjs.PropertyFormatter("noPermissionsList"), + new fjs.PropertyFormatter("noPermissionsCooldown"), + new fjs.PropertyFormatter("noPermissionsBlacklist"), + new fjs.PropertyFormatter("noPermissionsLimitGlobal"), + new fjs.PropertyFormatter("noPermissionsLimitGlobalUser"), + new fjs.PropertyFormatter("noPermissionsLimitOption"), + new fjs.PropertyFormatter("noPermissionsLimitOptionUser"), + new fjs.PropertyFormatter("unknownTicket"), + new fjs.PropertyFormatter("deprecatedTicket"), + new fjs.PropertyFormatter("notInGuild"), + new fjs.PropertyFormatter("channelRename"), + new fjs.PropertyFormatter("channelRenameSource"), + new fjs.PropertyFormatter("busy"), + new fjs.PropertyFormatter("closeBeforeMessage"), + new fjs.PropertyFormatter("closeBeforeAdminMessage"), + new fjs.PropertyFormatter("unableToCreateTicket"), + ]), + new fjs.ObjectFormatter("optionInvalidReasons",true,[ + new fjs.PropertyFormatter("stringRegex"), + new fjs.PropertyFormatter("stringMinLength"), + new fjs.PropertyFormatter("stringMaxLength"), + new fjs.PropertyFormatter("numberInvalid"), + new fjs.PropertyFormatter("numberMin"), + new fjs.PropertyFormatter("numberMax"), + new fjs.PropertyFormatter("numberDecimal"), + new fjs.PropertyFormatter("numberNegative"), + new fjs.PropertyFormatter("numberPositive"), + new fjs.PropertyFormatter("numberZero"), + new fjs.PropertyFormatter("channelNotFound"), + new fjs.PropertyFormatter("userNotFound"), + new fjs.PropertyFormatter("roleNotFound"), + new fjs.PropertyFormatter("memberNotFound"), + new fjs.PropertyFormatter("mentionableNotFound"), + new fjs.PropertyFormatter("channelType"), + new fjs.PropertyFormatter("notInGuild"), + ]), + new fjs.ObjectFormatter("permissions",true,[ + new fjs.PropertyFormatter("developer"), + new fjs.PropertyFormatter("owner"), + new fjs.PropertyFormatter("admin"), + new fjs.PropertyFormatter("moderator"), + new fjs.PropertyFormatter("support"), + new fjs.PropertyFormatter("member"), + new fjs.PropertyFormatter("discord-administrator"), + ]), + new fjs.ObjectFormatter("actionInvalid",true,[ + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("reopen"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.PropertyFormatter("add"), + new fjs.PropertyFormatter("remove"), + ]), + ]), + new fjs.ObjectFormatter("params",true,[ + new fjs.ObjectFormatter("uppercase",true,[ + new fjs.PropertyFormatter("ticket"), + new fjs.PropertyFormatter("tickets"), + new fjs.PropertyFormatter("reason"), + new fjs.PropertyFormatter("creator"), + new fjs.PropertyFormatter("remaining"), + new fjs.PropertyFormatter("added"), + new fjs.PropertyFormatter("removed"), + new fjs.PropertyFormatter("filter"), + new fjs.PropertyFormatter("method"), + new fjs.PropertyFormatter("type"), + new fjs.PropertyFormatter("blacklisted"), + new fjs.PropertyFormatter("panel"), + new fjs.PropertyFormatter("command"), + new fjs.PropertyFormatter("system"), + new fjs.PropertyFormatter("true"), + new fjs.PropertyFormatter("false"), + new fjs.PropertyFormatter("syntax"), + new fjs.PropertyFormatter("originalName"), + new fjs.PropertyFormatter("newName"), + new fjs.PropertyFormatter("until"), + new fjs.PropertyFormatter("validOptions"), + new fjs.PropertyFormatter("validPanels"), + new fjs.PropertyFormatter("autoclose"), + new fjs.PropertyFormatter("autodelete"), + new fjs.PropertyFormatter("startupDate"), + new fjs.PropertyFormatter("version"), + new fjs.PropertyFormatter("name"), + new fjs.PropertyFormatter("role"), + new fjs.PropertyFormatter("status"), + new fjs.PropertyFormatter("claimed"), + new fjs.PropertyFormatter("pinned"), + new fjs.PropertyFormatter("creationDate"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("noone"), + new fjs.PropertyFormatter("open"), + new fjs.PropertyFormatter("closed"), + new fjs.PropertyFormatter("priority"), + new fjs.PropertyFormatter("participants"), + new fjs.PropertyFormatter("yes"), + new fjs.PropertyFormatter("no"), + new fjs.PropertyFormatter("option"), + new fjs.PropertyFormatter("topic"), + new fjs.PropertyFormatter("uptime"), + new fjs.PropertyFormatter("messages"), + new fjs.PropertyFormatter("embeds"), + new fjs.PropertyFormatter("files"), + new fjs.PropertyFormatter("components"), + new fjs.PropertyFormatter("cooldown"), + new fjs.PropertyFormatter("maxTickets"), + new fjs.PropertyFormatter("admins"), + new fjs.PropertyFormatter("roles"), + new fjs.PropertyFormatter("size"), + ]), + new fjs.ObjectFormatter("lowercase",true,[ + new fjs.PropertyFormatter("text"), + new fjs.PropertyFormatter("html"), + new fjs.PropertyFormatter("command"), + new fjs.PropertyFormatter("modal"), + new fjs.PropertyFormatter("button"), + new fjs.PropertyFormatter("dropdown"), + new fjs.PropertyFormatter("method"), + ]), + ]), + new fjs.ObjectFormatter("commands",true,[ + new fjs.PropertyFormatter("reason"), + new fjs.PropertyFormatter("help"), + new fjs.PropertyFormatter("panel"), + new fjs.PropertyFormatter("panelId"), + new fjs.PropertyFormatter("panelAutoUpdate"), + new fjs.PropertyFormatter("ticket"), + new fjs.PropertyFormatter("ticketId"), + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("delete"), + new fjs.PropertyFormatter("deleteNoTranscript"), + new fjs.PropertyFormatter("reopen"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("claimUser"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("move"), + new fjs.PropertyFormatter("moveId"), + new fjs.PropertyFormatter("rename"), + new fjs.PropertyFormatter("renameName"), + new fjs.PropertyFormatter("add"), + new fjs.PropertyFormatter("addUser"), + new fjs.PropertyFormatter("remove"), + new fjs.PropertyFormatter("removeUser"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("blacklist"), + new fjs.PropertyFormatter("blacklistView"), + new fjs.PropertyFormatter("blacklistAdd"), + new fjs.PropertyFormatter("blacklistRemove"), + new fjs.PropertyFormatter("blacklistGet"), + new fjs.PropertyFormatter("blacklistGetUser"), + new fjs.PropertyFormatter("stats"), + new fjs.PropertyFormatter("statsReset"), + new fjs.PropertyFormatter("statsGlobal"), + new fjs.PropertyFormatter("statsUser"), + new fjs.PropertyFormatter("statsUserUser"), + new fjs.PropertyFormatter("statsTicket"), + new fjs.PropertyFormatter("statsTicketTicket"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("clear"), + new fjs.PropertyFormatter("clearFilter"), + new fjs.ObjectFormatter("clearFilters",true,[ + new fjs.PropertyFormatter("all"), + new fjs.PropertyFormatter("open"), + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.PropertyFormatter("autoclose"), + ]), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("autoclose"), + new fjs.PropertyFormatter("autocloseDisable"), + new fjs.PropertyFormatter("autocloseEnable"), + new fjs.PropertyFormatter("autocloseEnableTime"), + new fjs.PropertyFormatter("autodelete"), + new fjs.PropertyFormatter("autodeleteDisable"), + new fjs.PropertyFormatter("autodeleteEnable"), + new fjs.PropertyFormatter("autodeleteEnableTime"), + new fjs.TextFormatter(""), + new fjs.PropertyFormatter("topic"), + new fjs.PropertyFormatter("topicSet"), + new fjs.PropertyFormatter("topicValue"), + new fjs.PropertyFormatter("topicList"), + new fjs.PropertyFormatter("priority"), + new fjs.PropertyFormatter("prioritySet"), + new fjs.PropertyFormatter("priorityValue"), + new fjs.PropertyFormatter("priorityGet"), + new fjs.PropertyFormatter("priorityList"), + new fjs.PropertyFormatter("transfer"), + new fjs.PropertyFormatter("transferUser"), + ]), + new fjs.ObjectFormatter("helpMenu",true,[ + new fjs.PropertyFormatter("help"), + new fjs.PropertyFormatter("ticket"), + new fjs.PropertyFormatter("close"), + new fjs.PropertyFormatter("delete"), + new fjs.PropertyFormatter("reopen"), + new fjs.PropertyFormatter("pin"), + new fjs.PropertyFormatter("unpin"), + new fjs.PropertyFormatter("move"), + new fjs.PropertyFormatter("rename"), + new fjs.PropertyFormatter("claim"), + new fjs.PropertyFormatter("unclaim"), + new fjs.PropertyFormatter("add"), + new fjs.PropertyFormatter("remove"), + new fjs.PropertyFormatter("panel"), + new fjs.PropertyFormatter("blacklistView"), + new fjs.PropertyFormatter("blacklistAdd"), + new fjs.PropertyFormatter("blacklistRemove"), + new fjs.PropertyFormatter("blacklistGet"), + new fjs.PropertyFormatter("statsGlobal"), + new fjs.PropertyFormatter("statsTicket"), + new fjs.PropertyFormatter("statsUser"), + new fjs.PropertyFormatter("statsReset"), + new fjs.PropertyFormatter("autocloseDisable"), + new fjs.PropertyFormatter("autocloseEnable"), + new fjs.PropertyFormatter("autodeleteDisable"), + new fjs.PropertyFormatter("autodeleteEnable"), + new fjs.ObjectFormatter("categories",true,[ + new fjs.PropertyFormatter("general"), + new fjs.PropertyFormatter("basicTicket"), + new fjs.PropertyFormatter("advancedTicket"), + new fjs.PropertyFormatter("userTicket"), + new fjs.PropertyFormatter("admin"), + new fjs.PropertyFormatter("advanced"), + new fjs.PropertyFormatter("extra"), + ]) + ]), + new fjs.ObjectFormatter("stats",true,[ + new fjs.ObjectFormatter("scopes",true,[ + new fjs.PropertyFormatter("global"), + new fjs.PropertyFormatter("system"), + new fjs.PropertyFormatter("user"), + new fjs.PropertyFormatter("ticket"), + new fjs.PropertyFormatter("participants"), + new fjs.PropertyFormatter("messages"), + ]), + new fjs.ObjectFormatter("properties",true,[ + new fjs.PropertyFormatter("ticketsCreated"), + new fjs.PropertyFormatter("ticketsClosed"), + new fjs.PropertyFormatter("ticketsDeleted"), + new fjs.PropertyFormatter("ticketsReopened"), + new fjs.PropertyFormatter("ticketsAutoclosed"), + new fjs.PropertyFormatter("ticketsClaimed"), + new fjs.PropertyFormatter("ticketsPinned"), + new fjs.PropertyFormatter("ticketsMoved"), + new fjs.PropertyFormatter("usersBlacklisted"), + new fjs.PropertyFormatter("transcriptsCreated"), + new fjs.PropertyFormatter("ticketsAutodeleted"), + new fjs.PropertyFormatter("ticketsTransferred"), + new fjs.PropertyFormatter("ticketVolume"), + new fjs.PropertyFormatter("averageTickets"), + new fjs.PropertyFormatter("currentTickets"), + new fjs.PropertyFormatter("age"), + new fjs.PropertyFormatter("responseTime"), + new fjs.PropertyFormatter("resolutionTime"), + new fjs.PropertyFormatter("createdOn"), + new fjs.PropertyFormatter("createdBy"), + new fjs.PropertyFormatter("closedOn"), + new fjs.PropertyFormatter("closedBy"), + new fjs.PropertyFormatter("claimedOn"), + new fjs.PropertyFormatter("claimedBy"), + new fjs.PropertyFormatter("pinnedOn"), + new fjs.PropertyFormatter("pinnedBy"), + new fjs.PropertyFormatter("deletedOn"), + new fjs.PropertyFormatter("deletedBy"), + ]), + new fjs.ObjectFormatter("roles",true,[ + new fjs.PropertyFormatter("developer"), + new fjs.PropertyFormatter("serverOwner"), + new fjs.PropertyFormatter("serverAdmin"), + new fjs.PropertyFormatter("moderator"), + new fjs.PropertyFormatter("support"), + new fjs.PropertyFormatter("member"), + ]) + ]), + new fjs.ObjectFormatter("panel",true,[ + new fjs.PropertyFormatter("selectTicket"), + new fjs.PropertyFormatter("selectRole"), + new fjs.PropertyFormatter("selectOption"), + ]), + new fjs.ObjectFormatter("priorities",true,[ + new fjs.PropertyFormatter("urgent"), + new fjs.PropertyFormatter("veryHigh"), + new fjs.PropertyFormatter("high"), + new fjs.PropertyFormatter("normal"), + new fjs.PropertyFormatter("low"), + new fjs.PropertyFormatter("veryLow"), + new fjs.PropertyFormatter("none"), + ]), +]) + +for (const language of fs.readdirSync(".docs/languages/")){ + if (!fs.existsSync("./languages/"+language)){ + console.log("language:",language,"does not exist yet in the primary ./languages/ folder. Unable to merge!") + continue + } + console.log("merging "+language+"...") + const original = JSON.parse(fs.readFileSync("./languages/"+language).toString()) + const newSentences = JSON.parse(fs.readFileSync(".docs/languages/"+language).toString()) + + for (const key of Object.keys(newSentences)){ + if (key.startsWith("_")) continue + try{ + const splitted = key.split(".") + let currentObject = original + splitted.forEach((property,index) => { + let shouldBeObject = (splitted.length-1 !== index) + if (shouldBeObject && typeof currentObject[property] == "object"){ + currentObject = currentObject[property] + }else if (shouldBeObject && typeof currentObject[property] == "undefined"){ + currentObject[property] = {} + currentObject = currentObject[property] + }else if (typeof currentObject[property] == "string" || typeof currentObject[property] == "undefined"){ + currentObject[property] = newSentences[key] + }else{ + console.log("Failed to merge key:",key,"in file:",language,"--> Invalid type:",typeof currentObject[property]) + } + }) + }catch(err){ + console.log("Failed to merge key:",key,"in file:",language) + } + } + original["_TRANSLATION"]["lastedited"] = new Date().toLocaleDateString("nl-BE",{day:"2-digit",month:"2-digit",year:"numeric"}) + original["_TRANSLATION"]["otversion"] = "v4.1.0" + const finalText = formatter.stringify(original) + fs.writeFileSync("./languages/"+language,finalText) +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3a10e77..243efae 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ otdebug.txt .docs/* !.docs/createDocs.js +!.docs/mergeTranslations.js !.docs/typedoc-config.json \ No newline at end of file diff --git a/package.json b/package.json index dbb3e3e..b80f5ce 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "test": "node index.js --dev-config --dev-database --soft-plugins", "testsetup": "node index.js --cli --dev-config --dev-database --soft-plugins", "testnc": "node index.js --no-compile --dev-config --dev-database --soft-plugins", - "docs": "npx typedoc --options .docs/typedoc-config.json && node .docs/createDocs.js" + "docs": "npx typedoc --options .docs/typedoc-config.json && node .docs/createDocs.js", + "mergelang": "node .docs/mergeTranslations.js" }, "type": "commonjs", "license": "GPL-3.0-only", From 9668851a8212fdf3b4d93300f577d3e5d6480188 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 9 Nov 2025 13:04:51 +0100 Subject: [PATCH 77/78] Added 9 updated translations for v4.1 --- languages/catalan.json | 1084 +++++++++++++++++++++---------------- languages/custom.json | 180 +++++- languages/dutch.json | 180 +++++- languages/english.json | 180 +++++- languages/estonian.json | 184 ++++++- languages/finnish.json | 186 ++++++- languages/hindi.json | 184 ++++++- languages/indonesian.json | 182 ++++++- languages/thai.json | 956 ++++++++++++++++++-------------- 9 files changed, 2243 insertions(+), 1073 deletions(-) diff --git a/languages/catalan.json b/languages/catalan.json index 6f296d7..5f09dce 100644 --- a/languages/catalan.json +++ b/languages/catalan.json @@ -1,478 +1,608 @@ -{ - "_TRANSLATION":{ - "otversion":"v4.1.0", - "translators":["guillee3"], - "lastedited":"21/08/2024", - "language":"Catalan", - "automated":false - }, - "checker":{ - "system":{ - "typeError":"[ERROR]", - "headerOpenTicket":"OPEN TICKET", - "typeWarning":"[AVÍS]", - "typeInfo":"[INFO]", - "headerConfigChecker":"VERIFICADOR DE CONFIGURACIÓ", - "headerDescription":"comprova si existeixen errors als teus fitxers de configuració!", - "footerError":"el bot no s'iniciarà fins que tots els {0} es solucionin!", - "footerWarning":"es recomana solucionar tots els {0} abans de començar!", - "footerSupport":"SUPORT: {0} - DOCUMENTACIÓ: {1}", - "compactInformation":"utilitza {0} per obtenir més informació!", - "dataPath":"ruta", - "dataDocs":"documents", - "dataMessages":"missatge" - }, - "messages":{ - "stringTooShort":"Aquesta cadena no pot tenir menys de {0} caràcters!", - "stringTooLong":"Aquesta cadena no pot tenir més de {0} caràcters!", - "stringLengthInvalid":"Aquesta cadena ha de tenir {0} caràcters!", - "stringStartsWith":"Aquesta cadena ha de començar amb {0}!", - "stringEndsWith":"Aquesta cadena ha d'acabar amb {0}!", - "stringContains":"Aquesta cadena ha de contenir {0}!", - "stringChoices":"Aquesta cadena només pot ser un dels valors següents: {0}!", - "stringRegex":"Aquesta cadena és invàlida!", - - "numberTooShort":"Aquest nombre no pot tenir menys de {0} caràcters!", - "numberTooLong":"Aquest nombre no pot tenir més de {0} caràcters!", - "numberLengthInvalid":"Aquest nombre ha de tenir {0} caràcters!", - "numberTooSmall":"Aquest nombre ha de ser almenys {0}!", - "numberTooLarge":"Aquest nombre ha de ser com a màxim {0}!", - "numberNotEqual":"Aquest nombre ha de ser {0}!", - "numberStep":"Aquest nombre ha de ser un múltiple de {0}!", - "numberStepOffset":"Aquest nombre ha de ser un múltiple de {0} començant amb {1}!", - "numberStartsWith":"Aquest nombre ha de començar amb {0}!", - "numberEndsWith":"Aquest nombre ha d'acabar amb {0}!", - "numberContains":"Aquest nombre ha de contenir {0}!", - "numberChoices":"Aquest nombre només pot ser un dels valors següents: {0}!", - "numberFloat":"Aquest nombre no pot ser un decimal!", - "numberNegative":"Aquest nombre no pot ser negatiu!", - "numberPositive":"Aquest nombre no pot ser positiu!", - "numberZero":"Aquest nombre no pot ser zero!", - - "booleanTrue":"Aquest booleà no pot ser veritable!", - "booleanFalse":"Aquest booleà no pot ser fals!", - - "arrayEmptyDisabled":"Aquest array no pot estar buit!", - "arrayEmptyRequired":"Aquest array ha d'estar buit!", - "arrayTooShort":"Aquest array ha de tenir una longitud d'almenys {0}!", - "arrayTooLong":"Aquest array ha de tenir una longitud màxima de {0}!", - "arrayLengthInvalid":"Aquest array ha de tenir una longitud de {0}!", - "arrayInvalidTypes":"Aquest array només pot contenir els tipus següents: {0}!", - "arrayDouble":"Aquest array no permet el mateix valor dues vegades!", - - "discordInvalidId":"Aquest és un id de {0} de discord invàlid!", - "discordInvalidIdOptions":"Aquest és un id de {0} de discord invàlid! També en pots utilitzar un d'aquests: {1}!", - "discordInvalidToken":"Aquest és un token de discord invàlid (sintàcticament)!", - "colorInvalid":"Aquest és un color hex invàlid!", - "emojiTooShort":"Aquesta cadena ha de tenir almenys {0} emojis!", - "emojiTooLong":"Aquesta cadena ha de tenir com a màxim {0} emojis!", - "emojiCustom":"Aquest emoji no pot ser un emoji personalitzat de discord!", - "emojiInvalid":"Aquest és un emoji invàlid!", - "urlInvalid":"Aquesta url és invàlida!", - "urlInvalidHttp":"Aquesta url només pot utilitzar el protocol https://!", - "urlInvalidProtocol":"Aquesta url només pot utilitzar els protocols http:// i https://!", - "urlInvalidHostname":"Aquesta url té un nom de domini no permès!", - "urlInvalidExtension":"Aquesta url té una extensió invàlida! Tria entre: {0}!", - "urlInvalidPath":"Aquesta url té un camí invàlid!", - "idNotUnique":"Aquest id no és únic, utilitza un altre id!", - "idNonExistent":"L'id {0} no existeix!", - - "invalidType":"Aquesta propietat ha de ser del tipus: {0}!", - "propertyMissing":"La propietat {0} falta en aquest objecte!", - "propertyOptional":"La propietat {0} és opcional en aquest objecte!", - "objectDisabled":"Aquest objecte està desactivat, activa'l utilitzant {0}!", - "nullInvalid":"Aquesta propietat no pot ser nul·la!", - "switchInvalidType":"Això ha de ser d'un dels tipus següents: {0}!", - "objectSwitchInvalid":"Aquest objecte ha de ser d'un dels tipus següents: {0}!", - - "invalidLanguage":"Aquest és un idioma invàlid!", - "invalidButton":"Aquest botó ha de tenir almenys un {0} o {1}!", - "unusedOption":"L'opció {0} no s'utilitza enlloc!", - "unusedQuestion":"La pregunta {0} no s'utilitza enlloc!", - "dropdownOption":"Un panell amb desplegable activat només pot contenir opcions del tipus 'ticket'!" - } - }, - "actions":{ - "buttons":{ - "create":"Veure Ticket", - "close":"Tancar Ticket", - "delete":"Eliminar Ticket", - "reopen":"Reobrir Ticket", - "claim":"Reclamar Ticket", - "unclaim":"Alliberar Ticket", - "pin":"Fixar Ticket", - "unpin":"Desfixar Ticket", - "clear":"Eliminar Tickets", - "helpSwitchSlash":"Veure Comandes Slash", - "helpSwitchText":"Veure Comandes de Text", - "helpPage":"Pàgina {0}", - "withReason":"Amb Motiu", - "withoutTranscript":"Sense Transcripció" - }, - "titles":{ - "created":"Ticket Creat", - "close":"Ticket Tancat", - "delete":"Ticket Eliminat", - "reopen":"Ticket Reobert", - "claim":"Ticket Reclamat", - "unclaim":"Ticket Alliberat", - "pin":"Ticket Fixat", - "unpin":"Ticket Desfixat", - "rename":"Ticket Renomenat", - "move":"Ticket Mogut", - "add":"Usuari Afegit al Ticket", - "remove":"Usuari Eliminat del Ticket", - - "help":"Comandes Disponibles", - "statsReset":"Restablir Estadístiques", - "blacklistAdd":"Usuari a la Llista Negra", - "blacklistRemove":"Usuari Alliberat", - "blacklistGet":"Usuari a la Llista Negra", - "blacklistView":"Llista Negra Actual", - "blacklistAddDm":"Afegit a la Llista Negra", - "blacklistRemoveDm":"Eliminat de la Llista Negra", - "clear":"Tickets Netejats", - "roles":"Rols Actualitzats", - - "autoclose":"Ticket Tancat Automàticament", - "autocloseEnabled":"Tancament Automàtic Activat", - "autocloseDisabled":"Tancament Automàtic Desactivat", - "autodelete":"Ticket Eliminat Automàticament", - "autodeleteEnabled":"Eliminació Automàtica Activada", - "autodeleteDisabled":"Eliminació Automàtica Desactivada" - }, - "descriptions":{ - "create":"El teu ticket ha estat creat. Fes clic al botó a continuació per accedir-hi!", - "close":"El ticket ha estat tancat amb èxit!", - "delete":"El ticket ha estat eliminat amb èxit!", - "reopen":"El ticket ha estat reobert amb èxit!", - "claim":"El ticket ha estat reclamat amb èxit!", - "unclaim":"El ticket ha estat alliberat amb èxit!", - "pin":"El ticket ha estat fixat amb èxit!", - "unpin":"El ticket ha estat desfixat amb èxit!", - "rename":"El ticket ha estat renomenat a {0} amb èxit!", - "move":"El ticket ha estat mogut a {0} amb èxit!", - "add":"{0} ha estat afegit al ticket amb èxit!", - "remove":"{0} ha estat eliminat del ticket amb èxit!", - - "helpExplanation":"`` => paràmetre requerit\n`[nom]` => paràmetre opcional", - "statsReset":"Les estadístiques del bot s'han restablert correctament!", - "statsError":"No es poden veure les estadístiques del ticket!\n{0} no és un ticket!", - "blacklistAdd":"{0} ha estat afegit a la llista negra amb èxit!", - "blacklistRemove":"{0} ha estat alliberat amb èxit!", - "blacklistGetSuccess":"{0} està actualment a la llista negra!", - "blacklistGetEmpty":"{0} actualment no està a la llista negra!", - "blacklistViewEmpty":"Encara no hi ha ningú a la llista negra!", - "blacklistViewTip":"Utilitza \"/blacklist add\" per afegir un usuari a la llista negra!", - "clearVerify":"Estàs segur que vols eliminar múltiples tickets?\nAquesta acció no es pot desfer!", - "clearReady":"{0} tickets han estat eliminats amb èxit!", - "rolesEmpty":"No s'han actualitzat rols!", - - "autocloseLeave":"Aquest ticket ha estat tancat automàticament perquè el creador ha abandonat el servidor!", - "autocloseTimeout":"Aquest ticket ha estat tancat automàticament perquè ha estat inactiu durant més de `{0}h`!", - "autodeleteLeave":"Aquest ticket ha estat eliminat automàticament perquè el creador ha abandonat el servidor!", - "autodeleteTimeout":"Aquest ticket ha estat eliminat automàticament perquè ha estat inactiu durant més de `{0} dies`!", - "autocloseEnabled":"El tancament automàtic ha estat activat en aquest ticket!\nEs tancarà quan estigui inactiu durant més de `{0}h`!", - "autocloseDisabled":"El tancament automàtic ha estat desactivat en aquest ticket!\nJa no es tancarà automàticament!", - "autodeleteEnabled":"L'eliminació automàtica ha estat activada en aquest ticket!\nS'eliminarà quan estigui inactiu durant més de `{0} dies`!", - "autodeleteDisabled":"L'eliminació automàtica ha estat desactivada en aquest ticket!\nJa no s'eliminarà automàticament!", - - "ticketMessageLimit":"Només pots crear {0} ticket(s) al mateix temps!", - "ticketMessageAutoclose":"Aquest ticket es tancarà automàticament quan estigui inactiu durant {0}h!", - "ticketMessageAutodelete":"Aquest ticket es eliminarà automàticament quan estigui inactiu durant {0} dies!", - "panelReady":"Pots trobar el panell a continuació!\nAquest missatge ara es pot eliminar!" - }, - "modal":{ - "closePlaceholder":"Per què has tancat aquest ticket?", - "deletePlaceholder":"Per què has eliminat aquest ticket?", - "reopenPlaceholder":"Per què has reobert aquest ticket?", - "claimPlaceholder":"Per què has reclamat aquest ticket?", - "unclaimPlaceholder":"Per què has alliberat aquest ticket?", - "pinPlaceholder":"Per què has fixat aquest ticket?", - "unpinPlaceholder":"Per què has desfixat aquest ticket?" - }, - "logs":{ - "createLog":"Un nou ticket ha estat creat per {0}!", - "closeLog":"Aquest ticket ha estat tancat per {0}!", - "closeDm":"El teu ticket ha estat tancat al nostre servidor!", - "deleteLog":"Aquest ticket ha estat eliminat per {0}!", - "deleteDm":"El teu ticket ha estat eliminat al nostre servidor!", - "reopenLog":"Aquest ticket ha estat reobert per {0}!", - "reopenDm":"El teu ticket ha estat reobert al nostre servidor!", - "claimLog":"Aquest ticket ha estat reclamat per {0}!", - "claimDm":"El teu ticket ha estat reclamat al nostre servidor!", - "unclaimLog":"Aquest ticket ha estat alliberat per {0}!", - "unclaimDm":"El teu ticket ha estat alliberat al nostre servidor!", - "pinLog":"Aquest ticket ha estat fixat per {0}!", - "pinDm":"El teu ticket ha estat fixat al nostre servidor!", - "unpinLog":"Aquest ticket ha estat desfixat per {0}!", - "unpinDm":"El teu ticket ha estat desfixat al nostre servidor!", - "renameLog":"Aquest ticket ha estat renombrat a {0} per {1}!", - "renameDm":"El teu ticket ha estat renombrat a {0} al nostre servidor!", - "moveLog":"Aquest ticket ha estat mogut a {0} per {1}!", - "moveDm":"El teu ticket ha estat mogut a {0} al nostre servidor!", - "addLog":"{0} ha estat afegit a aquest ticket per {1}!", - "addDm":"{0} ha estat afegit al teu ticket al nostre servidor!", - "removeLog":"{0} ha estat eliminat d'aquest ticket per {1}!", - "removeDm":"{0} ha estat eliminat del teu ticket al nostre servidor!", - - "blacklistAddLog":"{0} ha estat afegit a la llista negra per {1}!", - "blacklistRemoveLog":"{0} ha estat eliminat de la llista negra per {1}!", - "blacklistAddDm":"Has estat afegit a la llista negra al nostre servidor!\nA partir d'ara, no pots crear cap ticket!", - "blacklistRemoveDm":"Has estat eliminat de la llista negra al nostre servidor!\nAra pots crear tickets de nou!", - "clearLog":"{0} tickets han estat eliminats per {1}!" - } - }, - "transcripts":{ - "success":{ - "visit":"Veure Transcripció", - "ready":"Transcripció Creada", - "textFileDescription":"Aquesta és la transcripció de text d'un ticket eliminat!", - "htmlProgress":"Si us plau, espera mentre aquesta transcripció html es processa...", - - "createdChannel":"S'ha creat una nova transcripció tipus {0} al servidor!", - "createdCreator":"S'ha creat una nova transcripció tipus {0} per un dels teus tickets!", - "createdParticipant":"S'ha creat una nova transcripció tipus {0} en un dels tickets on has participat!", - "createdActiveAdmin":"S'ha creat una nova transcripció tipus {0} en un dels tickets on has participat com a administrador!", - "createdEveryAdmin":"S'ha creat una nova transcripció tipus {0} en un dels tickets on eres administrador!", - "createdOther":"S'ha creat una nova transcripció tipus {0}!" - }, - "errors":{ - "retry":"Torna-ho a intentar", - "continue":"Elimina Sense Transcripció", - "backup":"Crea Transcripció de Backup", - "error":"Alguna cosa ha anat malament mentre intentàvem crear la transcripció.\nQuè t'agradaria fer?\n\nAquest ticket no s'eliminarà fins que facis clic en un d'aquests botons." - } - }, - "errors":{ - "titles":{ - "internalError":"Error Intern", - "optionMissing":"Opció de Comanda Absent", - "optionInvalid":"Opció de Comanda Invàlida", - "unknownCommand":"Comanda Desconeguda", - "noPermissions":"No Tens Permisos", - "unknownTicket":"Ticket Desconegut", - "deprecatedTicket":"Ticket Obsolet", - "unknownOption":"Opció Desconeguda", - "unknownPanel":"Panell Desconegut", - "notInGuild":"No Ets Al Servidor", - "channelRename":"No Es Pot Canviar El Nom Del Canal", - "busy":"El Ticket Està Ocupat" - }, - "descriptions":{ - "askForInfo":"Contacta amb el propietari d'aquest bot per obtenir més informació!", - "askForInfoResolve":"Contacta amb el propietari del bot si aquest problema no es resol després de diversos intents.", - "internalError":"No s'ha pogut respondre a aquest {0} a causa d'un error intern!", - "optionMissing":"Falta un paràmetre requerit en aquesta comanda!", - "optionInvalid":"Un paràmetre d'aquesta comanda és invàlid!", - "optionInvalidChoose":"Tria entre", - "unknownCommand":"Prova de visitar el menú d'ajuda per obtenir més informació!", - "noPermissions":"No tens permís per utilitzar aquest {0}!", - "noPermissionsList":"Permisos Requerits: (un d'ells)", - "noPermissionsCooldown":"No tens permís per utilitzar aquest {0} perquè tens un temps de refredament!", - "noPermissionsBlacklist":"No tens permís per utilitzar aquest {0} perquè estàs a la llista negra!", - "noPermissionsLimitGlobal":"No tens permís per crear un ticket perquè el servidor ha arribat al límit màxim de tickets!", - "noPermissionsLimitGlobalUser":"No tens permís per crear un ticket perquè has arribat al límit màxim de tickets!", - "noPermissionsLimitOption":"No tens permís per crear un ticket perquè el servidor ha arribat al límit màxim de tickets per aquesta opció!", - "noPermissionsLimitOptionUser":"No tens permís per crear un ticket perquè has arribat al límit màxim de tickets per aquesta opció!", - "unknownTicket":"Torna a intentar aquesta comanda en un ticket vàlid!", - "deprecatedTicket":"El canal actual no és un ticket vàlid! Pot ser que fos un ticket d'una versió antiga de Open Ticket!", - "notInGuild":"Aquest {0} no funciona en DM! Si us plau, torna a intentar-ho en un servidor!", - "channelRename":"Degut a les limitacions de discord, actualment és impossible per al bot canviar el nom del canal. El canal es canviarà automàticament en 10 minuts si el bot no es reinicia.", - "channelRenameSource":"La font d'aquest error és: {0}", - "busy":"No es pot utilitzar aquest {0}!\nEl ticket està sent processat pel bot.\n\nSi us plau, torna a intentar-ho en uns segons!" - }, - "optionInvalidReasons":{ - "stringRegex":"El valor no coincideix amb el patró!", - "stringMinLength":"El valor ha de tenir almenys {0} caràcters!", - "stringMaxLength":"El valor ha de tenir com a màxim {0} caràcters!", - "numberInvalid":"Nombre invàlid!", - "numberMin":"El nombre ha de ser almenys {0}!", - "numberMax":"El nombre ha de ser com a màxim {0}!", - "numberDecimal":"El nombre no pot ser decimal!", - "numberNegative":"El nombre no pot ser negatiu!", - "numberPositive":"El nombre no pot ser positiu!", - "numberZero":"El nombre no pot ser zero!", - "channelNotFound":"No es pot trobar el canal!", - "userNotFound":"No es pot trobar l'usuari!", - "roleNotFound":"No es pot trobar el rol!", - "memberNotFound":"No es pot trobar l'usuari!", - "mentionableNotFound":"No es pot trobar l'usuari o el rol!", - "channelType":"Tipus de canal invàlid!", - "notInGuild":"Aquesta opció requereix que siguis en un servidor!" - }, - "permissions":{ - "developer":"Has de ser el desenvolupador del bot.", - "owner":"Has de ser el propietari del servidor.", - "admin":"Has de ser un administrador del servidor.", - "moderator":"Has de ser un moderador.", - "support":"Has de formar part de l'equip de suport.", - "member":"Has de ser un membre.", - "discord-administrator":"Has de tenir el permís `ADMINISTRADOR`." - }, - "actionInvalid":{ - "close":"El ticket ja està tancat!", - "reopen":"El ticket encara no està tancat!", - "claim":"El ticket ja està reclamat!", - "unclaim":"El ticket encara no està reclamat!", - "pin":"El ticket ja està fixat!", - "unpin":"El ticket encara no està fixat!", - "add":"Aquest usuari ja té accés al ticket!", - "remove":"No es pot eliminar aquest usuari del ticket!" - } - }, - "params":{ - "uppercase":{ - "ticket":"Ticket", - "tickets":"Tickets", - "reason":"Motiu", - "creator":"Creador", - "remaining":"Temps Restant", - "added":"Afegit", - "removed":"Eliminat", - "filter":"Filtre", - "claimedBy":"Reclamat Per {0}", - "method":"Mètode", - "type":"Tipus", - "blacklisted":"Llista Negra", - "panel":"Panell", - "command":"Comanda", - "system":"Sistema", - "true":"Cert", - "false":"Fals", - "syntax":"Sintaxi", - "originalName":"Nom Original", - "newName":"Nom Nou", - "until":"Fins a", - "validOptions":"Opcions Vàlides", - "validPanels":"Panells Vàlids", - "autoclose":"Tancament Automàtic", - "autodelete":"Eliminació Automàtica", - "startupDate":"Data d'Inici", - "version":"Versió", - "name":"Nom", - "role":"Rol", - "status":"Estat", - "claimed":"Reclamat", - "pinned":"Fixat", - "creationDate":"Data de Creació" - }, - "lowercase":{ - "text":"text", - "html":"html", - "command":"comanda", - "modal":"modal", - "button":"botó", - "dropdown":"desplegable", - "method":"mètode" - } - }, - "commands":{ - "reason":"Especifica una raó opcional que serà visible als registres.", - "help":"Obtingues una llista de totes les comandes disponibles.", - "panel":"Genera un missatge amb un desplegable o botons (per a la creació de tickets).", - "panelId":"L'identificador del panell que vols generar.", - "panelAutoUpdate":"Vols que aquest panell s'actualitzi automàticament quan s'edita?", - "ticket":"Crea instantàniament un ticket.", - "ticketId":"L'identificador del ticket que vols crear.", - "close":"Tanca un ticket.", - "delete":"Elimina un ticket.", - "deleteNoTranscript":"Elimina aquest ticket sense crear una transcripció.", - "reopen":"Reobre un ticket.", - "claim":"Reclama un ticket.", - "claimUser":"Reclama aquest ticket per a una altra persona en lloc de tu mateix.", - "unclaim":"Allibera un ticket.", - "pin":"Fixa un ticket.", - "unpin":"Desfixa un ticket.", - "move":"Mou un ticket.", - "moveId":"L'identificador de l'opció a la qual vols moure't.", - "rename":"Renomena un ticket.", - "renameName":"El nou nom per aquest ticket.", - "add":"Afegeix un usuari a un ticket.", - "addUser":"L'usuari a afegir.", - "remove":"Elimina un usuari d'un ticket.", - "removeUser":"L'usuari a eliminar.", - "blacklist":"Gestiona la llista negra de tickets.", - "blacklistView":"Veure una llista de l'actual llista negra.", - "blacklistAdd":"Afegeix un usuari a la llista negra.", - "blacklistRemove":"Elimina un usuari de la llista negra.", - "blacklistGet":"Obtingues els detalls d'un usuari a la llista negra.", - "blacklistGetUser":"L'usuari dels qual vols obtenir detalls.", - "stats":"Veure estadístiques del bot, d'un membre o d'un ticket.", - "statsReset":"Restablir totes les estadístiques del bot (i començar a comptar des de zero).", - "statsGlobal":"Veure les estadístiques globals.", - "statsUser":"Veure les estadístiques d'un usuari al servidor.", - "statsUserUser":"L'usuari del qual vols veure les estadístiques.", - "statsTicket":"Veure les estadístiques d'un ticket al servidor.", - "statsTicketTicket":"El ticket que vols veure.", - "clear":"Elimina múltiples tickets al mateix temps.", - "clearFilter":"El filtre per a la neteja de tickets.", - "clearFilters":{ - "all":"Tots", - "open":"Oberts", - "close":"Tancats", - "claim":"Reclamats", - "unclaim":"Alliberats", - "pin":"Fixats", - "unpin":"Desfixats", - "autoclose":"Tancat Automàticaments" - }, - "autoclose":"Gestiona el tancament automàtic en un ticket.", - "autocloseDisable":"Desactiva el tancament automàtic en aquest ticket.", - "autocloseEnable":"Activa el tancament automàtic en aquest ticket.", - "autocloseEnableTime":"La quantitat d'hores que aquest ticket ha d'estar inactiu per tancar-lo.", - "autodelete":"Gestiona l'eliminació automàtica en un ticket.", - "autodeleteDisable":"Desactiva l'eliminació automàtica en aquest ticket.", - "autodeleteEnable":"Activa l'eliminació automàtica en aquest ticket.", - "autodeleteEnableTime":"La quantitat de dies que aquest ticket ha d'estar inactiu per eliminar-lo." - }, - "helpMenu":{ - "help":"Obtingues una llista de totes les comandes disponibles.", - "ticket":"Crea instantàniament un ticket.", - "close":"Tanca un ticket, això desactiva l'escriptura en aquest canal.", - "delete":"Elimina un ticket, això crea una transcripció quan està activat.", - "reopen":"Reobre un ticket, això permet escriure de nou en aquest canal.", - "pin":"Fixar un ticket. Això mou el ticket a la part superior i afegeix un emoji '📌' al nom.", - "unpin":"Desfixar un ticket. El ticket romandrà a la seva posició però perdrà l'emoji '📌'.", - "move":"Mou un ticket. Això canvia el tipus d'aquest ticket.", - "rename":"Renomena un ticket. Això canvia el nom del canal d'aquest ticket.", - "claim":"Reclama un ticket. Amb això, pots fer saber al teu equip que estàs gestionant aquest ticket.", - "unclaim":"Allibera un ticket. Amb això, pots fer saber al teu equip que aquest ticket està lliure de nou.", - "add":"Afegeix un usuari a un ticket. Això permetrà a l'usuari llegir i escriure en aquest ticket.", - "remove":"Elimina un usuari d'un ticket. Això eliminarà la capacitat de llegir i escriure per a un usuari en aquest ticket.", - "panel":"Genera un missatge amb un desplegable o botons (per a la creació de tickets).", - "blacklistView":"Veure una la llista negra actual.", - "blacklistAdd":"Afegeix un usuari a la llista negra.", - "blacklistRemove":"Elimina un usuari de la llista negra.", - "blacklistGet":"Obtingues els detalls d'un usuari de la llista negra.", - "statsGlobal":"Veure les estadístiques globals.", - "statsTicket":"Veure les estadístiques d'un ticket al servidor.", - "statsUser":"Veure les estadístiques d'un usuari al servidor.", - "statsReset":"Restablir totes les estadístiques del bot (i començar a comptar des de zero).", - "autocloseDisable":"Desactiva el tancament automàtic en aquest ticket.", - "autocloseEnable":"Activa el tancament automàtic en aquest ticket.", - "autodeleteDisable":"Desactiva l'eliminació automàtica en aquest ticket.", - "autodeleteEnable":"Activa l'eliminació automàtica en aquest ticket." - }, - "stats":{ - "scopes":{ - "global":"Estadístiques Globals", - "system":"Estadístiques del Sistema", - "user":"Estadístiques d'Usuari", - "ticket":"Estadístiques de Ticket", - "participants":"Participants" - }, - "properties":{ - "ticketsCreated":"Tickets Creats", - "ticketsClosed":"Tickets Tancats", - "ticketsDeleted":"Tickets Eliminats", - "ticketsReopened":"Tickets Reoberts", - "ticketsAutoclosed":"Tickets Tancats Automàticament", - "ticketsClaimed":"Tickets Reclamats", - "ticketsPinned":"Tickets Fixats", - "ticketsMoved":"Tickets Moguts", - "usersBlacklisted":"Usuaris a la Llista Negra", - "transcriptsCreated":"Transcripcions Creades" - } - } +{ + "_TRANSLATION":{ + "otversion":"v4.1.0", + "translators":["guillee3"], + "lastedited":"09/11/2025", + "language":"Catalan", + "automated":false + }, + "checker":{ + "system":{ + "typeError":"[ERROR]", + "headerOpenTicket":"OPEN TICKET", + "typeWarning":"[AVÍS]", + "typeInfo":"[INFO]", + "headerConfigChecker":"VERIFICADOR DE CONFIGURACIÓ", + "headerDescription":"comprova si existeixen errors als teus fitxers de configuració!", + "footerError":"el bot no s'iniciarà fins que tots els {0} es solucionin!", + "footerWarning":"es recomana solucionar tots els {0} abans de començar!", + "footerSupport":"SUPORT: {0} - DOCUMENTACIÓ: {1}", + "compactInformation":"utilitza {0} per obtenir més informació!", + "dataPath":"ruta", + "dataDocs":"documents", + "dataMessages":"missatge" + }, + "messages":{ + "stringTooShort":"Aquesta cadena no pot tenir menys de {0} caràcters!", + "stringTooLong":"Aquesta cadena no pot tenir més de {0} caràcters!", + "stringLengthInvalid":"Aquesta cadena ha de tenir {0} caràcters!", + "stringStartsWith":"Aquesta cadena ha de començar amb {0}!", + "stringEndsWith":"Aquesta cadena ha d'acabar amb {0}!", + "stringContains":"Aquesta cadena ha de contenir {0}!", + "stringChoices":"Aquesta cadena només pot ser un dels valors següents: {0}!", + "stringRegex":"Aquesta cadena és invàlida!", + "stringInvertedContains":"Aquesta cadena no pot contenir {0}!", + "stringLowercase":"Aquesta cadena ha d'estar escrita només en minúscules!", + "stringUppercase":"Aquesta cadena ha d'estar escrita només en majúscules!", + "stringSpecialCharacters":"Aquesta cadena no pot contenir cap caràcter especial! (només a-z, 0-9 i espai)", + "stringNoSpaces":"Aquesta cadena no pot contenir espais!", + "stringCapitalWord":"Es recomana que cada paraula d'aquesta cadena comenci amb una lletra majúscula!", + "stringCapitalSentence":"Sembla que algunes frases d'aquesta cadena no comencen amb una lletra majúscula!", + "stringPunctuation":"Sembla que la frase d'aquesta cadena no acaba amb un signe de puntuació!", + + "numberTooShort":"Aquest nombre no pot tenir menys de {0} caràcters!", + "numberTooLong":"Aquest nombre no pot tenir més de {0} caràcters!", + "numberLengthInvalid":"Aquest nombre ha de tenir {0} caràcters!", + "numberTooSmall":"Aquest nombre ha de ser almenys {0}!", + "numberTooLarge":"Aquest nombre ha de ser com a màxim {0}!", + "numberNotEqual":"Aquest nombre ha de ser {0}!", + "numberStep":"Aquest nombre ha de ser un múltiple de {0}!", + "numberStepOffset":"Aquest nombre ha de ser un múltiple de {0} començant amb {1}!", + "numberStartsWith":"Aquest nombre ha de començar amb {0}!", + "numberEndsWith":"Aquest nombre ha d'acabar amb {0}!", + "numberContains":"Aquest nombre ha de contenir {0}!", + "numberChoices":"Aquest nombre només pot ser un dels valors següents: {0}!", + "numberFloat":"Aquest nombre no pot ser un decimal!", + "numberNegative":"Aquest nombre no pot ser negatiu!", + "numberPositive":"Aquest nombre no pot ser positiu!", + "numberZero":"Aquest nombre no pot ser zero!", + "numberNan":"Aquest número no pot ser NaN (Not A Number)!", + "numberInvertedContains":"Aquest número no pot contenir {0}!", + + "booleanTrue":"Aquest booleà no pot ser veritable!", + "booleanFalse":"Aquest booleà no pot ser fals!", + + "arrayEmptyDisabled":"Aquest array no pot estar buit!", + "arrayEmptyRequired":"Aquest array ha d'estar buit!", + "arrayTooShort":"Aquest array ha de tenir una longitud d'almenys {0}!", + "arrayTooLong":"Aquest array ha de tenir una longitud màxima de {0}!", + "arrayLengthInvalid":"Aquest array ha de tenir una longitud de {0}!", + "arrayInvalidTypes":"Aquest array només pot contenir els tipus següents: {0}!", + "arrayDouble":"Aquest array no permet el mateix valor dues vegades!", + + "discordInvalidId":"Aquest és un id de {0} de discord invàlid!", + "discordInvalidIdOptions":"Aquest és un id de {0} de discord invàlid! També en pots utilitzar un d'aquests: {1}!", + "discordInvalidToken":"Aquest és un token de discord invàlid (sintàcticament)!", + "colorInvalid":"Aquest és un color hex invàlid!", + "emojiTooShort":"Aquesta cadena ha de tenir almenys {0} emojis!", + "emojiTooLong":"Aquesta cadena ha de tenir com a màxim {0} emojis!", + "emojiCustom":"Aquest emoji no pot ser un emoji personalitzat de discord!", + "emojiInvalid":"Aquest és un emoji invàlid!", + "urlInvalid":"Aquesta url és invàlida!", + "urlInvalidHttp":"Aquesta url només pot utilitzar el protocol https://!", + "urlInvalidProtocol":"Aquesta url només pot utilitzar els protocols http:// i https://!", + "urlInvalidHostname":"Aquesta url té un nom de domini no permès!", + "urlInvalidExtension":"Aquesta url té una extensió invàlida! Tria entre: {0}!", + "urlInvalidPath":"Aquesta url té un camí invàlid!", + "idNotUnique":"Aquest id no és únic, utilitza un altre id!", + "idNonExistent":"L'id {0} no existeix!", + + "invalidType":"Aquesta propietat ha de ser del tipus: {0}!", + "propertyMissing":"La propietat {0} falta en aquest objecte!", + "propertyOptional":"La propietat {0} és opcional en aquest objecte!", + "objectDisabled":"Aquest objecte està desactivat, activa'l utilitzant {0}!", + "nullInvalid":"Aquesta propietat no pot ser nul·la!", + "switchInvalidType":"Això ha de ser d'un dels tipus següents: {0}!", + "objectSwitchInvalid":"Aquest objecte ha de ser d'un dels tipus següents: {0}!", + + "invalidLanguage":"Aquest és un idioma invàlid!", + "invalidButton":"Aquest botó ha de tenir almenys un {0} o {1}!", + "unusedOption":"L'opció {0} no s'utilitza enlloc!", + "unusedQuestion":"La pregunta {0} no s'utilitza enlloc!", + "dropdownOption":"Un panell amb desplegable activat només pot contenir opcions del tipus 'ticket'!", + "customInvalidVersion":"La versió especificada a la teva configuració no coincideix! Assegura't d'haver actualitzat la configuració a la versió més recent!" + } + }, + "actions":{ + "buttons":{ + "create":"Veure Ticket", + "close":"Tancar Ticket", + "delete":"Eliminar Ticket", + "reopen":"Reobrir Ticket", + "claim":"Reclamar Ticket", + "unclaim":"Alliberar Ticket", + "pin":"Fixar Ticket", + "unpin":"Desfixar Ticket", + "clear":"Eliminar Tickets", + "helpSwitchSlash":"Veure Comandes Slash", + "helpSwitchText":"Veure Comandes de Text", + "helpPage":"Pàgina {0}", + "withReason":"Amb Motiu", + "withoutTranscript":"Sense Transcripció" + }, + "titles":{ + "created":"Ticket Creat", + "close":"Ticket Tancat", + "delete":"Ticket Eliminat", + "reopen":"Ticket Reobert", + "claim":"Ticket Reclamat", + "unclaim":"Ticket Alliberat", + "pin":"Ticket Fixat", + "unpin":"Ticket Desfixat", + "rename":"Ticket Renomenat", + "move":"Ticket Mogut", + "add":"Usuari Afegit al Ticket", + "remove":"Usuari Eliminat del Ticket", + + "help":"Comandes Disponibles", + "statsReset":"Restablir Estadístiques", + "blacklistAdd":"Usuari a la Llista Negra", + "blacklistRemove":"Usuari Alliberat", + "blacklistGet":"Usuari a la Llista Negra", + "blacklistView":"Llista Negra Actual", + "blacklistAddDm":"Afegit a la Llista Negra", + "blacklistRemoveDm":"Eliminat de la Llista Negra", + "clear":"Tickets Netejats", + "clearTickets":"Netejar Tickets", + "roles":"Rols Actualitzats", + + "autoclose":"Ticket Tancat Automàticament", + "autocloseEnabled":"Tancament Automàtic Activat", + "autocloseDisabled":"Tancament Automàtic Desactivat", + "autodelete":"Ticket Eliminat Automàticament", + "autodeleteEnabled":"Eliminació Automàtica Activada", + "autodeleteDisabled":"Eliminació Automàtica Desactivada", + + "topicSet":"Tema Canviat", + "prioritySet":"Prioritat Canviada", + "priorityGet":"Prioritat del Ticket", + "transfer":"Ticket Transferit" + }, + "descriptions":{ + "create":"El teu ticket ha estat creat. Fes clic al botó a continuació per accedir-hi!", + "close":"El ticket ha estat tancat amb èxit!", + "delete":"El ticket ha estat eliminat amb èxit!", + "reopen":"El ticket ha estat reobert amb èxit!", + "claim":"El ticket ha estat reclamat amb èxit!", + "unclaim":"El ticket ha estat alliberat amb èxit!", + "pin":"El ticket ha estat fixat amb èxit!", + "unpin":"El ticket ha estat desfixat amb èxit!", + "rename":"El ticket ha estat renomenat a {0} amb èxit!", + "move":"El ticket ha estat mogut a {0} amb èxit!", + "add":"{0} ha estat afegit al ticket amb èxit!", + "remove":"{0} ha estat eliminat del ticket amb èxit!", + + "helpExplanation":"`` => paràmetre requerit\n`[nom]` => paràmetre opcional", + "statsReset":"Les estadístiques del bot s'han restablert correctament!", + "statsError":"No es poden veure les estadístiques del ticket!\n{0} no és un ticket!", + "blacklistAdd":"{0} ha estat afegit a la llista negra amb èxit!", + "blacklistRemove":"{0} ha estat alliberat amb èxit!", + "blacklistGetSuccess":"{0} està actualment a la llista negra!", + "blacklistGetEmpty":"{0} actualment no està a la llista negra!", + "blacklistViewEmpty":"Encara no hi ha ningú a la llista negra!", + "blacklistViewTip":"Utilitza \"/blacklist add\" per afegir un usuari a la llista negra!", + "clearVerify":"Estàs segur que vols eliminar múltiples tickets?\nAquesta acció no es pot desfer!", + "clearReady":"{0} tickets han estat eliminats amb èxit!", + "rolesEmpty":"No s'han actualitzat rols!", + + "autocloseLeave":"Aquest ticket ha estat tancat automàticament perquè el creador ha abandonat el servidor!", + "autocloseTimeout":"Aquest ticket ha estat tancat automàticament perquè ha estat inactiu durant més de `{0}h`!", + "autodeleteLeave":"Aquest ticket ha estat eliminat automàticament perquè el creador ha abandonat el servidor!", + "autodeleteTimeout":"Aquest ticket ha estat eliminat automàticament perquè ha estat inactiu durant més de `{0} dies`!", + "autocloseEnabled":"El tancament automàtic ha estat activat en aquest ticket!\nEs tancarà quan estigui inactiu durant més de `{0}h`!", + "autocloseDisabled":"El tancament automàtic ha estat desactivat en aquest ticket!\nJa no es tancarà automàticament!", + "autodeleteEnabled":"L'eliminació automàtica ha estat activada en aquest ticket!\nS'eliminarà quan estigui inactiu durant més de `{0} dies`!", + "autodeleteDisabled":"L'eliminació automàtica ha estat desactivada en aquest ticket!\nJa no s'eliminarà automàticament!", + + "ticketMessageLimit":"Només pots crear {0} ticket(s) al mateix temps!", + "ticketMessageAutoclose":"Aquest ticket es tancarà automàticament quan estigui inactiu durant {0}h!", + "ticketMessageAutodelete":"Aquest ticket es eliminarà automàticament quan estigui inactiu durant {0} dies!", + "panelReady":"El panell està disponible en el missatge de seguiment!\nAquest missatge es pot eliminar ara!", + + "topicSet":"El tema del canal ha estat canviat per {0} amb èxit!", + "prioritySet":"La prioritat del ticket ha estat canviada a {0} per {1} amb èxit!", + "priorityGet":"La prioritat actual d'aquest ticket és {0}.", + "transfer":"La propietat del ticket ha estat transferida de {0} a {1} per {2} amb èxit!" + }, + "modal":{ + "closePlaceholder":"Per què has tancat aquest ticket?", + "deletePlaceholder":"Per què has eliminat aquest ticket?", + "reopenPlaceholder":"Per què has reobert aquest ticket?", + "claimPlaceholder":"Per què has reclamat aquest ticket?", + "unclaimPlaceholder":"Per què has alliberat aquest ticket?", + "pinPlaceholder":"Per què has fixat aquest ticket?", + "unpinPlaceholder":"Per què has desfixat aquest ticket?" + }, + "logs":{ + "createLog":"Un nou ticket ha estat creat per {0}!", + "closeLog":"Aquest ticket ha estat tancat per {0}!", + "closeDm":"El teu ticket ha estat tancat al nostre servidor!", + "deleteLog":"Aquest ticket ha estat eliminat per {0}!", + "deleteDm":"El teu ticket ha estat eliminat al nostre servidor!", + "reopenLog":"Aquest ticket ha estat reobert per {0}!", + "reopenDm":"El teu ticket ha estat reobert al nostre servidor!", + "claimLog":"Aquest ticket ha estat reclamat per {0}!", + "claimDm":"El teu ticket ha estat reclamat al nostre servidor!", + "unclaimLog":"Aquest ticket ha estat alliberat per {0}!", + "unclaimDm":"El teu ticket ha estat alliberat al nostre servidor!", + "pinLog":"Aquest ticket ha estat fixat per {0}!", + "pinDm":"El teu ticket ha estat fixat al nostre servidor!", + "unpinLog":"Aquest ticket ha estat desfixat per {0}!", + "unpinDm":"El teu ticket ha estat desfixat al nostre servidor!", + "renameLog":"Aquest ticket ha estat renombrat a {0} per {1}!", + "renameDm":"El teu ticket ha estat renombrat a {0} al nostre servidor!", + "moveLog":"Aquest ticket ha estat mogut a {0} per {1}!", + "moveDm":"El teu ticket ha estat mogut a {0} al nostre servidor!", + "addLog":"{0} ha estat afegit a aquest ticket per {1}!", + "addDm":"{0} ha estat afegit al teu ticket al nostre servidor!", + "removeLog":"{0} ha estat eliminat d'aquest ticket per {1}!", + "removeDm":"{0} ha estat eliminat del teu ticket al nostre servidor!", + + "blacklistAddLog":"{0} ha estat afegit a la llista negra per {1}!", + "blacklistRemoveLog":"{0} ha estat eliminat de la llista negra per {1}!", + "blacklistAddDm":"Has estat afegit a la llista negra al nostre servidor!\nA partir d'ara, no pots crear cap ticket!", + "blacklistRemoveDm":"Has estat eliminat de la llista negra al nostre servidor!\nAra pots crear tickets de nou!", + "clearLog":"{0} tickets han estat eliminats per {1}!", + + "transferLog":"La propietat d'aquest ticket ha estat transferida de {0} a {1} per {2}!", + "transferDm":"La propietat del teu ticket ha estat transferida de {0} a {1} al nostre servidor!", + "prioritySetLog":"La prioritat d'aquest ticket ha estat canviada a {0} per {1}!", + "prioritySetDm":"La prioritat del teu ticket ha estat canviada a {0} al nostre servidor!", + "roleUpdateLog":"{0} ha actualitzat els seus rols!", + "roleUpdateDm":"Els teus rols al nostre servidor han estat actualitzats!" + } + }, + "transcripts":{ + "success":{ + "visit":"Veure Transcripció", + "ready":"Transcripció Creada", + "textFileDescription":"Aquesta és la transcripció de text d'un ticket eliminat!", + "htmlProgress":"Si us plau, espera mentre aquesta transcripció html es processa...", + + "createdChannel":"S'ha creat una nova transcripció tipus {0} al servidor!", + "createdCreator":"S'ha creat una nova transcripció tipus {0} per un dels teus tickets!", + "createdParticipant":"S'ha creat una nova transcripció tipus {0} en un dels tickets on has participat!", + "createdActiveAdmin":"S'ha creat una nova transcripció tipus {0} en un dels tickets on has participat com a administrador!", + "createdEveryAdmin":"S'ha creat una nova transcripció tipus {0} en un dels tickets on eres administrador!", + "createdOther":"S'ha creat una nova transcripció tipus {0}!" + }, + "errors":{ + "retry":"Torna-ho a intentar", + "continue":"Elimina Sense Transcripció", + "backup":"Crea Transcripció de Backup", + "error":"Alguna cosa ha anat malament mentre intentàvem crear la transcripció.\nQuè t'agradaria fer?\n\nAquest ticket no s'eliminarà fins que facis clic en un d'aquests botons.", + "title":"Error de transcripció" + }, + "text":{ + "messagesTitle":"MISSATGES", + "embedTitle":"EMBED", + "fileTitle":"FITXER", + "fieldsTitle":"CAMPS", + "reactionsTitle":"REACCIONS", + "statsTitle":"ESTADÍSTIQUES", + "emptyContent":"", + "noTitle":"", + "noDesc":"" + } + }, + "errors":{ + "titles":{ + "internalError":"Error Intern", + "optionMissing":"Opció de Comanda Absent", + "optionInvalid":"Opció de Comanda Invàlida", + "unknownCommand":"Comanda Desconeguda", + "noPermissions":"No Tens Permisos", + "unknownTicket":"Ticket Desconegut", + "deprecatedTicket":"Ticket Obsolet", + "unknownOption":"Opció Desconeguda", + "unknownPanel":"Panell Desconegut", + "notInGuild":"No Ets Al Servidor", + "channelRename":"No Es Pot Canviar El Nom Del Canal", + "busy":"El Ticket Està Ocupat", + "permissionError":"Error de Permisos" + }, + "descriptions":{ + "askForInfo":"Contacta amb el propietari d'aquest bot per obtenir més informació!", + "askForInfoResolve":"Contacta amb el propietari del bot si aquest problema no es resol després de diversos intents.", + "internalError":"No s'ha pogut respondre a aquest {0} a causa d'un error intern!", + "optionMissing":"Falta un paràmetre requerit en aquesta comanda!", + "optionInvalid":"Un paràmetre d'aquesta comanda és invàlid!", + "optionInvalidChoose":"Tria entre", + "unknownCommand":"Prova de visitar el menú d'ajuda per obtenir més informació!", + "noPermissions":"No tens permís per utilitzar aquest {0}!", + "noPermissionsList":"Permisos Requerits: (un d'ells)", + "noPermissionsCooldown":"No tens permís per utilitzar aquest {0} perquè tens un temps de refredament!", + "noPermissionsBlacklist":"No tens permís per utilitzar aquest {0} perquè estàs a la llista negra!", + "noPermissionsLimitGlobal":"No tens permís per crear un ticket perquè el servidor ha arribat al límit màxim de tickets!", + "noPermissionsLimitGlobalUser":"No tens permís per crear un ticket perquè has arribat al límit màxim de tickets!", + "noPermissionsLimitOption":"No tens permís per crear un ticket perquè el servidor ha arribat al límit màxim de tickets per aquesta opció!", + "noPermissionsLimitOptionUser":"No tens permís per crear un ticket perquè has arribat al límit màxim de tickets per aquesta opció!", + "unknownTicket":"Torna a intentar aquesta comanda en un ticket vàlid!", + "deprecatedTicket":"El canal actual no és un ticket vàlid! Pot ser que fos un ticket d'una versió antiga de Open Ticket!", + "notInGuild":"Aquest {0} no funciona en DM! Si us plau, torna a intentar-ho en un servidor!", + "channelRename":"Degut a les limitacions de discord, actualment és impossible per al bot canviar el nom del canal. El canal es canviarà automàticament en 10 minuts si el bot no es reinicia.", + "channelRenameSource":"La font d'aquest error és: {0}", + "busy":"No es pot utilitzar aquest {0}!\nEl ticket està sent processat pel bot.\n\nSi us plau, torna a intentar-ho en uns segons!", + "closeBeforeMessage":"Aquest ticket no pot ser tancat/eliminat abans que un usuari hagi enviat un missatge.", + "closeBeforeAdminMessage":"Aquest ticket no pot ser tancat/eliminat abans que un administrador o membre de suport hagi enviat un missatge.", + "unableToCreateTicket":"No pots crear cap ticket." + }, + "optionInvalidReasons":{ + "stringRegex":"El valor no coincideix amb el patró!", + "stringMinLength":"El valor ha de tenir almenys {0} caràcters!", + "stringMaxLength":"El valor ha de tenir com a màxim {0} caràcters!", + "numberInvalid":"Nombre invàlid!", + "numberMin":"El nombre ha de ser almenys {0}!", + "numberMax":"El nombre ha de ser com a màxim {0}!", + "numberDecimal":"El nombre no pot ser decimal!", + "numberNegative":"El nombre no pot ser negatiu!", + "numberPositive":"El nombre no pot ser positiu!", + "numberZero":"El nombre no pot ser zero!", + "channelNotFound":"No es pot trobar el canal!", + "userNotFound":"No es pot trobar l'usuari!", + "roleNotFound":"No es pot trobar el rol!", + "memberNotFound":"No es pot trobar l'usuari!", + "mentionableNotFound":"No es pot trobar l'usuari o el rol!", + "channelType":"Tipus de canal invàlid!", + "notInGuild":"Aquesta opció requereix que siguis en un servidor!" + }, + "permissions":{ + "developer":"Has de ser el desenvolupador del bot.", + "owner":"Has de ser el propietari del servidor.", + "admin":"Has de ser un administrador del servidor.", + "moderator":"Has de ser un moderador.", + "support":"Has de formar part de l'equip de suport.", + "member":"Has de ser un membre.", + "discord-administrator":"Has de tenir el permís `ADMINISTRADOR`." + }, + "actionInvalid":{ + "close":"El ticket ja està tancat!", + "reopen":"El ticket encara no està tancat!", + "claim":"El ticket ja està reclamat!", + "unclaim":"El ticket encara no està reclamat!", + "pin":"El ticket ja està fixat!", + "unpin":"El ticket encara no està fixat!", + "add":"Aquest usuari ja té accés al ticket!", + "remove":"No es pot eliminar aquest usuari del ticket!" + } + }, + "params":{ + "uppercase":{ + "ticket":"Ticket", + "tickets":"Tickets", + "reason":"Motiu", + "creator":"Creador", + "remaining":"Temps Restant", + "added":"Afegit", + "removed":"Eliminat", + "filter":"Filtre", + "method":"Mètode", + "type":"Tipus", + "blacklisted":"Llista Negra", + "panel":"Panell", + "command":"Comanda", + "system":"Sistema", + "true":"Cert", + "false":"Fals", + "syntax":"Sintaxi", + "originalName":"Nom Original", + "newName":"Nom Nou", + "until":"Fins a", + "validOptions":"Opcions Vàlides", + "validPanels":"Panells Vàlids", + "autoclose":"Tancament Automàtic", + "autodelete":"Eliminació Automàtica", + "startupDate":"Data d'Inici", + "version":"Versió", + "name":"Nom", + "role":"Rol", + "status":"Estat", + "claimed":"Reclamat", + "pinned":"Fixat", + "creationDate":"Data de Creació", + + "noone":"Ningú", + "open":"Obert", + "closed":"Tancat", + "priority":"Prioritat", + "participants":"Participants", + "yes":"Si", + "no":"No", + "option":"Opció", + "topic":"Tema", + "uptime":"Temps d'Activitat del Sistema", + "messages":"Missatges", + "embeds":"Embeds", + "files":"Fitxers", + "components":"Components", + "cooldown":"Refredament", + "maxTickets":"Max Tickets", + "admins":"Admins", + "roles":"Rols", + "size":"Mida" + }, + "lowercase":{ + "text":"text", + "html":"html", + "command":"comanda", + "modal":"modal", + "button":"botó", + "dropdown":"desplegable", + "method":"mètode" + } + }, + "commands":{ + "reason":"Especifica una raó opcional que serà visible als registres.", + "help":"Obtingues una llista de totes les comandes disponibles.", + "panel":"Genera un missatge amb un desplegable o botons (per a la creació de tickets).", + "panelId":"L'identificador del panell que vols generar.", + "panelAutoUpdate":"Vols que aquest panell s'actualitzi automàticament quan s'edita?", + "ticket":"Crea instantàniament un ticket.", + "ticketId":"L'identificador del ticket que vols crear.", + "close":"Tanca un ticket.", + "delete":"Elimina un ticket.", + "deleteNoTranscript":"Elimina aquest ticket sense crear una transcripció.", + "reopen":"Reobre un ticket.", + "claim":"Reclama un ticket.", + "claimUser":"Reclama aquest ticket per a una altra persona en lloc de tu mateix.", + "unclaim":"Allibera un ticket.", + "pin":"Fixa un ticket.", + "unpin":"Desfixa un ticket.", + + "move":"Mou un ticket.", + "moveId":"L'identificador de l'opció a la qual vols moure't.", + "rename":"Renomena un ticket.", + "renameName":"El nou nom per aquest ticket.", + "add":"Afegeix un usuari a un ticket.", + "addUser":"L'usuari a afegir.", + "remove":"Elimina un usuari d'un ticket.", + "removeUser":"L'usuari a eliminar.", + + "blacklist":"Gestiona la llista negra de tickets.", + "blacklistView":"Veure una llista de l'actual llista negra.", + "blacklistAdd":"Afegeix un usuari a la llista negra.", + "blacklistRemove":"Elimina un usuari de la llista negra.", + "blacklistGet":"Obtingues els detalls d'un usuari a la llista negra.", + "blacklistGetUser":"L'usuari dels qual vols obtenir detalls.", + "stats":"Veure estadístiques del bot, d'un membre o d'un ticket.", + "statsReset":"Restablir totes les estadístiques del bot (i començar a comptar des de zero).", + "statsGlobal":"Veure les estadístiques globals.", + "statsUser":"Veure les estadístiques d'un usuari al servidor.", + "statsUserUser":"L'usuari del qual vols veure les estadístiques.", + "statsTicket":"Veure les estadístiques d'un ticket al servidor.", + "statsTicketTicket":"El ticket que vols veure.", + + "clear":"Elimina múltiples tickets al mateix temps.", + "clearFilter":"El filtre per a la neteja de tickets.", + "clearFilters":{ + "all":"Tots", + "open":"Oberts", + "close":"Tancats", + "claim":"Reclamats", + "unclaim":"Alliberats", + "pin":"Fixats", + "unpin":"Desfixats", + "autoclose":"Tancat Automàticaments" + }, + + "autoclose":"Gestiona el tancament automàtic en un ticket.", + "autocloseDisable":"Desactiva el tancament automàtic en aquest ticket.", + "autocloseEnable":"Activa el tancament automàtic en aquest ticket.", + "autocloseEnableTime":"La quantitat d'hores que aquest ticket ha d'estar inactiu per tancar-lo.", + "autodelete":"Gestiona l'eliminació automàtica en un ticket.", + "autodeleteDisable":"Desactiva l'eliminació automàtica en aquest ticket.", + "autodeleteEnable":"Activa l'eliminació automàtica en aquest ticket.", + "autodeleteEnableTime":"La quantitat de dies que aquest ticket ha d'estar inactiu per eliminar-lo.", + + "topic":"Gestiona el tema del canal del ticket.", + "topicSet":"Estableix el tema del canal del ticket.", + "topicValue":"El nou tema del canal.", + "topicList":"Obté una llista de tots els tickets amb el seu tema i estadístiques.", + "priority":"Gestiona la prioritat del ticket.", + "prioritySet":"Estableix la prioritat del ticket.", + "priorityValue":"La prioritat del canal.", + "priorityGet":"Obtenir la prioritat del ticket.", + "priorityList":"Obté una llista de tots els tickets amb el seu estat de prioritat.", + "transfer":"Transfereix la propietat del ticket d'un usuari a un altre.", + "transferUser":"L'usuari al qual transferir." + }, + "helpMenu":{ + "help":"Obtingues una llista de totes les comandes disponibles.", + "ticket":"Crea instantàniament un ticket.", + "close":"Tanca un ticket, això desactiva l'escriptura en aquest canal.", + "delete":"Elimina un ticket, això crea una transcripció quan està activat.", + "reopen":"Reobre un ticket, això permet escriure de nou en aquest canal.", + "pin":"Fixar un ticket. Això mou el ticket a la part superior i afegeix un emoji '📌' al nom.", + "unpin":"Desfixar un ticket. El ticket romandrà a la seva posició però perdrà l'emoji '📌'.", + "move":"Mou un ticket. Això canvia el tipus d'aquest ticket.", + "rename":"Renomena un ticket. Això canvia el nom del canal d'aquest ticket.", + "claim":"Reclama un ticket. Amb això, pots fer saber al teu equip que estàs gestionant aquest ticket.", + "unclaim":"Allibera un ticket. Amb això, pots fer saber al teu equip que aquest ticket està lliure de nou.", + "add":"Afegeix un usuari a un ticket. Això permetrà a l'usuari llegir i escriure en aquest ticket.", + "remove":"Elimina un usuari d'un ticket. Això eliminarà la capacitat de llegir i escriure per a un usuari en aquest ticket.", + "panel":"Genera un missatge amb un desplegable o botons (per a la creació de tickets).", + "blacklistView":"Veure una la llista negra actual.", + "blacklistAdd":"Afegeix un usuari a la llista negra.", + "blacklistRemove":"Elimina un usuari de la llista negra.", + "blacklistGet":"Obtingues els detalls d'un usuari de la llista negra.", + "statsGlobal":"Veure les estadístiques globals.", + "statsTicket":"Veure les estadístiques d'un ticket al servidor.", + "statsUser":"Veure les estadístiques d'un usuari al servidor.", + "statsReset":"Restablir totes les estadístiques del bot (i començar a comptar des de zero).", + "autocloseDisable":"Desactiva el tancament automàtic en aquest ticket.", + "autocloseEnable":"Activa el tancament automàtic en aquest ticket.", + "autodeleteDisable":"Desactiva l'eliminació automàtica en aquest ticket.", + "autodeleteEnable":"Activa l'eliminació automàtica en aquest ticket.", + "categories":{ + "general":"Comandes Generals", + "basicTicket":"Comandes Bàsiques de Tickets", + "advancedTicket":"Comandes Avançades de Tickets", + "userTicket":"Comandes de Tickets d'Usuari", + "admin":"Comandes d'Admin", + "advanced":"Comandes Avançades", + "extra":"Comandes Extra" + } + }, + "stats":{ + "scopes":{ + "global":"Estadístiques Globals", + "system":"Estadístiques del Sistema", + "user":"Estadístiques d'Usuari", + "ticket":"Estadístiques de Ticket", + "participants":"Participants", + "messages":"Missatges" + }, + "properties":{ + "ticketsCreated":"Tickets Creats", + "ticketsClosed":"Tickets Tancats", + "ticketsDeleted":"Tickets Eliminats", + "ticketsReopened":"Tickets Reoberts", + "ticketsAutoclosed":"Tickets Tancats Automàticament", + "ticketsClaimed":"Tickets Reclamats", + "ticketsPinned":"Tickets Fixats", + "ticketsMoved":"Tickets Moguts", + "usersBlacklisted":"Usuaris a la Llista Negra", + "transcriptsCreated":"Transcripcions Creades", + "ticketsAutodeleted":"Tickets Eliminats Automàticament", + "ticketsTransferred":"Tickets Transferits", + "ticketVolume":"Volum de Tickets", + "averageTickets":"Mitjana de Tickets/Usuari", + "currentTickets":"Tickets Actuals", + "age":"Edat del Ticket", + "responseTime":"Temps de Resposta", + "resolutionTime":"Temps de Resolució", + "createdOn":"Creat el", + "createdBy":"Creat per", + "closedOn":"Tancat el", + "closedBy":"Tancat per", + "claimedOn":"Reclamat el", + "claimedBy":"Reclamat per", + "pinnedOn":"Fixat el", + "pinnedBy":"Fixat per", + "deletedOn":"Eliminat el", + "deletedBy":"Eliminat per" + }, + "roles":{ + "developer":"Desenvolupador", + "serverOwner":"Propietari del Servidor", + "serverAdmin":"Admin del Servidor", + "moderator":"Equip de Moderació", + "support":"Equip de Suport", + "member":"Membre" + } + }, + "panel":{ + "selectTicket":"Selecciona el teu ticket", + "selectRole":"Selecciona el teu rol", + "selectOption":"Selecciona la teva opció" + }, + "priorities":{ + "urgent":"Urgent", + "veryHigh":"Molt Alt", + "high":"Alt", + "normal":"Normal", + "low":"Baix", + "veryLow":"Molt Baix", + "none":"Cap" + } } \ No newline at end of file diff --git a/languages/custom.json b/languages/custom.json index 60ed194..52cd1f4 100644 --- a/languages/custom.json +++ b/languages/custom.json @@ -2,7 +2,7 @@ "_TRANSLATION":{ "otversion":"v4.1.0", "translators":["DJj123dj"], - "lastedited":"21/08/2024", + "lastedited":"09/11/2025", "language":"Custom", "automated":false }, @@ -31,7 +31,15 @@ "stringContains":"This string needs to contain {0}!", "stringChoices":"This string can only be one of the following values: {0}!", "stringRegex":"This string is invalid!", - + "stringInvertedContains":"This string is not allowed to contain {0}!", + "stringLowercase":"This string must be written in lowercase only!", + "stringUppercase":"This string must be written in uppercase only!", + "stringSpecialCharacters":"This string is not allowed to contain any special characters! (a-z, 0-9 & space only)", + "stringNoSpaces":"This string is not allowed to contain spaces!", + "stringCapitalWord":"It's recommended that each word in this string starts with a capital letter!", + "stringCapitalSentence":"It looks like some sentences in this string don't start with a capital letter!", + "stringPunctuation":"It looks like the sentence in this string doesn't end with a punctuation mark!", + "numberTooShort":"This number can't be shorter than {0} characters!", "numberTooLong":"This number can't be longer than {0} characters!", "numberLengthInvalid":"This number needs to be {0} characters long!", @@ -48,10 +56,12 @@ "numberNegative":"This number can't be negative!", "numberPositive":"This number can't be positive!", "numberZero":"This number can't be zero!", - + "numberNan":"This number can't be NaN (Not A Number)!", + "numberInvertedContains":"This number is not allowed to contain {0}!", + "booleanTrue":"This boolean can't be true!", "booleanFalse":"This boolean can't be false!", - + "arrayEmptyDisabled":"This array isn't allowed to be empty!", "arrayEmptyRequired":"This array is required to be empty!", "arrayTooShort":"This array needs to have a length of at least {0}!", @@ -59,7 +69,7 @@ "arrayLengthInvalid":"This array needs to have a length of {0}!", "arrayInvalidTypes":"This array can only contain the following types: {0}!", "arrayDouble":"This array doesn't allow the same value twice!", - + "discordInvalidId":"This is an invalid discord {0} id!", "discordInvalidIdOptions":"This is an invalid discord {0} id! You can also use one of these: {1}!", "discordInvalidToken":"This is an invalid discord token (syntactically)!", @@ -76,7 +86,7 @@ "urlInvalidPath":"This url has an invalid path!", "idNotUnique":"This id isn't unique, use another id instead!", "idNonExistent":"The id {0} doesn't exist!", - + "invalidType":"This property needs to be the type: {0}!", "propertyMissing":"The property {0} is missing from this object!", "propertyOptional":"The property {0} is optional in this object!", @@ -84,12 +94,13 @@ "nullInvalid":"This property can't be null!", "switchInvalidType":"This needs to be one of the following types: {0}!", "objectSwitchInvalid":"This object needs to be one of the following types: {0}!", - + "invalidLanguage":"This is an invalid language!", "invalidButton":"This button needs to have at least an {0} or {1}!", "unusedOption":"The option {0} isn't used anywhere!", "unusedQuestion":"The question {0} isn't used anywhere!", - "dropdownOption":"A panel with dropdown enabled can only contain options of the 'ticket' type!" + "dropdownOption":"A panel with dropdown enabled can only contain options of the 'ticket' type!", + "customInvalidVersion":"The version specified in your config does not match! Make sure you have updated the config to the latest version!" } }, "actions":{ @@ -132,6 +143,7 @@ "blacklistAddDm":"Added To Blacklist", "blacklistRemoveDm":"Removed From Blacklist", "clear":"Tickets Cleared", + "clearTickets":"Clear Tickets", "roles":"Roles Updated", "autoclose":"Ticket Autoclosed", @@ -139,7 +151,12 @@ "autocloseDisabled":"Autoclose Disabled", "autodelete":"Ticket Autodeleted", "autodeleteEnabled":"Autodelete Enabled", - "autodeleteDisabled":"Autodelete Disabled" + "autodeleteDisabled":"Autodelete Disabled", + + "topicSet":"Topic Changed", + "prioritySet":"Priority Changed", + "priorityGet":"Ticket Priority", + "transfer":"Ticket Transferred" }, "descriptions":{ "create":"Your ticket has been created. Click the button below to access it!", @@ -154,7 +171,7 @@ "move":"The ticket has been moved to {0} successfully!", "add":"{0} has been added to the ticket successfully!", "remove":"{0} has been removed from the ticket successfully!", - + "helpExplanation":"`` => required parameter\n`[name]` => optional parameter", "statsReset":"The bot stats have been reset successfully!", "statsError":"Unable to view ticket stats!\n{0} is not a ticket!", @@ -167,7 +184,7 @@ "clearVerify":"Are you sure you want to delete multiple tickets?\nThis action can't be undone!", "clearReady":"{0} tickets have been deleted successfully!", "rolesEmpty":"No roles have been updated!", - + "autocloseLeave":"This ticket has been autoclosed because the creator left the server!", "autocloseTimeout":"This ticket has been autoclosed because it has been inactive for more than `{0}h`!", "autodeleteLeave":"This ticket has been autodeleted because the creator left the server!", @@ -176,11 +193,16 @@ "autocloseDisabled":"Autoclose has been disabled in this ticket!\nIt won't be closed automatically anymore!", "autodeleteEnabled":"Autodelete has been enabled in this ticket!\nIt will be deleted when it is inactive for more than `{0} days`!", "autodeleteDisabled":"Autodelete has been disabled in this ticket!\nIt won't be deleted automatically anymore!", - + "ticketMessageLimit":"You can only create {0} ticket(s) at the same time!", "ticketMessageAutoclose":"This ticket will be autoclosed when inactive for {0}h!", "ticketMessageAutodelete":"This ticket will be autodeleted when inactive for {0} days!", - "panelReady":"You can find the panel below!\nThis message can now be deleted!" + "panelReady":"The panel is available in the followup message!\nThis message can now be deleted!", + + "topicSet":"The channel topic has been changed by {0} successfully!", + "prioritySet":"The ticket priority has been changed to {0} by {1} successfully!", + "priorityGet":"The current priority of this ticket is {0}.", + "transfer":"The ticket ownership has been transferred from {0} to {1} by {2} successfully!" }, "modal":{ "closePlaceholder":"Why did you close this ticket?", @@ -215,12 +237,19 @@ "addDm":"{0} has been added to your ticket in our server!", "removeLog":"{0} has been removed from this ticket by {1}!", "removeDm":"{0} has been removed from your ticket in our server!", - + "blacklistAddLog":"{0} was blacklisted by {1}!", "blacklistRemoveLog":"{0} was removed from the blacklist by {1}!", "blacklistAddDm":"You have been blacklisted in our server!\nFrom now on, you are unable to create a ticket!", "blacklistRemoveDm":"You have been removed from the blacklist in our server!\nNow you can create tickets again!", - "clearLog":"{0} tickets have been deleted by {1}!" + "clearLog":"{0} tickets have been deleted by {1}!", + + "transferLog":"The ownership of this ticket has been transferred from {0} to {1} by {2}!", + "transferDm":"The ownership of your ticket has been transferred from {0} to {1} in our server!", + "prioritySetLog":"The priority of this ticket has been changed to {0} by {1}!", + "prioritySetDm":"The priority of your ticket has been changed to {0} in our server!", + "roleUpdateLog":"{0} has updated their roles!", + "roleUpdateDm":"Your roles in our server have been updated!" } }, "transcripts":{ @@ -229,7 +258,7 @@ "ready":"Transcript Created", "textFileDescription":"This is the text transcript of a deleted ticket!", "htmlProgress":"Please wait while this html transcript is getting processed...", - + "createdChannel":"A new {0} transcript has been created in the server!", "createdCreator":"A new {0} transcript has been created for one of your tickets!", "createdParticipant":"A new {0} transcript has been created in one of the tickets you participated in!", @@ -241,7 +270,19 @@ "retry":"Retry", "continue":"Delete Without Transcript", "backup":"Create Backup Transcript", - "error":"Something went wrong while trying to create the transcript.\nWhat would you like to do?\n\nThis ticket won't be deleted until you click one of these buttons." + "error":"Something went wrong while trying to create the transcript.\nWhat would you like to do?\n\nThis ticket won't be deleted until you click one of these buttons.", + "title":"Transcript Error" + }, + "text":{ + "messagesTitle":"MESSAGES", + "embedTitle":"EMBED", + "fileTitle":"FILE", + "fieldsTitle":"FIELDS", + "reactionsTitle":"REACTIONS", + "statsTitle":"STATS", + "emptyContent":"", + "noTitle":"", + "noDesc":"" } }, "errors":{ @@ -257,7 +298,8 @@ "unknownPanel":"Unknown Panel", "notInGuild":"Not In Server", "channelRename":"Unable To Rename Channel", - "busy":"Ticket Is Busy" + "busy":"Ticket Is Busy", + "permissionError":"Permission Error" }, "descriptions":{ "askForInfo":"Contact the owner of this bot for more info!", @@ -280,7 +322,10 @@ "notInGuild":"This {0} doesn't work in DM! Please try it again in a server!", "channelRename":"Due to discord ratelimits, it's currently impossible for the bot to rename the channel. The channel will automatically be renamed over 10 minutes if the bot isn't rebooted.", "channelRenameSource":"The source of this error is: {0}", - "busy":"Unable to use this {0}!\nThe ticket is currently being processed by the bot.\n\nPlease try again in a few seconds!" + "busy":"Unable to use this {0}!\nThe ticket is currently being processed by the bot.\n\nPlease try again in a few seconds!", + "closeBeforeMessage":"This ticket cannot be closed/deleted before a message has been sent by a user.", + "closeBeforeAdminMessage":"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.", + "unableToCreateTicket":"You are unable to create a ticket." }, "optionInvalidReasons":{ "stringRegex":"Value doesn't match pattern!", @@ -331,7 +376,6 @@ "added":"Added", "removed":"Removed", "filter":"Filter", - "claimedBy":"Claimed By {0}", "method":"Method", "type":"Type", "blacklisted":"Blacklisted", @@ -355,7 +399,27 @@ "status":"Status", "claimed":"Claimed", "pinned":"Pinned", - "creationDate":"Creation Date" + "creationDate":"Creation Date", + + "noone":"No-One", + "open":"Open", + "closed":"Closed", + "priority":"Priority", + "participants":"Participants", + "yes":"Yes", + "no":"No", + "option":"Option", + "topic":"Topic", + "uptime":"System Uptime", + "messages":"Messages", + "embeds":"Embeds", + "files":"Files", + "components":"Components", + "cooldown":"Cooldown", + "maxTickets":"Max Tickets", + "admins":"Admins", + "roles":"Roles", + "size":"Size" }, "lowercase":{ "text":"text", @@ -384,6 +448,7 @@ "unclaim":"Unclaim a ticket.", "pin":"Pin a ticket.", "unpin":"Unpin a ticket.", + "move":"Move a ticket.", "moveId":"The identifier of the option that you want to move to.", "rename":"Rename a ticket.", @@ -392,6 +457,7 @@ "addUser":"The user to add.", "remove":"Remove a user from a ticket.", "removeUser":"The user to remove.", + "blacklist":"Manage the ticket blacklist.", "blacklistView":"View a list of the current blacklist.", "blacklistAdd":"Add a user to the blacklist.", @@ -405,6 +471,7 @@ "statsUserUser":"The user to view.", "statsTicket":"View the stats of a ticket in the server.", "statsTicketTicket":"The ticket to view.", + "clear":"Delete multiple tickets at the same time.", "clearFilter":"The filter for clearing tickets.", "clearFilters":{ @@ -417,6 +484,7 @@ "unpin":"Unpinned", "autoclose":"Autoclosed" }, + "autoclose":"Manage autoclose in a ticket.", "autocloseDisable":"Disable autoclose in this ticket.", "autocloseEnable":"Enable autoclose in this ticket.", @@ -424,7 +492,19 @@ "autodelete":"Manage autodelete in a ticket.", "autodeleteDisable":"Disable autodelete in this ticket.", "autodeleteEnable":"Enable autodelete in this ticket.", - "autodeleteEnableTime":"The amount of days this ticket needs to be inactive to delete it." + "autodeleteEnableTime":"The amount of days this ticket needs to be inactive to delete it.", + + "topic":"Manage the topic of the ticket channel.", + "topicSet":"Set the topic of the ticket channel.", + "topicValue":"The new topic of the channel.", + "topicList":"Get a list of all tickets with their topic and stats.", + "priority":"Manage the priority of the ticket.", + "prioritySet":"Set the priority of the ticket.", + "priorityValue":"The priority of the channel.", + "priorityGet":"Get the priority of the ticket.", + "priorityList":"Get a list of all tickets with their priority status.", + "transfer":"Transfer the ticket ownership from one user to another.", + "transferUser":"The user to transfer to." }, "helpMenu":{ "help":"Get a list of all the available commands.", @@ -452,7 +532,16 @@ "autocloseDisable":"Disable autoclose in this ticket.", "autocloseEnable":"Enable autoclose in this ticket.", "autodeleteDisable":"Disable autodelete in this ticket.", - "autodeleteEnable":"Enable autodelete in this ticket." + "autodeleteEnable":"Enable autodelete in this ticket.", + "categories":{ + "general":"General Commands", + "basicTicket":"Basic Ticket Commands", + "advancedTicket":"Advanced Ticket Commands", + "userTicket":"User Ticket Commands", + "admin":"Admin Commands", + "advanced":"Advanced Commands", + "extra":"Extra Commands" + } }, "stats":{ "scopes":{ @@ -460,7 +549,8 @@ "system":"System Stats", "user":"User Stats", "ticket":"Ticket Stats", - "participants":"Participants" + "participants":"Participants", + "messages":"Messages" }, "properties":{ "ticketsCreated":"Tickets Created", @@ -472,7 +562,47 @@ "ticketsPinned":"Tickets Pinned", "ticketsMoved":"Tickets Moved", "usersBlacklisted":"Users Blacklisted", - "transcriptsCreated":"Transcripts Created" + "transcriptsCreated":"Transcripts Created", + "ticketsAutodeleted":"Tickets Autodeleted", + "ticketsTransferred":"Tickets Transferred", + "ticketVolume":"Ticket Volume", + "averageTickets":"Average Tickets/User", + "currentTickets":"Current Tickets", + "age":"Ticket Age", + "responseTime":"Response Time", + "resolutionTime":"Resolution Time", + "createdOn":"Created On", + "createdBy":"Created By", + "closedOn":"Closed On", + "closedBy":"Closed By", + "claimedOn":"Claimed On", + "claimedBy":"Claimed By", + "pinnedOn":"Pinned On", + "pinnedBy":"Pinned By", + "deletedOn":"Deleted On", + "deletedBy":"Deleted By" + }, + "roles":{ + "developer":"Developer", + "serverOwner":"Server Owner", + "serverAdmin":"Server Admin", + "moderator":"Moderator Team", + "support":"Support Team", + "member":"Member" } + }, + "panel":{ + "selectTicket":"Select your ticket", + "selectRole":"Select your role", + "selectOption":"Select your option" + }, + "priorities":{ + "urgent":"Urgent", + "veryHigh":"Very High", + "high":"High", + "normal":"Normal", + "low":"Low", + "veryLow":"Very Low", + "none":"None" } } \ No newline at end of file diff --git a/languages/dutch.json b/languages/dutch.json index 6168706..aee5be0 100644 --- a/languages/dutch.json +++ b/languages/dutch.json @@ -2,7 +2,7 @@ "_TRANSLATION":{ "otversion":"v4.1.0", "translators":["DJj123dj"], - "lastedited":"21/08/2024", + "lastedited":"09/11/2025", "language":"Dutch", "automated":false }, @@ -31,7 +31,15 @@ "stringContains":"Deze tekst moet {0} bevatten!", "stringChoices":"Deze tekst kan alleen de volgende waardes bevatten: {0}!", "stringRegex":"Deze tekst is ongeldig!", - + "stringInvertedContains":"Deze tekst mag niet {0} bevatten!", + "stringLowercase":"Deze tekst moet in kleine letters geschreven zijn!", + "stringUppercase":"Deze tekst moet in hoofdletters geschreven zijn!", + "stringSpecialCharacters":"Deze tekst mag geen speciale karakters bevatten! (enkel a-z, 0-9 & spatie)", + "stringNoSpaces":"Deze tekst mag geen spaties bevatten!", + "stringCapitalWord":"Het is aangeraden om ieder woord in de zin te laten starten met een hoofdletter!", + "stringCapitalSentence":"Het ziet er naar uit dat sommige zinnen in deze tekst niet met een hoofdletter beginnen!", + "stringPunctuation":"Het ziet er naar uit dat de zin in deze tekst niet met een leesteken eindigt!", + "numberTooShort":"Dit nummer kan niet korter dan {0} karakters zijn!", "numberTooLong":"Dit nummer kan niet langer dan {0} karakters zijn!", "numberLengthInvalid":"Dit nummer moet {0} karakters lang zijn!", @@ -48,10 +56,12 @@ "numberNegative":"Dit nummer kan niet negatief zijn!", "numberPositive":"Dit nummer kan niet positief zijn!", "numberZero":"Dit nummer kan niet nul zijn!", - + "numberNan":"Dit nummer kan geen NaN (Not A Number) zijn!", + "numberInvertedContains":"Dit nummer mag niet {0} bevatten!", + "booleanTrue":"Deze boolean kan niet true zijn!", "booleanFalse":"Deze boolean kan niet false zijn!", - + "arrayEmptyDisabled":"Deze array mag niet leeg zijn!", "arrayEmptyRequired":"Deze array moet leeg zijn!", "arrayTooShort":"Deze array moet een minimum lengte hebben van {0}!", @@ -59,7 +69,7 @@ "arrayLengthInvalid":"Deze array moet een lengte hebben van {0}!", "arrayInvalidTypes":"Deze array kan alleen de volgende waardes bevatten: {0}!", "arrayDouble":"Deze array staat dubbele waardes niet toe!", - + "discordInvalidId":"Dit is een ongeldig discord {0} id!", "discordInvalidIdOptions":"Dit is an ongeldig discord {0} id! Je kan ook een van deze gebruiken: {1}!", "discordInvalidToken":"Dit is een ongeldig discord token (syntactisch)!", @@ -76,7 +86,7 @@ "urlInvalidPath":"Deze url heeft een ongeldig pad!", "idNotUnique":"Dit id is niet uniek, gebruik een andere in de plaats!", "idNonExistent":"Het id {0} bestaat niet!", - + "invalidType":"Het type van deze property moet gelijk zijn aan: {0}!", "propertyMissing":"De property {0} mist in dit object!", "propertyOptional":"De property {0} is optioneel in dit object!", @@ -84,12 +94,13 @@ "nullInvalid":"Deze property kan niet null zijn!", "switchInvalidType":"Dit moet een van de volgende types zijn: {0}!", "objectSwitchInvalid":"Dit object moet een van de volgende types zijn: {0}!", - + "invalidLanguage":"Dit is een ongeldige taal!", "invalidButton":"Deze knop moet een {0} of {1} hebben!", "unusedOption":"De optie {0} wordt nergens gebruikt!", "unusedQuestion":"The vraag {0} wordt nergens gebruikt!", - "dropdownOption":"Een paneel met dropdown mag alleen opties bevaten van het 'ticket' type!" + "dropdownOption":"Een paneel met dropdown mag alleen opties bevaten van het 'ticket' type!", + "customInvalidVersion":"De versie gespecificeerd in u configbestand komt niet overeen! Zorg dat u het configbestand geüpdatet heeft naar de laatste versie!" } }, "actions":{ @@ -132,6 +143,7 @@ "blacklistAddDm":"Toegevoegd Aan Blacklist", "blacklistRemoveDm":"Verwijderd Van Blacklist", "clear":"Tickets Opgeruimd", + "clearTickets":"Tickets Opruimen", "roles":"Rollen Bijgewerkt", "autoclose":"Ticket Automatisch Gesloten", @@ -139,7 +151,12 @@ "autocloseDisabled":"Autoclose Uitgeschakeld", "autodelete":"Ticket Automatisch Verwijderd", "autodeleteEnabled":"Autodelete Ingeschakeld", - "autodeleteDisabled":"Autodelete Uitgeschakeld" + "autodeleteDisabled":"Autodelete Uitgeschakeld", + + "topicSet":"Onderwerp Veranderd", + "prioritySet":"Prioriteit Veranderd", + "priorityGet":"Ticket Prioriteit", + "transfer":"Ticket Overgedragen" }, "descriptions":{ "create":"Je ticket is aangemaakt. Klik op de knop hier onder om er naar toe te gaan!", @@ -154,7 +171,7 @@ "move":"Het ticket is succesvol verplaatst naar {0}!", "add":"{0} is succesvol toegevoegd aan het ticket!", "remove":"{0} is succesvol verwijderd van het ticket!", - + "helpExplanation":"`` => verplichte parameter\n`[naam]` => optionele parameter", "statsReset":"De bot stats zijn succesvol gereset!", "statsError":"Kan ticket stats niet bekijken!\n{0} is geen ticket!", @@ -167,7 +184,7 @@ "clearVerify":"Weet je zeker dat je meerdere tickets wilt verwijderen? Deze actie kan niet teruggedraaid worden!", "clearReady":"{0} tickets zijn succesvol verwijderd!", "rolesEmpty":"Geen enkele rollen zijn bijgewerkt!", - + "autocloseLeave":"Dit ticket is automatisch gesloten omdat de maker de server verlaten is!", "autocloseTimeout":"Dit ticket is automatisch gesloten omdat het meer dan `{0} uur` inactief was!", "autodeleteLeave":"Dit ticket is automatisch verwijderd omdat de maker de server verlaten is!", @@ -176,11 +193,16 @@ "autocloseDisabled":"Autoclose is uitgeschakeld in dit ticket!\nHet wordt niet meer automatisch gesloten!", "autodeleteEnabled":"Autodelete is ingeschakeld in dit ticket!\nHet wordt verwijderd na `{0} dagen` inactiviteit!", "autodeleteDisabled":"Autodelete is uitgeschakeld in dit ticket!\nHet wordt niet meer automatisch verwijderd!", - + "ticketMessageLimit":"Je kan maar {0} ticket(s) op het zelfde moment aanmaken!", "ticketMessageAutoclose":"Dit ticket wordt automatisch gesloten wanneer het inactief is voor {0} uur!", "ticketMessageAutodelete":"Dit ticket wordt automatisch verwijderd wanneer het inactief is voor {0} dagen!", - "panelReady":"Je kan het paneel hier onder vinden!\nDit bericht kan nu verwijderd worden!" + "panelReady":"Het paneel is beschikbaar in het volgende bericht!\nDit bericht kan verwijderd worden!", + + "topicSet":"Het kanaalsonderwerp is succesvol veranderd naar {0}!", + "prioritySet":"De ticket prioriteit is succesvol veranderd naar {0} door {1}!", + "priorityGet":"De huidige prioriteit van dit ticket is {0}.", + "transfer":"Het ticket is succesvol overgedragen van {0} naar {1} door {2}!" }, "modal":{ "closePlaceholder":"Waarom heb je dit ticket gesloten?", @@ -215,12 +237,19 @@ "addDm":"{0} is toegevoegd aan jouw ticket in onze server!", "removeLog":"{0} is verwijderd van dit ticket door {1}!", "removeDm":"{0} is verwijderd van jouw ticket in onze server!", - + "blacklistAddLog":"{0} is geblacklist door {1}!", "blacklistRemoveLog":"{0} is verwijderd van de blacklist door {1}!", "blacklistAddDm":"Je bent geblacklist in onze server!\nVanaf nu kan je geen tickets meer aanmaken!", "blacklistRemoveDm":"Je bent verwijderd van de blacklist in onze server!\nNormaal zou je nu terug tickets kunnen aanmaken!", - "clearLog":"{0} tickets zijn verwijderd door by {1}!" + "clearLog":"{0} tickets zijn verwijderd door by {1}!", + + "transferLog":"Het ticket is overgedragen van {0} naar {1} door {2}!", + "transferDm":"U ticket is overgedragen van {0} naar {1} in onze server!", + "prioritySetLog":"De prioriteit van dit ticket is veranderd naar {0} door {1}!", + "prioritySetDm":"De prioriteit van u ticket is veranderd naar {0} in onze server!", + "roleUpdateLog":"{0} heeft zijn/haar rollen bewerkt!", + "roleUpdateDm":"U rollen in onze server zijn veranderd!" } }, "transcripts":{ @@ -229,7 +258,7 @@ "ready":"Transcript Aangemaakt", "textFileDescription":"Dit is een tekst transcript van een verwijderd ticket!", "htmlProgress":"Gelieve te wachten terwijl dit html transcript verwerkt wordt...", - + "createdChannel":"Een nieuw {0} transcript is aangemaakt in de server!", "createdCreator":"Een nieuw {0} transcript is aangemaakt voor een van jouw tickets!", "createdParticipant":"Een nieuw {0} transcript is aangemaakt in een van de tickets waar jij lid van was!", @@ -241,7 +270,19 @@ "retry":"Herprobeer", "continue":"Verwijder Zonder Transcript", "backup":"Maak Backup Transcript", - "error":"Er is iets misgegaan bij het maken van het transcript.\nWat wil je doen?\n\nDit ticket wordt niet meer verwijderd to je een van de onderstaande knoppen gebruikt." + "error":"Er is iets misgegaan bij het maken van het transcript.\nWat wil je doen?\n\nDit ticket wordt niet meer verwijderd to je een van de onderstaande knoppen gebruikt.", + "title":"Transcript Error" + }, + "text":{ + "messagesTitle":"BERICHTEN", + "embedTitle":"EMBED", + "fileTitle":"BESTAND", + "fieldsTitle":"VELDEN", + "reactionsTitle":"REACTIES", + "statsTitle":"STATS", + "emptyContent":"", + "noTitle":"", + "noDesc":"" } }, "errors":{ @@ -257,7 +298,8 @@ "unknownPanel":"Onbekend Paneel", "notInGuild":"Niet In Server", "channelRename":"Kan Kanaal Niet Hernoemen", - "busy":"Ticket Is Bezig" + "busy":"Ticket Is Bezig", + "permissionError":"Permission Error" }, "descriptions":{ "askForInfo":"Contacteer the eigenaar van deze bot voor meer info!", @@ -280,7 +322,10 @@ "notInGuild":"Deze {0} werkt niet in DM! Probeer het opnieuw in een server!", "channelRename":"Door discord ratelimits is het op dit moment onmogelijk voor de bot om het kanaal te hernoemen. Het kanaal zal automatisch hernoemd worden over 10 min als de bot niet herstart wordt.", "channelRenameSource":"De bron van deze error is: {0}", - "busy":"Kan deze {0} niet gebruiken!\nHet ticket wordt op dit moment verwerkt door de bot.\n\nProbeer het opnieuw binnen een paar seconden!" + "busy":"Kan deze {0} niet gebruiken!\nHet ticket wordt op dit moment verwerkt door de bot.\n\nProbeer het opnieuw binnen een paar seconden!", + "closeBeforeMessage":"Dit ticket kan niet gesloten/verwijderd worden vooraleer er een bericht door een gebruiker verstuurd is.", + "closeBeforeAdminMessage":"Dit ticket kan niet gesloten/verwijderd worden vooraleer er een bericht door een ticket admin of supportlid verstuurd is.", + "unableToCreateTicket":"U kunt geen ticket aanmaken." }, "optionInvalidReasons":{ "stringRegex":"Waarde is niet gelijk aan het patroon!", @@ -331,7 +376,6 @@ "added":"Toegevoegd", "removed":"Verwijderd", "filter":"Filter", - "claimedBy":"Geclaimd Door {0}", "method":"Methode", "type":"Type", "blacklisted":"Geblacklist", @@ -355,7 +399,27 @@ "status":"Status", "claimed":"Geclaimd", "pinned":"Gepind", - "creationDate":"Aanmaakdatum" + "creationDate":"Aanmaakdatum", + + "noone":"Niemand", + "open":"Open", + "closed":"Gesloten", + "priority":"Prioriteit", + "participants":"Deelnemers", + "yes":"Ja", + "no":"Nee", + "option":"Optie", + "topic":"Onderwerp", + "uptime":"Systeem Uptime", + "messages":"Berichten", + "embeds":"Embeds", + "files":"Bestanden", + "components":"Componenten", + "cooldown":"Cooldown", + "maxTickets":"Max Tickets", + "admins":"Admins", + "roles":"Rollen", + "size":"Grootte" }, "lowercase":{ "text":"tekst", @@ -384,6 +448,7 @@ "unclaim":"Ontclaim dit ticket.", "pin":"Pin dit ticket.", "unpin":"Maak dit ticket los.", + "move":"Verplaats een ticket.", "moveId":"Het id van de optie waarnaar je wilt verplaatsen.", "rename":"Hernoem een ticket.", @@ -392,6 +457,7 @@ "addUser":"De gebruiker om toe te voegen.", "remove":"Verwijder een user van dit ticket.", "removeUser":"De gebruiker om te verwijderen.", + "blacklist":"Beheer de ticket blacklist.", "blacklistView":"Bekijk de huidige blacklist.", "blacklistAdd":"Voeg een gebruiker toe aan de blacklist.", @@ -405,6 +471,7 @@ "statsUserUser":"De gebruiker om te bekijken.", "statsTicket":"Bekijk de stats van een ticket in de server.", "statsTicketTicket":"Het ticket om te bekijken.", + "clear":"Verwijder meerdere tickets tegelijkertijd.", "clearFilter":"De filter om tickets op te ruimen.", "clearFilters":{ @@ -417,6 +484,7 @@ "unpin":"Losgemaakt (niet gepind)", "autoclose":"Automatisch Gesloten" }, + "autoclose":"Beheer autoclose in een ticket.", "autocloseDisable":"Schakel autoclose in dit ticket uit.", "autocloseEnable":"Schakel autoclose in dit ticket in.", @@ -424,7 +492,19 @@ "autodelete":"Beheer autodelete in een ticket.", "autodeleteDisable":"Schakel autodelete in dit ticket aan.", "autodeleteEnable":"Schakel autodelete in dit ticket aan.", - "autodeleteEnableTime":"De hoeveelheid dagen dat dit ticket inactief moet zijn voor het te verwijderen." + "autodeleteEnableTime":"De hoeveelheid dagen dat dit ticket inactief moet zijn voor het te verwijderen.", + + "topic":"Beheer het onderwerp van een ticket kanaal.", + "topicSet":"Verander het onderwerp van een ticket kanaal.", + "topicValue":"Het nieuwe onderwerp van het kanaal.", + "topicList":"Bekijk een lijst van alle tickets met hun onderwerp en stats.", + "priority":"Beheer de prioriteit van een ticket.", + "prioritySet":"Verander de prioriteit van een ticket.", + "priorityValue":"De prioriteit van het kanaal.", + "priorityGet":"Bekijk de prioriteit van een ticket.", + "priorityList":"Bekijk een lijst van alle tickets met hun prioriteit.", + "transfer":"Draag het ticket eigendom over van de ene naar de andere gebruiker.", + "transferUser":"De user om naar over te dragen." }, "helpMenu":{ "help":"Krijg een lijst van alle beschikbare commands.", @@ -452,7 +532,16 @@ "autocloseDisable":"Zet autoclose uit in dit ticket.", "autocloseEnable":"Zet autoclose in aan dit ticket.", "autodeleteDisable":"Zet autodelete uit in dit ticket.", - "autodeleteEnable":"Zet autodelete aan in dit ticket." + "autodeleteEnable":"Zet autodelete aan in dit ticket.", + "categories":{ + "general":"Algemene Commands", + "basicTicket":"Basis Ticket Commands", + "advancedTicket":"Geavanceerde Ticket Commands", + "userTicket":"Gebruiker Ticket Commands", + "admin":"Admin Commands", + "advanced":"Geavanceerde Commands", + "extra":"Extra Commands" + } }, "stats":{ "scopes":{ @@ -460,7 +549,8 @@ "system":"Systeem Stats", "user":"User Stats", "ticket":"Ticket Stats", - "participants":"Deelnemers" + "participants":"Deelnemers", + "messages":"Berichten" }, "properties":{ "ticketsCreated":"Tickets Aangemaakt", @@ -472,7 +562,47 @@ "ticketsPinned":"Tickets Gepind", "ticketsMoved":"Tickets Verplaatst", "usersBlacklisted":"Users Geblacklist", - "transcriptsCreated":"Transcripts Aangemaakt" + "transcriptsCreated":"Transcripts Aangemaakt", + "ticketsAutodeleted":"Tickets Autodeleted", + "ticketsTransferred":"Tickets Overgedragen", + "ticketVolume":"Ticket Volume", + "averageTickets":"Gemiddelde Tickets/Gebruiker", + "currentTickets":"Huidige Tickets", + "age":"Ticket Leeftijd", + "responseTime":"Antwoordtijd", + "resolutionTime":"Oplostijd", + "createdOn":"Gemaakt Op", + "createdBy":"Gemaakt Door", + "closedOn":"Gesloten Op", + "closedBy":"Gesloten Door", + "claimedOn":"Geclaimed Op", + "claimedBy":"Geclaimed Door", + "pinnedOn":"Gepint Op", + "pinnedBy":"Gepint Door", + "deletedOn":"Verwijderd Op", + "deletedBy":"Verwijderd Door" + }, + "roles":{ + "developer":"Developer", + "serverOwner":"Server Eigenaar", + "serverAdmin":"Server Admin", + "moderator":"Moderator Team", + "support":"Support Team", + "member":"Serverlid" } + }, + "panel":{ + "selectTicket":"Selecteer u ticket", + "selectRole":"Selecteer u rol", + "selectOption":"Selecteer u optie" + }, + "priorities":{ + "urgent":"Urgent", + "veryHigh":"Zeer Hoog", + "high":"Hoog", + "normal":"Normaal", + "low":"Laag", + "veryLow":"Zeer Laag", + "none":"Geen" } } \ No newline at end of file diff --git a/languages/english.json b/languages/english.json index 9fe9246..9b86fa2 100644 --- a/languages/english.json +++ b/languages/english.json @@ -2,7 +2,7 @@ "_TRANSLATION":{ "otversion":"v4.1.0", "translators":["DJj123dj"], - "lastedited":"21/08/2024", + "lastedited":"09/11/2025", "language":"English", "automated":false }, @@ -31,7 +31,15 @@ "stringContains":"This string needs to contain {0}!", "stringChoices":"This string can only be one of the following values: {0}!", "stringRegex":"This string is invalid!", - + "stringInvertedContains":"This string is not allowed to contain {0}!", + "stringLowercase":"This string must be written in lowercase only!", + "stringUppercase":"This string must be written in uppercase only!", + "stringSpecialCharacters":"This string is not allowed to contain any special characters! (a-z, 0-9 & space only)", + "stringNoSpaces":"This string is not allowed to contain spaces!", + "stringCapitalWord":"It's recommended that each word in this string starts with a capital letter!", + "stringCapitalSentence":"It looks like some sentences in this string don't start with a capital letter!", + "stringPunctuation":"It looks like the sentence in this string doesn't end with a punctuation mark!", + "numberTooShort":"This number can't be shorter than {0} characters!", "numberTooLong":"This number can't be longer than {0} characters!", "numberLengthInvalid":"This number needs to be {0} characters long!", @@ -48,10 +56,12 @@ "numberNegative":"This number can't be negative!", "numberPositive":"This number can't be positive!", "numberZero":"This number can't be zero!", - + "numberNan":"This number can't be NaN (Not A Number)!", + "numberInvertedContains":"This number is not allowed to contain {0}!", + "booleanTrue":"This boolean can't be true!", "booleanFalse":"This boolean can't be false!", - + "arrayEmptyDisabled":"This array isn't allowed to be empty!", "arrayEmptyRequired":"This array is required to be empty!", "arrayTooShort":"This array needs to have a length of at least {0}!", @@ -59,7 +69,7 @@ "arrayLengthInvalid":"This array needs to have a length of {0}!", "arrayInvalidTypes":"This array can only contain the following types: {0}!", "arrayDouble":"This array doesn't allow the same value twice!", - + "discordInvalidId":"This is an invalid discord {0} id!", "discordInvalidIdOptions":"This is an invalid discord {0} id! You can also use one of these: {1}!", "discordInvalidToken":"This is an invalid discord token (syntactically)!", @@ -76,7 +86,7 @@ "urlInvalidPath":"This url has an invalid path!", "idNotUnique":"This id isn't unique, use another id instead!", "idNonExistent":"The id {0} doesn't exist!", - + "invalidType":"This property needs to be the type: {0}!", "propertyMissing":"The property {0} is missing from this object!", "propertyOptional":"The property {0} is optional in this object!", @@ -84,12 +94,13 @@ "nullInvalid":"This property can't be null!", "switchInvalidType":"This needs to be one of the following types: {0}!", "objectSwitchInvalid":"This object needs to be one of the following types: {0}!", - + "invalidLanguage":"This is an invalid language!", "invalidButton":"This button needs to have at least an {0} or {1}!", "unusedOption":"The option {0} isn't used anywhere!", "unusedQuestion":"The question {0} isn't used anywhere!", - "dropdownOption":"A panel with dropdown enabled can only contain options of the 'ticket' type!" + "dropdownOption":"A panel with dropdown enabled can only contain options of the 'ticket' type!", + "customInvalidVersion":"The version specified in your config does not match! Make sure you have updated the config to the latest version!" } }, "actions":{ @@ -132,6 +143,7 @@ "blacklistAddDm":"Added To Blacklist", "blacklistRemoveDm":"Removed From Blacklist", "clear":"Tickets Cleared", + "clearTickets":"Clear Tickets", "roles":"Roles Updated", "autoclose":"Ticket Autoclosed", @@ -139,7 +151,12 @@ "autocloseDisabled":"Autoclose Disabled", "autodelete":"Ticket Autodeleted", "autodeleteEnabled":"Autodelete Enabled", - "autodeleteDisabled":"Autodelete Disabled" + "autodeleteDisabled":"Autodelete Disabled", + + "topicSet":"Topic Changed", + "prioritySet":"Priority Changed", + "priorityGet":"Ticket Priority", + "transfer":"Ticket Transferred" }, "descriptions":{ "create":"Your ticket has been created. Click the button below to access it!", @@ -154,7 +171,7 @@ "move":"The ticket has been moved to {0} successfully!", "add":"{0} has been added to the ticket successfully!", "remove":"{0} has been removed from the ticket successfully!", - + "helpExplanation":"`` => required parameter\n`[name]` => optional parameter", "statsReset":"The bot stats have been reset successfully!", "statsError":"Unable to view ticket stats!\n{0} is not a ticket!", @@ -167,7 +184,7 @@ "clearVerify":"Are you sure you want to delete multiple tickets?\nThis action can't be undone!", "clearReady":"{0} tickets have been deleted successfully!", "rolesEmpty":"No roles have been updated!", - + "autocloseLeave":"This ticket has been autoclosed because the creator left the server!", "autocloseTimeout":"This ticket has been autoclosed because it has been inactive for more than `{0}h`!", "autodeleteLeave":"This ticket has been autodeleted because the creator left the server!", @@ -176,11 +193,16 @@ "autocloseDisabled":"Autoclose has been disabled in this ticket!\nIt won't be closed automatically anymore!", "autodeleteEnabled":"Autodelete has been enabled in this ticket!\nIt will be deleted when it is inactive for more than `{0} days`!", "autodeleteDisabled":"Autodelete has been disabled in this ticket!\nIt won't be deleted automatically anymore!", - + "ticketMessageLimit":"You can only create {0} ticket(s) at the same time!", "ticketMessageAutoclose":"This ticket will be autoclosed when inactive for {0}h!", "ticketMessageAutodelete":"This ticket will be autodeleted when inactive for {0} days!", - "panelReady":"You can find the panel below!\nThis message can now be deleted!" + "panelReady":"The panel is available in the followup message!\nThis message can now be deleted!", + + "topicSet":"The channel topic has been changed by {0} successfully!", + "prioritySet":"The ticket priority has been changed to {0} by {1} successfully!", + "priorityGet":"The current priority of this ticket is {0}.", + "transfer":"The ticket ownership has been transferred from {0} to {1} by {2} successfully!" }, "modal":{ "closePlaceholder":"Why did you close this ticket?", @@ -215,12 +237,19 @@ "addDm":"{0} has been added to your ticket in our server!", "removeLog":"{0} has been removed from this ticket by {1}!", "removeDm":"{0} has been removed from your ticket in our server!", - + "blacklistAddLog":"{0} was blacklisted by {1}!", "blacklistRemoveLog":"{0} was removed from the blacklist by {1}!", "blacklistAddDm":"You have been blacklisted in our server!\nFrom now on, you are unable to create a ticket!", "blacklistRemoveDm":"You have been removed from the blacklist in our server!\nNow you can create tickets again!", - "clearLog":"{0} tickets have been deleted by {1}!" + "clearLog":"{0} tickets have been deleted by {1}!", + + "transferLog":"The ownership of this ticket has been transferred from {0} to {1} by {2}!", + "transferDm":"The ownership of your ticket has been transferred from {0} to {1} in our server!", + "prioritySetLog":"The priority of this ticket has been changed to {0} by {1}!", + "prioritySetDm":"The priority of your ticket has been changed to {0} in our server!", + "roleUpdateLog":"{0} has updated their roles!", + "roleUpdateDm":"Your roles in our server have been updated!" } }, "transcripts":{ @@ -229,7 +258,7 @@ "ready":"Transcript Created", "textFileDescription":"This is the text transcript of a deleted ticket!", "htmlProgress":"Please wait while this html transcript is getting processed...", - + "createdChannel":"A new {0} transcript has been created in the server!", "createdCreator":"A new {0} transcript has been created for one of your tickets!", "createdParticipant":"A new {0} transcript has been created in one of the tickets you participated in!", @@ -241,7 +270,19 @@ "retry":"Retry", "continue":"Delete Without Transcript", "backup":"Create Backup Transcript", - "error":"Something went wrong while trying to create the transcript.\nWhat would you like to do?\n\nThis ticket won't be deleted until you click one of these buttons." + "error":"Something went wrong while trying to create the transcript.\nWhat would you like to do?\n\nThis ticket won't be deleted until you click one of these buttons.", + "title":"Transcript Error" + }, + "text":{ + "messagesTitle":"MESSAGES", + "embedTitle":"EMBED", + "fileTitle":"FILE", + "fieldsTitle":"FIELDS", + "reactionsTitle":"REACTIONS", + "statsTitle":"STATS", + "emptyContent":"", + "noTitle":"", + "noDesc":"" } }, "errors":{ @@ -257,7 +298,8 @@ "unknownPanel":"Unknown Panel", "notInGuild":"Not In Server", "channelRename":"Unable To Rename Channel", - "busy":"Ticket Is Busy" + "busy":"Ticket Is Busy", + "permissionError":"Permission Error" }, "descriptions":{ "askForInfo":"Contact the owner of this bot for more info!", @@ -280,7 +322,10 @@ "notInGuild":"This {0} doesn't work in DM! Please try it again in a server!", "channelRename":"Due to discord ratelimits, it's currently impossible for the bot to rename the channel. The channel will automatically be renamed over 10 minutes if the bot isn't rebooted.", "channelRenameSource":"The source of this error is: {0}", - "busy":"Unable to use this {0}!\nThe ticket is currently being processed by the bot.\n\nPlease try again in a few seconds!" + "busy":"Unable to use this {0}!\nThe ticket is currently being processed by the bot.\n\nPlease try again in a few seconds!", + "closeBeforeMessage":"This ticket cannot be closed/deleted before a message has been sent by a user.", + "closeBeforeAdminMessage":"This ticket cannot be closed/deleted before a message has been sent by a ticket admin or support member.", + "unableToCreateTicket":"You are unable to create a ticket." }, "optionInvalidReasons":{ "stringRegex":"Value doesn't match pattern!", @@ -331,7 +376,6 @@ "added":"Added", "removed":"Removed", "filter":"Filter", - "claimedBy":"Claimed By {0}", "method":"Method", "type":"Type", "blacklisted":"Blacklisted", @@ -355,7 +399,27 @@ "status":"Status", "claimed":"Claimed", "pinned":"Pinned", - "creationDate":"Creation Date" + "creationDate":"Creation Date", + + "noone":"No-One", + "open":"Open", + "closed":"Closed", + "priority":"Priority", + "participants":"Participants", + "yes":"Yes", + "no":"No", + "option":"Option", + "topic":"Topic", + "uptime":"System Uptime", + "messages":"Messages", + "embeds":"Embeds", + "files":"Files", + "components":"Components", + "cooldown":"Cooldown", + "maxTickets":"Max Tickets", + "admins":"Admins", + "roles":"Roles", + "size":"Size" }, "lowercase":{ "text":"text", @@ -384,6 +448,7 @@ "unclaim":"Unclaim a ticket.", "pin":"Pin a ticket.", "unpin":"Unpin a ticket.", + "move":"Move a ticket.", "moveId":"The identifier of the option that you want to move to.", "rename":"Rename a ticket.", @@ -392,6 +457,7 @@ "addUser":"The user to add.", "remove":"Remove a user from a ticket.", "removeUser":"The user to remove.", + "blacklist":"Manage the ticket blacklist.", "blacklistView":"View a list of the current blacklist.", "blacklistAdd":"Add a user to the blacklist.", @@ -405,6 +471,7 @@ "statsUserUser":"The user to view.", "statsTicket":"View the stats of a ticket in the server.", "statsTicketTicket":"The ticket to view.", + "clear":"Delete multiple tickets at the same time.", "clearFilter":"The filter for clearing tickets.", "clearFilters":{ @@ -417,6 +484,7 @@ "unpin":"Unpinned", "autoclose":"Autoclosed" }, + "autoclose":"Manage autoclose in a ticket.", "autocloseDisable":"Disable autoclose in this ticket.", "autocloseEnable":"Enable autoclose in this ticket.", @@ -424,7 +492,19 @@ "autodelete":"Manage autodelete in a ticket.", "autodeleteDisable":"Disable autodelete in this ticket.", "autodeleteEnable":"Enable autodelete in this ticket.", - "autodeleteEnableTime":"The amount of days this ticket needs to be inactive to delete it." + "autodeleteEnableTime":"The amount of days this ticket needs to be inactive to delete it.", + + "topic":"Manage the topic of the ticket channel.", + "topicSet":"Set the topic of the ticket channel.", + "topicValue":"The new topic of the channel.", + "topicList":"Get a list of all tickets with their topic and stats.", + "priority":"Manage the priority of the ticket.", + "prioritySet":"Set the priority of the ticket.", + "priorityValue":"The priority of the channel.", + "priorityGet":"Get the priority of the ticket.", + "priorityList":"Get a list of all tickets with their priority status.", + "transfer":"Transfer the ticket ownership from one user to another.", + "transferUser":"The user to transfer to." }, "helpMenu":{ "help":"Get a list of all the available commands.", @@ -452,7 +532,16 @@ "autocloseDisable":"Disable autoclose in this ticket.", "autocloseEnable":"Enable autoclose in this ticket.", "autodeleteDisable":"Disable autodelete in this ticket.", - "autodeleteEnable":"Enable autodelete in this ticket." + "autodeleteEnable":"Enable autodelete in this ticket.", + "categories":{ + "general":"General Commands", + "basicTicket":"Basic Ticket Commands", + "advancedTicket":"Advanced Ticket Commands", + "userTicket":"User Ticket Commands", + "admin":"Admin Commands", + "advanced":"Advanced Commands", + "extra":"Extra Commands" + } }, "stats":{ "scopes":{ @@ -460,7 +549,8 @@ "system":"System Stats", "user":"User Stats", "ticket":"Ticket Stats", - "participants":"Participants" + "participants":"Participants", + "messages":"Messages" }, "properties":{ "ticketsCreated":"Tickets Created", @@ -472,7 +562,47 @@ "ticketsPinned":"Tickets Pinned", "ticketsMoved":"Tickets Moved", "usersBlacklisted":"Users Blacklisted", - "transcriptsCreated":"Transcripts Created" + "transcriptsCreated":"Transcripts Created", + "ticketsAutodeleted":"Tickets Autodeleted", + "ticketsTransferred":"Tickets Transferred", + "ticketVolume":"Ticket Volume", + "averageTickets":"Average Tickets/User", + "currentTickets":"Current Tickets", + "age":"Ticket Age", + "responseTime":"Response Time", + "resolutionTime":"Resolution Time", + "createdOn":"Created On", + "createdBy":"Created By", + "closedOn":"Closed On", + "closedBy":"Closed By", + "claimedOn":"Claimed On", + "claimedBy":"Claimed By", + "pinnedOn":"Pinned On", + "pinnedBy":"Pinned By", + "deletedOn":"Deleted On", + "deletedBy":"Deleted By" + }, + "roles":{ + "developer":"Developer", + "serverOwner":"Server Owner", + "serverAdmin":"Server Admin", + "moderator":"Moderator Team", + "support":"Support Team", + "member":"Member" } + }, + "panel":{ + "selectTicket":"Select your ticket", + "selectRole":"Select your role", + "selectOption":"Select your option" + }, + "priorities":{ + "urgent":"Urgent", + "veryHigh":"Very High", + "high":"High", + "normal":"Normal", + "low":"Low", + "veryLow":"Very Low", + "none":"None" } } \ No newline at end of file diff --git a/languages/estonian.json b/languages/estonian.json index e5d0ac4..1430e9d 100644 --- a/languages/estonian.json +++ b/languages/estonian.json @@ -2,7 +2,7 @@ "_TRANSLATION":{ "otversion":"v4.1.0", "translators":["iamnotmega","ChatGPT"], - "lastedited":"21/10/2024", + "lastedited":"09/11/2025", "language":"Estonian", "automated":true }, @@ -31,7 +31,15 @@ "stringContains":"See string peab sisaldama {0}!", "stringChoices":"See string võib olla ainult üks järgmistest väärtustest: {0}!", "stringRegex":"See string on kehtetu!", - + "stringInvertedContains":"See string ei tohi sisaldada {0}!", + "stringLowercase":"See string peab olema kirjutatud ainult väikeste tähtedega!", + "stringUppercase":"See string peab olema kirjutatud ainult suurte tähtedega!", + "stringSpecialCharacters":"See string ei tohi sisaldada erimärke! (ainult a-z, 0-9 ja tühik)", + "stringNoSpaces":"See string ei tohi sisaldada tühikuid!", + "stringCapitalWord":"Soovitatav on, et iga sõna selles stringis algaks suure tähega!", + "stringCapitalSentence":"Tundub, et mõned laused selles stringis ei alga suure tähega!", + "stringPunctuation":"Tundub, et lause selles stringis ei lõpe kirjavahemärgiga!", + "numberTooShort":"See number ei tohi olla lühem kui {0} tähemärki!", "numberTooLong":"See number ei tohi olla pikem kui {0} tähemärki!", "numberLengthInvalid":"See number peab olema {0} tähemärki pikk!", @@ -48,10 +56,12 @@ "numberNegative":"See arv ei saa olla negatiivne!", "numberPositive":"See arv ei saa olla positiivne!", "numberZero":"See arv ei saa olla null!", - + "numberNan":"See number ei tohi olla NaN (mitte number)!", + "numberInvertedContains":"See number ei tohi sisaldada {0}!", + "booleanTrue":"See tõeväärtus ei saa olla tõsi!", "booleanFalse":"See tõeväärtus ei saa olla vale!", - + "arrayEmptyDisabled":"See massiiv ei tohi olla tühi!", "arrayEmptyRequired":"See massiiv peab olema tühi!", "arrayTooShort":"Selle massiivi pikkus peab olema vähemalt {0}!", @@ -59,7 +69,7 @@ "arrayLengthInvalid":"Selle massiivi pikkus peab olema {0}!", "arrayInvalidTypes":"See massiiv võib sisaldada ainult järgmisi tüüpe: {0}!", "arrayDouble":"See massiiv ei luba sama väärtust kaks korda kasutada!", - + "discordInvalidId":"See on kehtetu Discordi {0} ID!", "discordInvalidIdOptions":"See on kehtetu Discordi {0} ID! Võite kasutada ka ühte järgmistest: {1}!", "discordInvalidToken":"See on kehtetu Discordi tooken (süntaktiliselt)!", @@ -76,7 +86,7 @@ "urlInvalidPath":"Sellel URL-il on vale tee!", "idNotUnique":"See ID ei ole kordumatu, kasutage selle asemel teist ID-d!", "idNonExistent":"ID-d {0} pole olemas!", - + "invalidType":"Selle atribuudi tüüp peab olema: {0}!", "propertyMissing":"Sellel objektil puudub atribuut {0}!", "propertyOptional":"Atribuut {0} on selles objektis valikuline!", @@ -84,12 +94,13 @@ "nullInvalid":"See vara ei saa olla tühi!", "switchInvalidType":"See peab olema üks järgmistest tüüpidest: {0}!", "objectSwitchInvalid":"See objekt peab olema ühte järgmistest tüüpidest: {0}!", - + "invalidLanguage":"See on sobimatu keel!", "invalidButton":"Sellel nupul peab olema vähemalt {0} või {1}!", "unusedOption":"Valik {0} ei ole kusagil kasutusel!", "unusedQuestion":"Küsimust {0} ei kasutata kuskil!", - "dropdownOption":"Lubatud rippmenüüga paneel võib sisaldada ainult \"pileti\" tüüpi valikuid!" + "dropdownOption":"Lubatud rippmenüüga paneel võib sisaldada ainult \"pileti\" tüüpi valikuid!", + "customInvalidVersion":"Teie konfiguratsioonis määratud versioon ei ühti! Veenduge, et olete konfiguratsiooni uuendanud uusimale versioonile!" } }, "actions":{ @@ -122,7 +133,7 @@ "move":"Pilet teisaldatud", "add":"Pileti kasutaja lisatud", "remove":"Pileti kasutaja eemaldatud", - + "help":"Saadaolevad käsud", "statsReset":"Lähtestage statistika", "blacklistAdd":"Kasutaja on mustas nimekirjas", @@ -132,14 +143,20 @@ "blacklistAddDm":"Lisatud musta nimekirja", "blacklistRemoveDm":"Eemaldatud mustast nimekirjast", "clear":"Piletid kustutatud", + "clearTickets":"Kustuta Piletid", "roles":"Rollid uuendatud", - + "autoclose":"Pilet automaatselt suletud", "autocloseEnabled":"Automaatne sulgemine lubatud", "autocloseDisabled":"Automaatne sulgemine keelatud", "autodelete":"Pilet on automaatselt kustutatud", "autodeleteEnabled":"Automaatne kustutamine lubatud", - "autodeleteDisabled":"Automaatne kustutamine keelatud" + "autodeleteDisabled":"Automaatne kustutamine keelatud", + + "topicSet":"Teema Muudetud", + "prioritySet":"Prioriteet Muudetud", + "priorityGet":"Pileti Prioriteet", + "transfer":"Pilet Üle Antud" }, "descriptions":{ "create":"Teie pilet on loodud. Sellele juurdepääsuks klõpsake alloleval nupul!", @@ -154,7 +171,7 @@ "move":"Pilet teisaldati asukohta {0} edukalt!", "add":"{0} lisati piletile edukalt!", "remove":"{0} eemaldati piletist edukalt!", - + "helpExplanation":"`` => nõutav parameeter\n`[nimi]` => valikuline parameeter", "statsReset":"Boti statistika on edukalt lähtestatud!", "statsError":"Piletistatistikat ei saa vaadata!\n{0} ei ole pilet!", @@ -167,7 +184,7 @@ "clearVerify":"Kas soovite kindlasti mitu piletit kustutada?\nSeda toimingut ei saa tagasi võtta!", "clearReady":"{0} piletit on edukalt kustutatud!", "rolesEmpty":"Ühtegi rolli pole värskendatud!", - + "autocloseLeave":"See pilet on automaatselt suletud, kuna looja lahkus serverist!", "autocloseTimeout":"See pilet on automaatselt suletud, kuna see on olnud passiivne rohkem kui `{0}h`!", "autodeleteLeave":"See pilet on automaatselt kustutatud, kuna looja lahkus serverist!", @@ -176,11 +193,16 @@ "autocloseDisabled":"Automaatne sulgemine on sellel piletil keelatud!\nSeda ei suleta enam automaatselt!", "autodeleteEnabled":"Sellel piletil on automaatne kustutamine lubatud!\nSee kustutatakse, kui see on passiivne rohkem kui `{0} päeva`!", "autodeleteDisabled":"Automaatne kustutamine on sellel piletil keelatud!\nSeda ei kustutata enam automaatselt!", - + "ticketMessageLimit":"Saate korraga luua ainult {0} piletit!", "ticketMessageAutoclose":"See pilet suletakse automaatselt, kui see on {0} tundi passiivne!", "ticketMessageAutodelete":"See pilet kustutatakse automaatselt, kui see on {0} päeva passiivne!", - "panelReady":"Paneeli leiate altpoolt!\nSelle sõnumi saab nüüd kustutada!" + "panelReady":"Paneel on saadaval järelteates!\nSelle sõnumi võib nüüd kustutada!", + + "topicSet":"Kanaliteema on edukalt muudetud kasutaja {0} poolt!", + "prioritySet":"Pileti prioriteet on edukalt muudetud väärtuseks {0} kasutaja {1} poolt!", + "priorityGet":"Selle pileti praegune prioriteet on {0}.", + "transfer":"Pileti omand on edukalt üle antud kasutajalt {0} kasutajale {1} kasutaja {2} poolt!" }, "modal":{ "closePlaceholder":"Miks sa selle pileti kinni panid?", @@ -215,12 +237,19 @@ "addDm":"{0} lisati teie piletile meie serveris!", "removeLog":"Kasutaja {1} eemaldas sellelt piletilt {0}!", "removeDm":"{0} on teie piletist meie serveris eemaldatud!", - + "blacklistAddLog":"{1} lisas kasutaja {0} musta nimekirja!", "blacklistRemoveLog":"Kasutaja {1} eemaldas kasutaja {0} mustast nimekirjast!", "blacklistAddDm":"Olete meie serveris mustas nimekirjas!\nNüüdsest ei saa te piletit luua!", "blacklistRemoveDm":"Teid on meie serveri mustast nimekirjast eemaldatud!\nNüüd saate uuesti pileteid luua!", - "clearLog":"{1} on kustutanud {0} piletit!" + "clearLog":"{1} on kustutanud {0} piletit!", + + "transferLog":"Selle pileti omand on üle antud kasutajalt {0} kasutajale {1} kasutaja {2} poolt!", + "transferDm":"Teie pileti omand on üle antud kasutajalt {0} kasutajale {1} meie serveris!", + "prioritySetLog":"Selle pileti prioriteet on muudetud väärtuseks {0} kasutaja {1} poolt!", + "prioritySetDm":"Teie pileti prioriteet on muudetud väärtuseks {0} meie serveris!", + "roleUpdateLog":"{0} uuendas oma rolle!", + "roleUpdateDm":"Teie rolle meie serveris on uuendatud!" } }, "transcripts":{ @@ -229,7 +258,7 @@ "ready":"Transkriptsioon loodud", "textFileDescription":"See on kustutatud pileti teksti ärakiri!", "htmlProgress":"Palun oodake, kuni seda html-i ärakirja töödeldakse...", - + "createdChannel":"Serveris on loodud uus {0} ärakiri!", "createdCreator":"Ühe teie pileti jaoks on loodud uus {0} ärakiri!", "createdParticipant":"Ühes teie osalenud piletis on loodud uus {0} ärakiri!", @@ -241,7 +270,19 @@ "retry":"Proovi uuesti", "continue":"Kustuta ilma ärakirjata", "backup":"Loo varukoopia ärakirjast", - "error":"Midagi läks ärakirja loomisel valesti.\nMida sa teha tahaksid?\n\nSeda piletit ei kustutata enne, kui klõpsate ühel neist nuppudest." + "error":"Midagi läks ärakirja loomisel valesti.\nMida sa teha tahaksid?\n\nSeda piletit ei kustutata enne, kui klõpsate ühel neist nuppudest.", + "title":"Transkriptsiooniviga" + }, + "text":{ + "messagesTitle":"SÕNUMID", + "embedTitle":"EMBED", + "fileTitle":"FAIL", + "fieldsTitle":"VÄLJAD", + "reactionsTitle":"REAKTSIOONID", + "statsTitle":"STATISTIKA", + "emptyContent":"", + "noTitle":"", + "noDesc":"" } }, "errors":{ @@ -257,7 +298,8 @@ "unknownPanel":"Tundmatu paneel", "notInGuild":"Pole serveris", "channelRename":"Kanalit ei saa ümber nimetada", - "busy":"Pilet on kinni" + "busy":"Pilet on kinni", + "permissionError":"Õiguste Viga" }, "descriptions":{ "askForInfo":"Lisateabe saamiseks võtke ühendust selle roboti omanikuga!", @@ -280,7 +322,10 @@ "notInGuild":"See {0} ei tööta DM-is! Palun proovi uuesti serveris!", "channelRename":"Ebakõlaliste kiiruspiirangute tõttu on robotil praegu võimatu kanalit ümber nimetada. Kui robotit ei taaskäivitata, nimetatakse kanal 10 minuti jooksul automaatselt ümber.", "channelRenameSource":"Selle vea allikas on: {0}", - "busy":"Seda {0} ei saa kasutada!\nPiletit töötleb praegu bot.\n\nPalun proovige mõne sekundi pärast uuesti!" + "busy":"Seda {0} ei saa kasutada!\nPiletit töötleb praegu bot.\n\nPalun proovige mõne sekundi pärast uuesti!", + "closeBeforeMessage":"Seda piletit ei saa sulgeda/kustutada enne, kui kasutaja on sõnumi saatnud.", + "closeBeforeAdminMessage":"Seda piletit ei saa sulgeda/kustutada enne, kui piletihaldur või tugiliige on sõnumi saatnud.", + "unableToCreateTicket":"Te ei saa piletit luua." }, "optionInvalidReasons":{ "stringRegex":"Väärtus ei vasta mustrile!", @@ -331,7 +376,6 @@ "added":"Lisatud", "removed":"Eemaldatud", "filter":"Filter", - "claimedBy":"Nõude esitas {0}", "method":"Meetod", "type":"Tüüp", "blacklisted":"Mustas nimekirjas", @@ -355,7 +399,27 @@ "status":"Olek", "claimed":"Nõutud", "pinned":"Kinnitatud", - "creationDate":"Loomise kuupäev" + "creationDate":"Loomise kuupäev", + + "noone":"Mitte Keegi", + "open":"Avatud", + "closed":"Suletud", + "priority":"Prioriteet", + "participants":"Osalejad", + "yes":"Jah", + "no":"Ei", + "option":"Valik", + "topic":"Teema", + "uptime":"Süsteemi Töötamise Aeg", + "messages":"Sõnumid", + "embeds":"Embedid", + "files":"Failid", + "components":"Komponendid", + "cooldown":"Jahtumisaeg", + "maxTickets":"Maksimaalselt Pileteid", + "admins":"Adminid", + "roles":"Rollid", + "size":"Suurus" }, "lowercase":{ "text":"tekst", @@ -384,6 +448,7 @@ "unclaim":"Tühista pilet.", "pin":"Kinnitage pilet.", "unpin":"Vabastage pilet.", + "move":"Liigutage pilet.", "moveId":"Selle valiku identifikaator, millele soovite liikuda.", "rename":"Nimetage pilet ümber.", @@ -392,6 +457,7 @@ "addUser":"Lisatav kasutaja.", "remove":"Kasutaja eemaldamine piletist.", "removeUser":"Eemaldatav kasutaja.", + "blacklist":"Hallake piletite musta nimekirja.", "blacklistView":"Vaadake praeguse musta nimekirja loendit.", "blacklistAdd":"Lisage kasutaja musta nimekirja.", @@ -405,6 +471,7 @@ "statsUserUser":"Kasutaja vaatamiseks.", "statsTicket":"Vaadake serveris pileti statistikat.", "statsTicketTicket":"Pilet vaatamiseks.", + "clear":"Kustutage mitu piletit korraga.", "clearFilter":"Piletite puhastamise filter.", "clearFilters":{ @@ -417,6 +484,7 @@ "unpin":"Vabastatud", "autoclose":"Automaatselt suletud" }, + "autoclose":"Hallake piletis automaatset sulgemist.", "autocloseDisable":"Keela sellel piletil automaatne sulgemine.", "autocloseEnable":"Luba sellel piletil automaatne sulgemine.", @@ -424,7 +492,19 @@ "autodelete":"Automaatse kustutamise haldamine piletis.", "autodeleteDisable":"Keela sellel piletil automaatne kustutamine.", "autodeleteEnable":"Luba sellel piletil automaatne kustutamine.", - "autodeleteEnableTime":"Päevade arv, mille jooksul see pilet peab olema selle kustutamiseks passiivne." + "autodeleteEnableTime":"Päevade arv, mille jooksul see pilet peab olema selle kustutamiseks passiivne.", + + "topic":"Halda pileti kanali teemat.", + "topicSet":"Määra pileti kanali teema.", + "topicValue":"Kanali uus teema.", + "topicList":"Hangi nimekiri kõigist piletitest koos nende teemade ja statistikaga.", + "priority":"Halda pileti prioriteeti.", + "prioritySet":"Määra pileti prioriteet.", + "priorityValue":"Kanali prioriteet.", + "priorityGet":"Hangi pileti prioriteet.", + "priorityList":"Hangi nimekiri kõigist piletitest ja nende prioriteedi olekust.", + "transfer":"Anna pileti omand üle ühelt kasutajalt teisele.", + "transferUser":"Kasutaja, kellele omand üle anda." }, "helpMenu":{ "help":"Hankige kõigi saadaolevate käskude loend.", @@ -452,7 +532,16 @@ "autocloseDisable":"Keela sellel piletil automaatne sulgemine.", "autocloseEnable":"Luba sellel piletil automaatne sulgemine.", "autodeleteDisable":"Keela sellel piletil automaatne kustutamine.", - "autodeleteEnable":"Luba sellel piletil automaatne kustutamine." + "autodeleteEnable":"Luba sellel piletil automaatne kustutamine.", + "categories":{ + "general":"Üldised Käsud", + "basicTicket":"Põhilised Piletikäsud", + "advancedTicket":"Täpsemad Piletikäsud", + "userTicket":"Kasutaja Piletikäsud", + "admin":"Admini Käsud", + "advanced":"Täpsemad Käsud", + "extra":"Lisakäsud" + } }, "stats":{ "scopes":{ @@ -460,7 +549,8 @@ "system":"Süsteemi statistika", "user":"Kasutaja statistika", "ticket":"Piletistatistika", - "participants":"Osalejad" + "participants":"Osalejad", + "messages":"Sõnumid" }, "properties":{ "ticketsCreated":"Pileteid loodud", @@ -472,7 +562,47 @@ "ticketsPinned":"Pileteid kinnitatud", "ticketsMoved":"Pileteid teisaldatud", "usersBlacklisted":"Kasutajaid mustas nimekirjas", - "transcriptsCreated":"Loodud ärakirjad" + "transcriptsCreated":"Loodud ärakirjad", + "ticketsAutodeleted":"Piletid Automaatselt Kustutatud", + "ticketsTransferred":"Piletid Üle Antud", + "ticketVolume":"Piletite Arv", + "averageTickets":"Keskmine Piletite Arv/Kasutaja", + "currentTickets":"Praegused Piletid", + "age":"Pileti Vanus", + "responseTime":"Reageerimisaeg", + "resolutionTime":"Lahendusaeg", + "createdOn":"Loodud", + "createdBy":"Looja", + "closedOn":"Suletud", + "closedBy":"Sulgenud", + "claimedOn":"Võetud", + "claimedBy":"Võtnud", + "pinnedOn":"Kinnitatud", + "pinnedBy":"Kinnitanud", + "deletedOn":"Kustutatud", + "deletedBy":"Kustutanud" + }, + "roles":{ + "developer":"Arendaja", + "serverOwner":"Serveri Omanik", + "serverAdmin":"Serveri Administraator", + "moderator":"Moderaatorite Meeskond", + "support":"Tugimeeskond", + "member":"Liige" } + }, + "panel":{ + "selectTicket":"Vali oma pilet", + "selectRole":"Vali oma roll", + "selectOption":"Vali oma valik" + }, + "priorities":{ + "urgent":"Kiireloomuline", + "veryHigh":"Väga Kõrge", + "high":"Kõrge", + "normal":"Tavaline", + "low":"Madal", + "veryLow":"Väga Madal", + "none":"Puudub" } } \ No newline at end of file diff --git a/languages/finnish.json b/languages/finnish.json index e3a4589..3ccba5a 100644 --- a/languages/finnish.json +++ b/languages/finnish.json @@ -2,7 +2,7 @@ "_TRANSLATION":{ "otversion":"v4.1.0", "translators":["iamnotmega","ChatGPT"], - "lastedited":"21/10/2024", + "lastedited":"09/11/2025", "language":"Finnish", "automated":true }, @@ -31,7 +31,15 @@ "stringContains":"Tämän merkkijonon täytyy sisältää {0}!", "stringChoices":"Tämä merkkijono voi olla vain yksi seuraavista arvoista: {0}!", "stringRegex":"Tämä merkkijono on virheellinen!", - + "stringInvertedContains":"Tämä merkkijono ei saa sisältää {0}!", + "stringLowercase":"Tämä merkkijono on kirjoitettava vain pienillä kirjaimilla!", + "stringUppercase":"Tämä merkkijono on kirjoitettava vain isoilla kirjaimilla!", + "stringSpecialCharacters":"Tämä merkkijono ei saa sisältää erikoismerkkejä! (vain a–z, 0–9 ja välilyönti)", + "stringNoSpaces":"Tämä merkkijono ei saa sisältää välilyöntejä!", + "stringCapitalWord":"On suositeltavaa, että jokainen sana tässä merkkijonossa alkaa isolla kirjaimella!", + "stringCapitalSentence":"Näyttää siltä, että jotkut lauseet tässä merkkijonossa eivät ala isolla kirjaimella!", + "stringPunctuation":"Näyttää siltä, että tämän merkkijonon lause ei pääty välimerkkiin!", + "numberTooShort":"Tämä numero ei voi olla lyhyempi kuin {0} merkkiä!", "numberTooLong":"Tämä numero ei voi olla pidempi kuin {0} merkkiä!", "numberLengthInvalid":"Tämän numeron on oltava {0} merkkiä pitkä!", @@ -48,10 +56,12 @@ "numberNegative":"Tämä luku ei voi olla negatiivinen!", "numberPositive":"Tämä luku ei voi olla positiivinen!", "numberZero":"Tämä luku ei voi olla nolla!", - + "numberNan":"Tämä numero ei voi olla NaN (ei numero)!", + "numberInvertedContains":"Tämä numero ei saa sisältää {0}!", + "booleanTrue":"Tämä boolean ei voi olla totta!", "booleanFalse":"Tämä boolean ei voi olla väärä!", - + "arrayEmptyDisabled":"Tämä taulukko ei saa olla tyhjä!", "arrayEmptyRequired":"Tämän taulukon on oltava tyhjä!", "arrayTooShort":"Tämän taulukon pituuden on oltava vähintään {0}!", @@ -59,7 +69,7 @@ "arrayLengthInvalid":"Tämän taulukon pituuden on oltava {0}!", "arrayInvalidTypes":"Tämä matriisi voi sisältää vain seuraavat tyypit: {0}!", "arrayDouble":"Tämä matriisi ei salli samaa arvoa kahdesti!", - + "discordInvalidId":"Tämä on virheellinen discord {0} -tunnus!", "discordInvalidIdOptions":"Tämä on virheellinen discord {0} -tunnus! Voit myös käyttää jotakin seuraavista: {1}!", "discordInvalidToken":"Tämä on virheellinen discord tunnus (syntaktisesti)!", @@ -76,7 +86,7 @@ "urlInvalidPath":"Tällä URL-osoitteella on virheellinen polku!", "idNotUnique":"Tämä tunnus ei ole ainutlaatuinen, käytä sen sijaan toista tunnusta!", "idNonExistent":"Tunnusta {0} ei ole olemassa!", - + "invalidType":"Tämän ominaisuuden on oltava tyyppiä: {0}!", "propertyMissing":"Ominaisuus {0} puuttuu tästä objektista!", "propertyOptional":"Ominaisuus {0} on valinnainen tässä objektissa!", @@ -84,12 +94,13 @@ "nullInvalid":"Tämä omaisuus ei voi olla tyhjä!", "switchInvalidType":"Tämän on oltava jokin seuraavista: {0}!", "objectSwitchInvalid":"Tämän objektin on oltava jokin seuraavista tyypeistä: {0}!", - + "invalidLanguage":"Tämä on virheellinen kieli!", "invalidButton":"Tässä painikkeessa on oltava vähintään {0} tai {1}!", "unusedOption":"Vaihtoehtoa {0} ei käytetä missään!", "unusedQuestion":"Kysymystä {0} ei käytetä missään!", - "dropdownOption":"Paneeli, jossa pudotusvalikko on käytössä, voi sisältää vain \"lippu\"-tyyppisiä vaihtoehtoja!" + "dropdownOption":"Paneeli, jossa pudotusvalikko on käytössä, voi sisältää vain \"lippu\"-tyyppisiä vaihtoehtoja!", + "customInvalidVersion":"Määritetty versio kokoonpanossasi ei vastaa! Varmista, että olet päivittänyt kokoonpanon uusimpaan versioon!" } }, "actions":{ @@ -122,7 +133,7 @@ "move":"Lippu siirretty", "add":"Lipun käyttäjä lisätty", "remove":"Lipun käyttäjä poistettu", - + "help":"Käytettävissä olevat komennot", "statsReset":"Nollaa tilastot", "blacklistAdd":"Käyttäjä mustalla listalla", @@ -132,14 +143,20 @@ "blacklistAddDm":"Lisätty mustalle listalle", "blacklistRemoveDm":"Poistettu mustalta listalta", "clear":"Liput tyhjennetty", + "clearTickets":"Tyhjennä Tiketit", "roles":"Roolit päivitetty", - + "autoclose":"Lippu suljettu automaattisesti", "autocloseEnabled":"Automaattinen sulkeminen käytössä", "autocloseDisabled":"Automaattinen sulkeminen pois käytöstä", "autodelete":"Lippu poistettu automaattisesti", "autodeleteEnabled":"Automaattinen poisto käytössä", - "autodeleteDisabled":"Automaattinen poisto poissa käytöstä" + "autodeleteDisabled":"Automaattinen poisto poissa käytöstä", + + "topicSet":"Aihe Muutettu", + "prioritySet":"Prioriteetti Muutettu", + "priorityGet":"Tiketin Prioriteetti", + "transfer":"Tiketti Siirretty" }, "descriptions":{ "create":"Lippusi on luotu. Napsauta alla olevaa painiketta päästäksesi siihen!", @@ -154,7 +171,7 @@ "move":"Lippu on siirretty kohteeseen {0} onnistuneesti!", "add":"{0} on lisätty lippuun onnistuneesti!", "remove":"{0} on poistettu lipusta onnistuneesti!", - + "helpExplanation":"`` => pakollinen parametri\n`[nimi]` => valinnainen parametri", "statsReset":"Bottitilastot on nollattu onnistuneesti!", "statsError":"Lipputilastoja ei voi tarkastella!\n{0} ei ole lippu!", @@ -167,7 +184,7 @@ "clearVerify":"Haluatko varmasti poistaa useita lippuja? Tätä toimintoa ei voi kumota!", "clearReady":"{0} lippua on poistettu onnistuneesti!", "rolesEmpty":"Rooleja ei ole päivitetty!", - + "autocloseLeave":"Tämä lippu on suljettu automaattisesti, koska luoja poistui palvelimelta!", "autocloseTimeout":"Tämä lippu on suljettu automaattisesti, koska se on ollut passiivinen yli `{0}h`!", "autodeleteLeave":"Tämä lippu on poistettu automaattisesti, koska sisällöntuottaja poistui palvelimelta!", @@ -176,11 +193,16 @@ "autocloseDisabled":"Automaattinen sulkeminen on poistettu käytöstä tässä lipussa! Se ei enää sulkeudu automaattisesti!", "autodeleteEnabled":"Automaattinen poisto on otettu käyttöön tässä lipussa! Se poistetaan, kun se on passiivinen yli `{0} päivään`!", "autodeleteDisabled":"Automaattinen poisto on poistettu käytöstä tässä lipussa! Sitä ei enää poisteta automaattisesti!", - + "ticketMessageLimit":"Voit luoda vain {0} lippua samanaikaisesti!", "ticketMessageAutoclose":"Tämä lippu suljetaan automaattisesti, kun se ei ole aktiivinen {0} tuntia!", "ticketMessageAutodelete":"Tämä lippu poistetaan automaattisesti, kun se ei ole aktiivinen {0} päivään!", - "panelReady":"Löydät paneelin alta! Tämä viesti voidaan nyt poistaa!" + "panelReady":"Paneeli on saatavilla jatkoviestissä!\nTämä viesti voidaan nyt poistaa!", + + "topicSet":"Kanavan aihe on vaihdettu käyttäjän {0} toimesta onnistuneesti!", + "prioritySet":"Tiketin prioriteetti on vaihdettu arvoksi {0} käyttäjän {1} toimesta onnistuneesti!", + "priorityGet":"Tämän tiketin nykyinen prioriteetti on {0}.", + "transfer":"Tiketin omistus on siirretty käyttäjältä {0} käyttäjälle {1} käyttäjän {2} toimesta onnistuneesti!" }, "modal":{ "closePlaceholder":"Miksi suljit tämän lipun?", @@ -215,12 +237,19 @@ "addDm":"{0} on lisätty lippuusi palvelimellamme!", "removeLog":"{1} on poistanut {0} tästä lipusta!", "removeDm":"{0} on poistettu lipustasi palvelimellamme!", - + "blacklistAddLog":"{1} lisäsi käyttäjän {0} mustalle listalle!", "blacklistRemoveLog":"{1} poisti käyttäjän {0} mustalta listalta!", "blacklistAddDm":"Sinut on mustalla listalla palvelimellamme! Tästä eteenpäin et voi luoda lippua!", "blacklistRemoveDm":"Sinut on poistettu palvelimemme mustalta listalta! Nyt voit luoda lippuja uudelleen!", - "clearLog":"{1} on poistanut {0} lippua!" + "clearLog":"{1} on poistanut {0} lippua!", + + "transferLog":"Tämän tiketin omistus on siirretty käyttäjältä {0} käyttäjälle {1} käyttäjän {2} toimesta!", + "transferDm":"Tiketin omistus on siirretty käyttäjältä {0} käyttäjälle {1} palvelimellamme!", + "prioritySetLog":"Tämän tiketin prioriteetti on vaihdettu arvoksi {0} käyttäjän {1} toimesta!", + "prioritySetDm":"Tiketin prioriteetti on vaihdettu arvoksi {0} palvelimellamme!", + "roleUpdateLog":"{0} on päivittänyt roolinsa!", + "roleUpdateDm":"Roolisi palvelimellamme on päivitetty!" } }, "transcripts":{ @@ -229,7 +258,7 @@ "ready":"Transkriptio luotu", "textFileDescription":"Tämä on poistetun lipun tekstikopio!", "htmlProgress":"Odota, kun tätä html-transkriptiota käsitellään...", - + "createdChannel":"Uusi {0}-transkriptio on luotu palvelimelle!", "createdCreator":"Yhdelle lipullesi on luotu uusi transkriptio {0}!", "createdParticipant":"Uusi {0}-transkriptio on luotu yhteen lipuista, joihin osallistuit!", @@ -241,7 +270,19 @@ "retry":"Yritä uudelleen", "continue":"Poista ilman transkriptiota", "backup":"Luo varmuuskopio", - "error":"Jotain meni pieleen, kun yritettiin luoda transkriptiota. Mitä haluaisit tehdä? Tätä lippua ei poisteta, ennen kuin napsautat jotakin näistä painikkeista. " + "error":"Jotain meni pieleen, kun yritettiin luoda transkriptiota. Mitä haluaisit tehdä? Tätä lippua ei poisteta, ennen kuin napsautat jotakin näistä painikkeista. ", + "title":"Transkriptiovirhe" + }, + "text":{ + "messagesTitle":"VIESTIT", + "embedTitle":"UPOTUS", + "fileTitle":"TIEDOSTO", + "fieldsTitle":"KENTÄT", + "reactionsTitle":"REAKTIOT", + "statsTitle":"TILASTOT", + "emptyContent":"", + "noTitle":"", + "noDesc":"" } }, "errors":{ @@ -257,7 +298,8 @@ "unknownPanel":"Tuntematon paneeli", "notInGuild":"Ei Palvelimessa", "channelRename":"Kanavaa ei voi nimetä uudelleen", - "busy":"Lippu on varattu" + "busy":"Lippu on varattu", + "permissionError":"Käyttöoikeusvirhe" }, "descriptions":{ "askForInfo":"Ota yhteyttä tämän botin omistajaan saadaksesi lisätietoja!", @@ -280,7 +322,10 @@ "notInGuild":"Tämä {0} ei toimi DM:ssä! Ole hyvä ja yritä uudelleen palvelimella!", "channelRename":"Discordin nopeusrajoitusten vuoksi botin on tällä hetkellä mahdotonta nimetä kanavaa uudelleen. Kanava nimetään automaattisesti uudelleen 10 minuutin kuluttua, jos bottia ei käynnistetä uudelleen.", "channelRenameSource":"Tämän virheen lähde on: {0}", - "busy":"Tätä {0} ei voi käyttää! Botti käsittelee lippua parhaillaan. Yritä uudelleen muutaman sekunnin kuluttua!" + "busy":"Tätä {0} ei voi käyttää! Botti käsittelee lippua parhaillaan. Yritä uudelleen muutaman sekunnin kuluttua!", + "closeBeforeMessage":"Tätä tikettiä ei voi sulkea/poistaa ennen kuin käyttäjä on lähettänyt viestin.", + "closeBeforeAdminMessage":"Tätä tikettiä ei voi sulkea/poistaa ennen kuin tikettien ylläpitäjä tai tukihenkilö on lähettänyt viestin.", + "unableToCreateTicket":"Et voi luoda tikettiä." }, "optionInvalidReasons":{ "stringRegex":"Arvo ei vastaa mallia!", @@ -331,7 +376,6 @@ "added":"Lisätty", "removed":"Poistettu", "filter":"Suodattaa", - "claimedBy":"Vaatimuksen tekijä: {0}", "method":"Menetelmä", "type":"Tyyppi", "blacklisted":"Mustalla listalla", @@ -352,10 +396,30 @@ "version":"Versio", "name":"Nimi", "role":"Rooli", - "status":"Status", + "status":"Tila", "claimed":"Väitetty", "pinned":"Kiinnitetty", - "creationDate":"Luontipäivämäärä" + "creationDate":"Luontipäivämäärä", + + "noone":"Ei Kukaan", + "open":"Avoin", + "closed":"Suljettu", + "priority":"Prioriteetti", + "participants":"Osallistujat", + "yes":"Kyllä", + "no":"Ei", + "option":"Vaihtoehto", + "topic":"Aihe", + "uptime":"Järjestelmän Käyttöaika", + "messages":"Viestit", + "embeds":"Upotukset", + "files":"Tiedostot", + "components":"Komponentit", + "cooldown":"Jäähy", + "maxTickets":"Maksimi Tiketit", + "admins":"Ylläpitäjät", + "roles":"Roolit", + "size":"Koko" }, "lowercase":{ "text":"teksti", @@ -384,6 +448,7 @@ "unclaim":"Peruuta lippu.", "pin":"Kiinnitä lippu.", "unpin":"Irrota lippu.", + "move":"Siirrä lippu.", "moveId":"Sen vaihtoehdon tunniste, johon haluat siirtyä.", "rename":"Nimeä lippu uudelleen.", @@ -392,6 +457,7 @@ "addUser":"Lisättävä käyttäjä.", "remove":"Poista käyttäjä lipusta.", "removeUser":"Poistettava käyttäjä.", + "blacklist":"Hallitse lippujen mustaa listaa.", "blacklistView":"Näytä luettelo nykyisestä mustasta listasta.", "blacklistAdd":"Lisää käyttäjä mustalle listalle.", @@ -405,6 +471,7 @@ "statsUserUser":"Käyttäjä katsella.", "statsTicket":"Tarkastele lipun tilastoja palvelimella.", "statsTicketTicket":"Lippu katsottavaksi.", + "clear":"Poista useita lippuja samanaikaisesti.", "clearFilter":"Lippujen tyhjennyssuodatin.", "clearFilters":{ @@ -417,6 +484,7 @@ "unpin":"Irrotettu", "autoclose":"Automaattisuljettu" }, + "autoclose":"Hallitse automaattista sulkemista lipussa.", "autocloseDisable":"Poista automaattinen sulkeminen käytöstä tässä lipussa.", "autocloseEnable":"Ota automaattinen sulkeminen käyttöön tässä lipussa.", @@ -424,7 +492,19 @@ "autodelete":"Hallitse automaattista poistoa lipussa.", "autodeleteDisable":"Poista automaattinen poisto käytöstä tässä lipussa.", "autodeleteEnable":"Ota automaattinen poisto käyttöön tässä lipussa.", - "autodeleteEnableTime":"Kuinka monta päivää tämän lipun on oltava passiivinen, jotta se voidaan poistaa." + "autodeleteEnableTime":"Kuinka monta päivää tämän lipun on oltava passiivinen, jotta se voidaan poistaa.", + + "topic":"Hallitse tikettikanavan aihetta.", + "topicSet":"Aseta tikettikanavan aihe.", + "topicValue":"Kanavan uusi aihe.", + "topicList":"Hae lista kaikista tiketeistä niiden aiheen ja tilastojen kanssa.", + "priority":"Hallitse tiketin prioriteettia.", + "prioritySet":"Aseta tiketin prioriteetti.", + "priorityValue":"Kanavan prioriteetti.", + "priorityGet":"Hae tiketin prioriteetti.", + "priorityList":"Hae lista kaikista tiketeistä ja niiden prioriteettitilasta.", + "transfer":"Siirrä tiketin omistus käyttäjältä toiselle.", + "transferUser":"Käyttäjä, jolle omistus siirretään." }, "helpMenu":{ "help":"Hanki luettelo kaikista käytettävissä olevista komennoista.", @@ -452,7 +532,16 @@ "autocloseDisable":"Poista automaattinen sulkeminen käytöstä tässä lipussa.", "autocloseEnable":"Ota automaattinen sulkeminen käyttöön tässä lipussa.", "autodeleteDisable":"Poista automaattinen poisto käytöstä tässä lipussa.", - "autodeleteEnable":"Ota automaattinen poisto käyttöön tässä lipussa." + "autodeleteEnable":"Ota automaattinen poisto käyttöön tässä lipussa.", + "categories":{ + "general":"Yleiset Komennot", + "basicTicket":"Perus Tikettikomennot", + "advancedTicket":"Edistyneet Tikettikomennot", + "userTicket":"Käyttäjän Tikettikomennot", + "admin":"Ylläpitäjäkomennot", + "advanced":"Edistyneet Komennot", + "extra":"Lisäkomennot" + } }, "stats":{ "scopes":{ @@ -460,7 +549,8 @@ "system":"Järjestelmätilastot", "user":"Käyttäjätilastot", "ticket":"Lipputilastot", - "participants":"Osallistujat" + "participants":"Osallistujat", + "messages":"Viestit" }, "properties":{ "ticketsCreated":"Liput luotu", @@ -472,7 +562,47 @@ "ticketsPinned":"Liput kiinni", "ticketsMoved":"Liput siirretty", "usersBlacklisted":"Käyttäjät mustalla listalla", - "transcriptsCreated":"Transkriptiot luotu" + "transcriptsCreated":"Transkriptiot luotu", + "ticketsAutodeleted":"Tiketit Automaattisesti Poistettu", + "ticketsTransferred":"Tiketit Siirretty", + "ticketVolume":"Tikettilukumäärä", + "averageTickets":"Keskimääräiset Tiketit/Käyttäjä", + "currentTickets":"Nykyiset Tiketit", + "age":"Tiketin Ikä", + "responseTime":"Vastausaika", + "resolutionTime":"Ratkaisuaika", + "createdOn":"Luotu", + "createdBy":"Luonut", + "closedOn":"Suljettu", + "closedBy":"Sulkenut", + "claimedOn":"Otettu Vastaan", + "claimedBy":"Vastaanottanut", + "pinnedOn":"Kiinnitetty", + "pinnedBy":"Kiinnittänyt", + "deletedOn":"Poistettu", + "deletedBy":"Poistanut" + }, + "roles":{ + "developer":"Kehittäjä", + "serverOwner":"Palvelimen Omistaja", + "serverAdmin":"Palvelimen Ylläpitäjä", + "moderator":"Moderaattoritiimi", + "support":"Tukitiimi", + "member":"Jäsen" } + }, + "panel":{ + "selectTicket":"Valitse tikettisi", + "selectRole":"Valitse roolisi", + "selectOption":"Valitse vaihtoehtosi" + }, + "priorities":{ + "urgent":"Kiireellinen", + "veryHigh":"Erittäin Korkea", + "high":"Korkea", + "normal":"Normaali", + "low":"Matala", + "veryLow":"Erittäin Matala", + "none":"Ei Mikään" } } \ No newline at end of file diff --git a/languages/hindi.json b/languages/hindi.json index 78e4577..439729f 100644 --- a/languages/hindi.json +++ b/languages/hindi.json @@ -1,8 +1,8 @@ { "_TRANSLATION":{ "otversion":"v4.1.0", - "translators":["an_developer"], - "lastedited":"14/12/2024", + "translators":["challenger_nova"], + "lastedited":"09/11/2025", "language":"Hindi", "automated":false }, @@ -31,7 +31,15 @@ "stringContains":"इस स्ट्रिंग में {0} होना आवश्यक है!", "stringChoices":"यह स्ट्रिंग केवल निम्नलिखित मानों में से एक हो सकती है: {0}!", "stringRegex":"यह स्ट्रिंग अमान्य है!", - + "stringInvertedContains":"इस स्ट्रिंग में {0} शामिल नहीं होना चाहिए!", + "stringLowercase":"यह स्ट्रिंग केवल छोटे अक्षरों में लिखी जानी चाहिए!", + "stringUppercase":"यह स्ट्रिंग केवल बड़े अक्षरों में लिखी जानी चाहिए!", + "stringSpecialCharacters":"इस स्ट्रिंग में कोई विशेष अक्षर नहीं होना चाहिए! (केवल a-z, 0-9 और स्पेस अनुमत हैं)", + "stringNoSpaces":"इस स्ट्रिंग में स्पेस नहीं होना चाहिए!", + "stringCapitalWord":"यह अनुशंसा की जाती है कि इस स्ट्रिंग के प्रत्येक शब्द की शुरुआत बड़े अक्षर से हो!", + "stringCapitalSentence":"ऐसा लगता है कि इस स्ट्रिंग में कुछ वाक्य बड़े अक्षर से शुरू नहीं होते हैं!", + "stringPunctuation":"ऐसा लगता है कि इस स्ट्रिंग का वाक्य विराम चिह्न के साथ समाप्त नहीं होता है!", + "numberTooShort":"यह संख्या {0} वर्णों से छोटी नहीं हो सकती!", "numberTooLong":"यह संख्या {0} वर्णों से अधिक लंबी नहीं हो सकती!", "numberLengthInvalid":"यह संख्या {0} वर्ण लंबी होनी चाहिए!", @@ -48,10 +56,12 @@ "numberNegative":"यह संख्या ऋणात्मक नहीं हो सकती!", "numberPositive":"यह संख्या सकारात्मक नहीं हो सकती!", "numberZero":"यह संख्या शून्य नहीं हो सकती!", - + "numberNan":"यह संख्या NaN (Not A Number) नहीं हो सकती!", + "numberInvertedContains":"इस संख्या में {0} शामिल नहीं होना चाहिए!", + "booleanTrue":"यह बूलियन सत्य नहीं हो सकता!", "booleanFalse":"यह बूलियन झूठा नहीं हो सकता!", - + "arrayEmptyDisabled":"इस सरणी को खाली रहने की अनुमति नहीं है!", "arrayEmptyRequired":"यह सरणी खाली होनी आवश्यक है!", "arrayTooShort":"इस सरणी की लंबाई कम से कम {0} होनी चाहिए!", @@ -59,7 +69,7 @@ "arrayLengthInvalid":"इस सरणी की लंबाई {0} होनी चाहिए!", "arrayInvalidTypes":"इस सरणी में केवल निम्नलिखित प्रकार हो सकते हैं: {0}!", "arrayDouble":"यह सरणी एक ही मान को दो बार अनुमति नहीं देती है!", - + "discordInvalidId":"यह एक अमान्य कलह {0} आईडी है!", "discordInvalidIdOptions":"यह एक अमान्य कलह {0} आईडी है! ", "discordInvalidToken":"यह एक अमान्य कलह टोकन है (वाक्यविन्यास की दृष्टि से)!", @@ -76,7 +86,7 @@ "urlInvalidPath":"इस यूआरएल में एक अमान्य पथ है!", "idNotUnique":"यह आईडी अद्वितीय नहीं है, इसके बजाय किसी अन्य आईडी का उपयोग करें!", "idNonExistent":"आईडी {0} मौजूद नहीं है!", - + "invalidType":"यह संपत्ति इस प्रकार की होनी चाहिए: {0}!", "propertyMissing":"इस ऑब्जेक्ट में संपत्ति {0} गायब है!", "propertyOptional":"इस ऑब्जेक्ट में संपत्ति {0} वैकल्पिक है!", @@ -84,12 +94,13 @@ "nullInvalid":"यह संपत्ति शून्य नहीं हो सकती!", "switchInvalidType":"इसे निम्न प्रकारों में से एक होना आवश्यक है: {0}!", "objectSwitchInvalid":"यह ऑब्जेक्ट निम्न प्रकारों में से एक होना चाहिए: {0}!", - + "invalidLanguage":"यह एक अमान्य भाषा है!", "invalidButton":"इस बटन पर कम से कम {0} या {1} होना आवश्यक है!", "unusedOption":"विकल्प {0} का उपयोग कहीं भी नहीं किया जाता है!", "unusedQuestion":"प्रश्न {0} का प्रयोग कहीं भी नहीं किया गया है!", - "dropdownOption":"ड्रॉपडाउन सक्षम पैनल में केवल 'टिकट' प्रकार के विकल्प हो सकते हैं!" + "dropdownOption":"ड्रॉपडाउन सक्षम पैनल में केवल 'टिकट' प्रकार के विकल्प हो सकते हैं!", + "customInvalidVersion":"आपके कॉन्फ़िग में निर्दिष्ट संस्करण मेल नहीं खाता! कृपया सुनिश्चित करें कि आपने कॉन्फ़िग को नवीनतम संस्करण में अपडेट किया है!" } }, "actions":{ @@ -122,7 +133,7 @@ "move":"टिकट स्थानांतरित", "add":"टिकट उपयोगकर्ता जोड़ा गया", "remove":"टिकट उपयोगकर्ता हटा दिया गया", - + "help":"उपलब्ध आदेश", "statsReset":"आँकड़े रीसेट करें", "blacklistAdd":"उपयोगकर्ता को ब्लैकलिस्ट किया गया", @@ -132,14 +143,20 @@ "blacklistAddDm":"ब्लैकलिस्ट में जोड़ा गया", "blacklistRemoveDm":"काली सूची से हटाया गया", "clear":"टिकट साफ़ हो गए", + "clearTickets":"टिकट साफ़ करें", "roles":"भूमिकाएँ अद्यतन की गईं", - + "autoclose":"टिकट स्वतः बंद", "autocloseEnabled":"स्वत: बंद सक्षम", "autocloseDisabled":"स्वत: बंद अक्षम", "autodelete":"टिकट स्वतः हटा दिया गया", "autodeleteEnabled":"स्वतः हटाना सक्षम", - "autodeleteDisabled":"स्वतः हटाना अक्षम" + "autodeleteDisabled":"स्वतः हटाना अक्षम", + + "topicSet":"विषय बदला गया", + "prioritySet":"प्राथमिकता बदली गई", + "priorityGet":"टिकट प्राथमिकता", + "transfer":"टिकट स्थानांतरित किया गया" }, "descriptions":{ "create":"आपका टिकट बन गया है. ", @@ -154,7 +171,7 @@ "move":"टिकट को सफलतापूर्वक {0} पर ले जाया गया है!", "add":"{0} को टिकट में सफलतापूर्वक जोड़ दिया गया है!", "remove":"{0} को टिकट से सफलतापूर्वक हटा दिया गया है!", - + "helpExplanation":"`` => आवश्यक पैरामीटर\n", "statsReset":"बॉट आँकड़े सफलतापूर्वक रीसेट कर दिए गए हैं!", "statsError":"टिकट आँकड़े देखने में असमर्थ!\n", @@ -167,7 +184,7 @@ "clearVerify":"क्या आप वाकई एकाधिक टिकट हटाना चाहते हैं?\n", "clearReady":"{0} टिकट सफलतापूर्वक हटा दिए गए हैं!", "rolesEmpty":"कोई भूमिका अद्यतन नहीं की गई है!", - + "autocloseLeave":"यह टिकट स्वतः बंद हो गया है क्योंकि निर्माता ने सर्वर छोड़ दिया है!", "autocloseTimeout":"यह टिकट स्वतः बंद कर दिया गया है क्योंकि यह `{0}h` से अधिक समय से निष्क्रिय है!", "autodeleteLeave":"यह टिकट स्वतः हटा दिया गया है क्योंकि निर्माता ने सर्वर छोड़ दिया है!", @@ -176,11 +193,16 @@ "autocloseDisabled":"इस टिकट में ऑटोक्लोज़ अक्षम कर दिया गया है!\n", "autodeleteEnabled":"इस टिकट में ऑटोडिलीट सक्षम कर दिया गया है!\n", "autodeleteDisabled":"इस टिकट में ऑटोडिलीट अक्षम कर दिया गया है!\n", - + "ticketMessageLimit":"आप एक ही समय में केवल {0} टिकट बना सकते हैं!", "ticketMessageAutoclose":"{0}घंटे तक निष्क्रिय रहने पर यह टिकट स्वतः बंद हो जाएगा!", "ticketMessageAutodelete":"{0} दिनों तक निष्क्रिय रहने पर यह टिकट स्वतः हटा दिया जाएगा!", - "panelReady":"आप नीचे पैनल पा सकते हैं!\n" + "panelReady":"पैनल फॉलोअप संदेश में उपलब्ध है!\nयह संदेश अब हटाया जा सकता है!", + + "topicSet":"चैनल का विषय {0} द्वारा सफलतापूर्वक बदल दिया गया है!", + "prioritySet":"टिकट की प्राथमिकता {1} द्वारा {0} में सफलतापूर्वक बदल दी गई है!", + "priorityGet":"इस टिकट की वर्तमान प्राथमिकता {0} है।", + "transfer":"टिकट का स्वामित्व {0} से {1} में {2} द्वारा सफलतापूर्वक स्थानांतरित कर दिया गया है!" }, "modal":{ "closePlaceholder":"आपने यह टिकट क्यों बंद कर दिया?", @@ -215,12 +237,19 @@ "addDm":"हमारे सर्वर में आपके टिकट में {0} जोड़ दिया गया है!", "removeLog":"{1} द्वारा इस टिकट से {0} हटा दिया गया है!", "removeDm":"हमारे सर्वर में आपके टिकट से {0} हटा दिया गया है!", - + "blacklistAddLog":"{0} को {1} द्वारा काली सूची में डाल दिया गया था!", "blacklistRemoveLog":"{0} को {1} द्वारा काली सूची से हटा दिया गया था!", "blacklistAddDm":"आपको हमारे सर्वर में काली सूची में डाल दिया गया है!\n", "blacklistRemoveDm":"आपको हमारे सर्वर में ब्लैकलिस्ट से हटा दिया गया है!\n", - "clearLog":"{0} टिकटें {1} द्वारा हटा दी गई हैं!" + "clearLog":"{0} टिकटें {1} द्वारा हटा दी गई हैं!", + + "transferLog":"इस टिकट का स्वामित्व {0} से {1} में {2} द्वारा स्थानांतरित किया गया है!", + "transferDm":"आपके टिकट का स्वामित्व हमारे सर्वर में {0} से {1} में स्थानांतरित कर दिया गया है!", + "prioritySetLog":"इस टिकट की प्राथमिकता {1} द्वारा {0} में बदल दी गई है!", + "prioritySetDm":"आपके टिकट की प्राथमिकता हमारे सर्वर में {0} में बदल दी गई है!", + "roleUpdateLog":"{0} ने अपनी भूमिकाएँ अपडेट की हैं!", + "roleUpdateDm":"आपकी भूमिकाएँ हमारे सर्वर में अपडेट कर दी गई हैं!" } }, "transcripts":{ @@ -241,7 +270,19 @@ "retry":"पुन: प्रयास करें", "continue":"प्रतिलेख के बिना हटाएँ", "backup":"बैकअप ट्रांस्क्रिप्ट बनाएं", - "error":"प्रतिलेख बनाने का प्रयास करते समय कुछ गलत हो गया।\n" + "error":"प्रतिलेख बनाने का प्रयास करते समय कुछ गलत हो गया।\n", + "title":"ट्रांसक्रिप्ट त्रुटि" + }, + "text":{ + "messagesTitle":"संदेश", + "embedTitle":"एम्बेड", + "fileTitle":"फ़ाइल", + "fieldsTitle":"फ़ील्ड्स", + "reactionsTitle":"प्रतिक्रियाएँ", + "statsTitle":"आँकड़े", + "emptyContent":"<सामग्री खाली है>", + "noTitle":"<कोई शीर्षक नहीं>", + "noDesc":"<कोई विवरण नहीं>" } }, "errors":{ @@ -257,7 +298,8 @@ "unknownPanel":"अज्ञात पैनल", "notInGuild":"सर्वर में नहीं", "channelRename":"चैनल का नाम बदलने में असमर्थ", - "busy":"टिकट व्यस्त है" + "busy":"टिकट व्यस्त है", + "permissionError":"अनुमति त्रुटि" }, "descriptions":{ "askForInfo":"अधिक जानकारी के लिए इस बॉट के मालिक से संपर्क करें!", @@ -280,7 +322,10 @@ "notInGuild":"यह {0} डीएम में काम नहीं करता! ", "channelRename":"विवाद की दर सीमा के कारण, वर्तमान में बॉट के लिए चैनल का नाम बदलना असंभव है। ", "channelRenameSource":"इस त्रुटि का स्रोत है: {0}", - "busy":"इस {0} का उपयोग करने में असमर्थ!\n" + "busy":"इस {0} का उपयोग करने में असमर्थ!\n", + "closeBeforeMessage":"किसी उपयोगकर्ता द्वारा संदेश भेजे जाने से पहले इस टिकट को बंद/हटाया नहीं जा सकता।", + "closeBeforeAdminMessage":"टिकट एडमिन या सपोर्ट सदस्य द्वारा संदेश भेजे जाने से पहले इस टिकट को बंद/हटाया नहीं जा सकता।", + "unableToCreateTicket":"आप टिकट बनाने में असमर्थ हैं।" }, "optionInvalidReasons":{ "stringRegex":"मान पैटर्न से मेल नहीं खाता!", @@ -331,7 +376,6 @@ "added":"जोड़ा", "removed":"निकाला गया", "filter":"फ़िल्टर", - "claimedBy":"{0} द्वारा दावा किया गया", "method":"तरीका", "type":"प्रकार", "blacklisted":"ब्लैक लिस्ट किया", @@ -355,7 +399,27 @@ "status":"स्थिति", "claimed":"दावा किया", "pinned":"पिन की गई", - "creationDate":"निर्माण तिथि" + "creationDate":"निर्माण तिथि", + + "noone":"कोई नहीं", + "open":"खुला", + "closed":"बंद", + "priority":"प्राथमिकता", + "participants":"प्रतिभागी", + "yes":"हाँ", + "no":"नहीं", + "option":"विकल्प", + "topic":"विषय", + "uptime":"सिस्टम अपटाइम", + "messages":"संदेश", + "embeds":"एम्बेड्स", + "files":"फ़ाइलें", + "components":"घटक", + "cooldown":"कूलडाउन", + "maxTickets":"अधिकतम टिकट", + "admins":"एडमिन", + "roles":"भूमिकाएँ", + "size":"आकार" }, "lowercase":{ "text":"मूलपाठ", @@ -384,6 +448,7 @@ "unclaim":"टिकट का दावा रद्द करें.", "pin":"टिकट पिन करें.", "unpin":"टिकट अनपिन करें.", + "move":"एक टिकट ले जाएँ.", "moveId":"उस विकल्प का पहचानकर्ता जिस पर आप जाना चाहते हैं.", "rename":"टिकट का नाम बदलें.", @@ -392,6 +457,7 @@ "addUser":"उपयोगकर्ता को जोड़ना है.", "remove":"किसी उपयोगकर्ता को टिकट से हटाएँ.", "removeUser":"उपयोगकर्ता को हटाना है.", + "blacklist":"टिकट ब्लैकलिस्ट प्रबंधित करें.", "blacklistView":"वर्तमान ब्लैकलिस्ट की सूची देखें.", "blacklistAdd":"किसी उपयोगकर्ता को काली सूची में जोड़ें.", @@ -405,6 +471,7 @@ "statsUserUser":"देखने के लिए उपयोगकर्ता.", "statsTicket":"सर्वर में टिकट के आँकड़े देखें।", "statsTicketTicket":"देखने के लिए टिकट.", + "clear":"एक ही समय में एकाधिक टिकट हटाएँ।", "clearFilter":"टिकट साफ़ करने के लिए फ़िल्टर.", "clearFilters":{ @@ -417,6 +484,7 @@ "unpin":"अनपिन किया गया", "autoclose":"स्वतः बंद" }, + "autoclose":"किसी टिकट में ऑटोक्लोज़ प्रबंधित करें.", "autocloseDisable":"इस टिकट में ऑटोक्लोज़ अक्षम करें.", "autocloseEnable":"इस टिकट में ऑटोक्लोज़ सक्षम करें।", @@ -424,7 +492,19 @@ "autodelete":"किसी टिकट में स्वत: हटाना प्रबंधित करें.", "autodeleteDisable":"इस टिकट में ऑटोडिलीट अक्षम करें।", "autodeleteEnable":"इस टिकट में ऑटोडिलीट सक्षम करें।", - "autodeleteEnableTime":"इस टिकट को हटाने के लिए उतने दिनों की संख्या निष्क्रिय होनी चाहिए।" + "autodeleteEnableTime":"इस टिकट को हटाने के लिए उतने दिनों की संख्या निष्क्रिय होनी चाहिए।", + + "topic":"टिकट चैनल के विषय का प्रबंधन करें।", + "topicSet":"टिकट चैनल का विषय सेट करें।", + "topicValue":"चैनल का नया विषय।", + "topicList":"सभी टिकटों की सूची प्राप्त करें जिनके विषय और आँकड़े शामिल हैं।", + "priority":"टिकट की प्राथमिकता का प्रबंधन करें।", + "prioritySet":"टिकट की प्राथमिकता सेट करें।", + "priorityValue":"चैनल की प्राथमिकता।", + "priorityGet":"टिकट की प्राथमिकता प्राप्त करें।", + "priorityList":"सभी टिकटों की प्राथमिकता स्थिति की सूची प्राप्त करें।", + "transfer":"टिकट का स्वामित्व एक उपयोगकर्ता से दूसरे को स्थानांतरित करें।", + "transferUser":"जिस उपयोगकर्ता को स्थानांतरित करना है।" }, "helpMenu":{ "help":"सभी उपलब्ध आदेशों की सूची प्राप्त करें.", @@ -452,7 +532,16 @@ "autocloseDisable":"इस टिकट में ऑटोक्लोज़ अक्षम करें.", "autocloseEnable":"इस टिकट में ऑटोक्लोज़ सक्षम करें।", "autodeleteDisable":"इस टिकट में ऑटोडिलीट अक्षम करें।", - "autodeleteEnable":"इस टिकट में ऑटोडिलीट सक्षम करें।" + "autodeleteEnable":"इस टिकट में ऑटोडिलीट सक्षम करें।", + "categories":{ + "general":"सामान्य कमांड्स", + "basicTicket":"मूल टिकट कमांड्स", + "advancedTicket":"उन्नत टिकट कमांड्स", + "userTicket":"उपयोगकर्ता टिकट कमांड्स", + "admin":"एडमिन कमांड्स", + "advanced":"उन्नत कमांड्स", + "extra":"अतिरिक्त कमांड्स" + } }, "stats":{ "scopes":{ @@ -460,7 +549,8 @@ "system":"सिस्टम आँकड़े", "user":"उपयोगकर्ता आँकड़े", "ticket":"टिकट आँकड़े", - "participants":"प्रतिभागियों" + "participants":"प्रतिभागियों", + "messages":"संदेश" }, "properties":{ "ticketsCreated":"टिकट बनाये गये", @@ -472,7 +562,47 @@ "ticketsPinned":"टिकट पिन किये गये", "ticketsMoved":"टिकट ले जाया गया", "usersBlacklisted":"उपयोगकर्ता ब्लैकलिस्टेड", - "transcriptsCreated":"प्रतिलिपियाँ बनाई गईं" + "transcriptsCreated":"प्रतिलिपियाँ बनाई गईं", + "ticketsAutodeleted":"टिकट स्वचालित रूप से हटाए गए", + "ticketsTransferred":"स्थानांतरित टिकट", + "ticketVolume":"टिकट मात्रा", + "averageTickets":"औसत टिकट/उपयोगकर्ता", + "currentTickets":"वर्तमान टिकट", + "age":"टिकट की आयु", + "responseTime":"प्रतिक्रिया समय", + "resolutionTime":"समाधान समय", + "createdOn":"निर्माण तिथि", + "createdBy":"द्वारा बनाया गया", + "closedOn":"बंद करने की तिथि", + "closedBy":"द्वारा बंद किया गया", + "claimedOn":"दावा की गई तिथि", + "claimedBy":"द्वारा दावा किया गया", + "pinnedOn":"पिन की गई तिथि", + "pinnedBy":"द्वारा पिन किया गया", + "deletedOn":"हटाने की तिथि", + "deletedBy":"द्वारा हटाया गया" + }, + "roles":{ + "developer":"डेवलपर", + "serverOwner":"सर्वर स्वामी", + "serverAdmin":"सर्वर एडमिन", + "moderator":"मॉडरेटर टीम", + "support":"सपोर्ट टीम", + "member":"सदस्य" } + }, + "panel":{ + "selectTicket":"अपना टिकट चुनें", + "selectRole":"अपनी भूमिका चुनें", + "selectOption":"अपना विकल्प चुनें" + }, + "priorities":{ + "urgent":"अत्यावश्यक", + "veryHigh":"बहुत उच्च", + "high":"उच्च", + "normal":"सामान्य", + "low":"निम्न", + "veryLow":"बहुत निम्न", + "none":"कोई नहीं" } } \ No newline at end of file diff --git a/languages/indonesian.json b/languages/indonesian.json index f9a43dd..44a64b3 100644 --- a/languages/indonesian.json +++ b/languages/indonesian.json @@ -2,7 +2,7 @@ "_TRANSLATION":{ "otversion":"v4.1.0", "translators":["erxg"], - "lastedited":"25/08/2024", + "lastedited":"09/11/2025", "language":"Indonesian", "automated":false }, @@ -31,7 +31,15 @@ "stringContains":"String ini harus mengandung {0}!", "stringChoices":"String ini hanya dapat berupa salah satu dari nilai berikut ini: {0}!", "stringRegex":"String ini tidak valid!", - + "stringInvertedContains":"Teks ini tidak boleh mengandung {0}!", + "stringLowercase":"Teks ini harus ditulis dalam huruf kecil saja!", + "stringUppercase":"Teks ini harus ditulis dalam huruf kecil besar!", + "stringSpecialCharacters":"Teks ini tidak boleh mengandung karakter khusus apa pun! (Hanya huruf a-z, angka 0-9, dan spasi)", + "stringNoSpaces":"Teks ini tidak boleh mengandung spasi", + "stringCapitalWord":"Disarankan agar setiap kata dalam teks ini dimulai dengan huruf kapital!", + "stringCapitalSentence":"Sepertinya beberapa kalimat dalam teks ini tidak dimulai dengan huruf kapital!", + "stringPunctuation":"Sepertinya kalimat dalam teks ini tidak diakhiri dengan tanda baca!", + "numberTooShort":"Nomor ini tidak boleh lebih pendek dari {0} karakter!", "numberTooLong":"Nomor ini tidak boleh lebih panjang dari {0} karakter!", "numberLengthInvalid":"Nomor ini harus setidaknya {0} karakter!", @@ -48,10 +56,12 @@ "numberNegative":"Nomor ini tidak boleh negatif!", "numberPositive":"Nomor ini tidak boleh positif!", "numberZero":"Nomor ini tidak boleh nol!", - + "numberNan":"Nomor ini tidak boleh NaN (Bukan Angka)!", + "numberInvertedContains":"Nomor ini tidak boleh mengandung {0}!", + "booleanTrue":"Boolean ini tidak boleh benar!", "booleanFalse":"Boolean ini tidak boleh salah!", - + "arrayEmptyDisabled":"Array ini tidak boleh kosong!", "arrayEmptyRequired":"Array ini harus kosong!", "arrayTooShort":"Array ini harus memiliki panjang setidaknya {0}!", @@ -59,7 +69,7 @@ "arrayLengthInvalid":"Array ini hanya dapat berisi tipe berikut ini: {0}!", "arrayInvalidTypes":"Array ini hanya dapat berisi tipe berikut: {0}!", "arrayDouble":"Array ini tidak mengizinkan nilai yang sama dua kali!", - + "discordInvalidId":"Ini adalah id Discord {0} yang tidak valid!", "discordInvalidIdOptions":"Ini adalah id Discord {0} yang tidak valid! Anda juga dapat menggunakan salah satu dari ini: {1}!", "discordInvalidToken":"Ini adalah token Discord yang tidak valid (secara sintaksis)!", @@ -76,7 +86,7 @@ "urlInvalidPath":"URL ini memiliki jalur yang tidak valid!", "idNotUnique":"Id ini tidak unik, gunakan id lain sebagai gantinya!", "idNonExistent":"Id {0} tidak ada!", - + "invalidType":"Properti ini harus sesuai dengan jenisnya: {0}!", "propertyMissing":"Properti {0} tidak ada pada objek ini!", "propertyOptional":"Properti {0} bersifat opsional dalam objek ini!", @@ -84,12 +94,13 @@ "nullInvalid":"Properti ini tidak boleh bernilai nol!", "switchInvalidType":"Ini harus salah satu dari jenis berikut ini: {0}!", "objectSwitchInvalid":"Objek ini harus merupakan salah satu dari jenis berikut: {0}!", - + "invalidLanguage":"Ini adalah bahasa yang tidak valid!", "invalidButton":"Tombol ini harus memiliki setidaknya {0} atau {1}!", "unusedOption":"Opsi {0} tidak digunakan di mana pun!", "unusedQuestion":"Pertanyaan {0} tidak digunakan di mana pun!", - "dropdownOption":"Panel dengan menu tarik-ulur yang diaktifkan hanya dapat berisi opsi dengan tipe 'tiket'!" + "dropdownOption":"Panel dengan menu tarik-ulur yang diaktifkan hanya dapat berisi opsi dengan tipe 'tiket'!", + "customInvalidVersion":"Versi yang ditetapkan dalam konfigurasi tidak sesuai! Pastikan konfigurasi Anda telah diperbarui ke versi terbaru!" } }, "actions":{ @@ -132,6 +143,7 @@ "blacklistAddDm":"Ditambahkan ke Daftar Hitam", "blacklistRemoveDm":"Dihapus dari Daftar Hitam", "clear":"Tiket dibersihkan.", + "clearTickets":"Bersihkan Tiket", "roles":"Peran Diperbarui", "autoclose":"Tiket Ditutup Otomatis", @@ -139,7 +151,12 @@ "autocloseDisabled":"Tutup Otomatis Dimatikan", "autodelete":"Tiket Dihapus Otomatis", "autodeleteEnabled":"Hapus Otomatis Diaktifkan", - "autodeleteDisabled":"Hapus Otomatis Dinonaktifkan" + "autodeleteDisabled":"Hapus Otomatis Dinonaktifkan", + + "topicSet":"Topik Diganti", + "prioritySet":"Prioritas Berganti", + "priorityGet":"Prioritas Tiket", + "transfer":"Tiket Ditransfer" }, "descriptions":{ "create":"Tiket kamu telah dibuat. Klik tombol di bawah ini untuk mengaksesnya!", @@ -154,7 +171,7 @@ "move":"Tiket telah berhasil dipindahkan ke {0}!", "add":"{0} telah berhasil ditambahkan ke tiket!", "remove":"{0} telah berhasil dihapus dari tiket!", - + "helpExplanation":"`` => parameter wajib\n`[name]` => parameter opsional", "statsReset":"Statistik bot telah berhasil disetel ulang!", "statsError":"Tidak dapat melihat statistik tiket!\n{0} bukan tiket!", @@ -167,7 +184,7 @@ "clearVerify":"Apakah Anda yakin ingin menghapus beberapa tiket?\nTindakan ini tidak dapat dibatalkan!", "clearReady":"Tiket {0} telah berhasil dihapus!", "rolesEmpty":"Tidak ada peran yang telah diperbarui!", - + "autocloseLeave":"tiket ini telah ditutup secara otomatis karena pembuatnya meninggalkan server!", "autocloseTimeout":"tiket ini telah ditutup otomatis karena tidak aktif selama lebih dari `{0} jam`!", "autodeleteLeave":"tiket ini telah dihapus secara otomatis karena pembuatnya meninggalkan server!", @@ -176,11 +193,16 @@ "autocloseDisabled":"Penutupan otomatis telah dinonaktifkan untuk tiket ini!\nIt won't be closed automatically anymore!", "autodeleteEnabled":"Penghapsan otomatis telah diaktifkan untuk tiket ini!\nIni akan dihapus jika tidak aktif selama lebih dari `{0} hari`!", "autodeleteDisabled":"Penghapusan otomatis telah dinonaktifkan untuk tiket ini!\nTiket ini tidak akan dihapus secara otomatis lagi!", - + "ticketMessageLimit":"Anda hanya dapat membuat {0} tiket pada saat yang bersamaan!", "ticketMessageAutoclose":"Tiket ini akan ditutup secara otomatis ketika tidak aktif selama {0} jam!", "ticketMessageAutodelete":"Tiket ini akan dihapus secara otomatis ketika tidak aktif selama {0} hari!", - "panelReady":"Anda dapat menemukan panel di bawah ini!\nPesan ini sekarang dapat dihapus!" + "panelReady":"Panel ini tersedia dalam pesan tindak lanjut!\nPesan ini sekarang dapat dihapus!", + + "topicSet":"Topik kanal ini telah diubah oleh {0}!", + "prioritySet":"Prioritas tiket ini telah diubah menjadi {0} oleh {1}!", + "priorityGet":"Prioritas tiket ini pada saat ini adalah {0}.", + "transfer":"Status kepemilikan tiket telah berhasil dipindahkan dari {0} ke {1} oleh {2}!" }, "modal":{ "closePlaceholder":"Mengapa kamu menutup tiket ini?", @@ -215,12 +237,19 @@ "addDm":"{0} telah ditambahkan ke tiket kamu di server kami!", "removeLog":"{0} telah dihapus dari tiket ini oleh {1}!", "removeDm":"{0} telah dihapus dari tiket kamu di server kami!", - + "blacklistAddLog":"{0} dimasukkan ke dalam daftar hitam oleh {1}!", "blacklistRemoveLog":"{0} dihapus dari daftar hitam oleh {1}!", "blacklistAddDm":"Anda telah masuk ke dalam daftar hitam di server kami!\nMulai sekarang, Anda tidak dapat membuat tiket!", "blacklistRemoveDm":"Kamu telah dihapus dari daftar hitam di server kami!\nSekarang Anda dapat membuat tiket lagi!", - "clearLog":"Tiket {0} telah dihapus oleh {1}!" + "clearLog":"Tiket {0} telah dihapus oleh {1}!", + + "transferLog":"Kepemilikan tiket ini telah berhasil dipindahkan dari {0} ke {1} oleh {2}", + "transferDm":"Kepemilikan tiket kamu sudah dialihkan dari {0} ke {1} di server kami!", + "prioritySetLog":"Prioritas tiket ini telah diubah menjadi {0} oleh {1}!", + "prioritySetDm":"Prioritas tiket kamu telah diubah menjadi {0} di server kami!", + "roleUpdateLog":"{0} telah memperbarui status mereka!", + "roleUpdateDm":"Status kamu di server kami telah diperbarui!" } }, "transcripts":{ @@ -241,7 +270,19 @@ "retry":"Coba Ulang", "continue":"Hapus Tanpa Transkrip", "backup":"Membuat Transkrip Cadangan", - "error":"Terjadi kesalahan saat mencoba membuat transkrip.\nApa yang ingin Anda lakukan?\n\nTiket ini tidak akan dihapus hingga Anda mengeklik salah satu tombol ini." + "error":"Terjadi kesalahan saat mencoba membuat transkrip.\nApa yang ingin Anda lakukan?\n\nTiket ini tidak akan dihapus hingga Anda mengeklik salah satu tombol ini.", + "title":"Kesalahan Dalam Pembuatan Transkrip" + }, + "text":{ + "messagesTitle":"PESAN", + "embedTitle":"SEMATAN", + "fileTitle":"BERKAS", + "fieldsTitle":"BIDANG", + "reactionsTitle":"REAKSI", + "statsTitle":"STATUS", + "emptyContent":"", + "noTitle":"", + "noDesc":"" } }, "errors":{ @@ -257,7 +298,8 @@ "unknownPanel":"Panel Tidak Diketahui", "notInGuild":"Tidak Berada Di Dalam Server", "channelRename":"Tidak Dapat Mengganti Nama Saluran", - "busy":"Tidak Sedang Sibuk" + "busy":"Tidak Sedang Sibuk", + "permissionError":"Terjadi Kesalahan Hak Akses" }, "descriptions":{ "askForInfo":"Hubungi pemilik bot ini untuk info lebih lanjut!", @@ -272,15 +314,18 @@ "noPermissionsCooldown":"Anda tidak diperbolehkan menggunakan {0} karena kamu sedang dalam cooldown!", "noPermissionsBlacklist":"Anda tidak diperbolehkan menggunakan {0} karena Anda telah masuk dalam daftar hitam!", "noPermissionsLimitGlobal":"Anda tidak diizinkan untuk membuat tiket karena server telah mencapai batas maksimal tiket!", - "noPermissionsLimitGlobalPengguna":"Anda tidak dapat membuat tiket karena Anda telah mencapai batas maksimal tiket!", + "noPermissionsLimitGlobalUser":"Anda tidak dapat membuat tiket karena Anda telah mencapai batas maksimal tiket!", "noPermissionsLimitOption":"Anda tidak diizinkan untuk membuat tiket karena server telah mencapai batas maksimal tiket untuk opsi ini!", - "noPermissionsLimitOptionPengguna":"Anda tidak dapat membuat tiket karena Anda telah mencapai batas maksimal tiket untuk opsi ini!", + "noPermissionsLimitOptionUser":"Anda tidak dapat membuat tiket karena Anda telah mencapai batas maksimal tiket untuk opsi ini!", "unknownTicket":"Gunakan perintah ini lagi dalam tiket yang valid!", "deprecatedTicket":"Saluran saat ini bukanlah tiket yang valid! Ini mungkin merupakan tiket dari versi Open Tiket yang lama!", "notInGuild":"Ini {0} tidak berfungsi di DM! Silakan coba lagi di server!", "channelRename":"Karena batasan rasio Discord, saat ini bot tidak mungkin mengganti nama saluran. Saluran akan secara otomatis diganti namanya dalam waktu 10 menit jika bot tidak di-boot ulang.", "channelRenameSource":"Sumber dari masalah: {0}", - "busy":"Dapat dapat menggunakan {0}!\nTiket sedang diproses oleh bot.\n\nMohon coba beberapa saat lagi!" + "busy":"Dapat dapat menggunakan {0}!\nTiket sedang diproses oleh bot.\n\nMohon coba beberapa saat lagi!", + "closeBeforeMessage":"Tiket ini tidak dapat ditutup ataupun dihapus sebelum pesan dikirim oleh seseorang.", + "closeBeforeAdminMessage":"Tiket ini tidak dapat ditutup ataupun dihapus sebelum admin tiket atau anggota tim dukungan mengirimkan pesan.", + "unableToCreateTicket":"Anda tidak dapat membuat tiket.." }, "optionInvalidReasons":{ "stringRegex":"Nilai tidak sesuai pola!", @@ -331,7 +376,6 @@ "added":"Ditambahkan", "removed":"Dikeluarkan", "filter":"Penyaring", - "claimedBy":"Diklaim oleh {0}", "method":"Metode", "type":"Tipe", "blacklisted":"Daftar hitam", @@ -355,7 +399,27 @@ "status":"Status", "claimed":"Diklaim", "pinned":"Disematkan", - "creationDate":"Tanggal Pembuatan" + "creationDate":"Tanggal Pembuatan", + + "noone":"Tidak Seorangpun", + "open":"Buka", + "closed":"Tutup", + "priority":"Prioritas", + "participants":"Para Partisipan", + "yes":"Ya", + "no":"Tidak", + "option":"Opsi", + "topic":"Topik", + "uptime":"Waktu Aktif Sistem", + "messages":"Pesan", + "embeds":"Sematan", + "files":"Berkas", + "components":"Komponen", + "cooldown":"Cooldown", + "maxTickets":"Tiket Maksimum", + "admins":"Admin", + "roles":"Hak", + "size":"Ukuran" }, "lowercase":{ "text":"teks", @@ -384,6 +448,7 @@ "unclaim":"Batalkan klaim tiket.", "pin":"Sematkan sebuah tiket.", "unpin":"Batalkan penyematan tiket.", + "move":"Pindahkan tiket.", "moveId":"Penanda opsi yang ingin anda pindahkan.", "rename":"Ganti nama tiket.", @@ -392,6 +457,7 @@ "addUser":"Pengguna yang akan ditambahkan.", "remove":"Keluarkan seseorang dari tiket.", "removeUser":"Pengguna yang akan dikeluarkan.", + "blacklist":"Kelola the Tiket blacklist.", "blacklistView":"Lihat daftar yang masuk ke dalam daftar hitam.", "blacklistAdd":"Tambahkan seseorang ke dalam daftar hitam.", @@ -405,6 +471,7 @@ "statsUserUser":"Pengguna yang akan dilihat.", "statsTicket":"Lihat status dari sebuah tiket di server.", "statsTicketTicket":"Tiket yang akan dilihat.", + "clear":"Hapus banyak tiket sekaligus.", "clearFilter":"Filter untuk menghapus tiket.", "clearFilters":{ @@ -417,6 +484,7 @@ "unpin":"Pembatalan penyematan", "autoclose":"Ditutup otomatis" }, + "autoclose":"Kelola penutupan otomatis tiket.", "autocloseDisable":"Non-aktifkan penutupan otomatis.", "autocloseEnable":"Aktifkan penutupan otomatis.", @@ -424,7 +492,19 @@ "autodelete":"Kelola penghapusan otomatis tiket.", "autodeleteDisable":"Non-aktifkan penghapusan otomatis.", "autodeleteEnable":"Aktifkan penghapusan otomatis.", - "autodeleteEnableTime":"Lama waktu saluran tidak aktif sebelum dihapus." + "autodeleteEnableTime":"Lama waktu saluran tidak aktif sebelum dihapus.", + + "topic":"Kelola topik kanal tiket.", + "topicSet":"Tentukan topik kanal tiket.", + "topicValue":"Topik baru kanal ini.", + "topicList":"Dapatkan informasi lengkap tentang semua tiket beserta topik dan statistiknya.", + "priority":"Kelola prioritas tiket.", + "prioritySet":"Tentukan prioritas tiket.", + "priorityValue":"Prioritas kanal.", + "priorityGet":"Dapatkan prioritas tiket.", + "priorityList":"Dapatkan daftar semua tiket beserta status prioritasnya.", + "transfer":"Alihkan kepemilikan tiket dari satu pengguna ke pengguna lain..", + "transferUser":"Pengguna yang akan ditransferkan." }, "helpMenu":{ "help":"Dapatkan daftar perintah yang tersedia.", @@ -452,7 +532,16 @@ "autocloseDisable":"Nonaktifkan penutupan otomatis.", "autocloseEnable":"Aktifkan penutupan otomatis.", "autodeleteDisable":"Non-aktifkan penghapusan otomatis.", - "autodeleteEnable":"Aktifkan penghapusan otomatis." + "autodeleteEnable":"Aktifkan penghapusan otomatis.", + "categories":{ + "general":"Perintah Umum", + "basicTicket":"Perintah Dasar Tiket", + "advancedTicket":"Perintah Lanjutan Ticket", + "userTicket":"Perintah Tiket untuk Pengguna", + "admin":"Perintah Admin", + "advanced":"Perintah Lanjutan", + "extra":"Perintah Tambahan" + } }, "stats":{ "scopes":{ @@ -460,7 +549,8 @@ "system":"Status Sistem", "user":"Status Pengguna", "ticket":"Status Tiket", - "participants":"Partisipan" + "participants":"Partisipan", + "messages":"Pesan" }, "properties":{ "ticketsCreated":"Tiket Dibuat", @@ -472,7 +562,47 @@ "ticketsPinned":"Tiket Disematkan", "ticketsMoved":"Tickets Dipindahkan", "usersBlacklisted":"Pengguna Masuk Ke Dalam Daftar Hitam", - "transcriptsCreated":"Transkrip Dibuat" + "transcriptsCreated":"Transkrip Dibuat", + "ticketsAutodeleted":"Tiket Dihapus Secara Otomatis", + "ticketsTransferred":"Tiket Dipindahkan", + "ticketVolume":"Kapasitas Tiket", + "averageTickets":"Rata-rata Tiket per Pengguna", + "currentTickets":"Tiket Saat Ini", + "age":"Umur Tiket", + "responseTime":"Waktu Respons", + "resolutionTime":"Resolution Time", + "createdOn":"Dibuat Pada", + "createdBy":"Dibuat Oleh", + "closedOn":"Ditutup Pada", + "closedBy":"Ditutup Oleh", + "claimedOn":"Diklaim Pada", + "claimedBy":"Diklaim Oleh", + "pinnedOn":"Disematkan Pada", + "pinnedBy":"Disematkan Oleh", + "deletedOn":"Dihapus Pada", + "deletedBy":"Dihapus Oleh" + }, + "roles":{ + "developer":"Pembuat", + "serverOwner":"Pemilik Server", + "serverAdmin":"Admin Server", + "moderator":"Tim Moderator", + "support":"Tim Pendukung", + "member":"Anggota" } + }, + "panel":{ + "selectTicket":"Pilih tiket kamu", + "selectRole":"Pilih status kamu", + "selectOption":"Pilih opsi kamu" + }, + "priorities":{ + "urgent":"Sangat Penting", + "veryHigh":"Sangat Tinggi", + "high":"Tinggi", + "normal":"Normal", + "low":"Rendah", + "veryLow":"Sangat Rendah", + "none":"Tidak Ada" } } \ No newline at end of file diff --git a/languages/thai.json b/languages/thai.json index 1a937cc..f8a633c 100644 --- a/languages/thai.json +++ b/languages/thai.json @@ -2,477 +2,607 @@ "_TRANSLATION":{ "otversion":"v4.1.0", "translators":["modshd"], - "lastedited":"13/11/2024", + "lastedited":"09/11/2025", "language":"Thai", "automated":false }, "checker":{ "system":{ - "typeError": "[ข้อผิดพลาด]", - "headerOpenTicket": "เปิดห้องติดต่อ", - "typeWarning": "[คำเตือน]", - "typeInfo": "[ข้อมูล]", - "headerConfigChecker": "ตัวตรวจสอบการตั้งค่า", - "headerDescription": "ตรวจสอบข้อผิดพลาดในไฟล์การตั้งค่าของคุณ", - "footerError": "บอทจะไม่เริ่มจนกว่าจะแก้ไขข้อผิดพลาดทั้งหมดใน {0}", - "footerWarning": "แนะนำให้แก้ไขข้อผิดพลาดทั้งหมดใน {0} ก่อนเริ่มใช้งาน", - "footerSupport": "สนับสนุน: {0} - เอกสาร: {1}", - "compactInformation": "ใช้ {0} เพื่อข้อมูลเพิ่มเติม", - "dataPath": "ทีอยู่", - "dataDocs": "เอกสารช่วยเหลือ", - "dataMessages": "ข้อความ" + "typeError":"[ข้อผิดพลาด]", + "headerOpenTicket":"เปิดห้องติดต่อ", + "typeWarning":"[คำเตือน]", + "typeInfo":"[ข้อมูล]", + "headerConfigChecker":"ตัวตรวจสอบการตั้งค่า", + "headerDescription":"ตรวจสอบข้อผิดพลาดในไฟล์การตั้งค่าของคุณ", + "footerError":"บอทจะไม่เริ่มจนกว่าจะแก้ไขข้อผิดพลาดทั้งหมดใน {0}", + "footerWarning":"แนะนำให้แก้ไขข้อผิดพลาดทั้งหมดใน {0} ก่อนเริ่มใช้งาน", + "footerSupport":"สนับสนุน: {0} - เอกสาร: {1}", + "compactInformation":"ใช้ {0} เพื่อข้อมูลเพิ่มเติม", + "dataPath":"ทีอยู่", + "dataDocs":"เอกสารช่วยเหลือ", + "dataMessages":"ข้อความ" }, "messages":{ - "stringTooShort": "ข้อความนี้สั้นเกินไปไม่สามารถน้อยกว่า {0} ตัวอักษรได้", - "stringTooLong": "ข้อความนี้ยาวเกินไปไม่สามารถมากกว่า {0} ตัวอักษรได้", - "stringLengthInvalid": "ข้อความนี้ต้องมีความยาว {0} ตัวอักษร", - "stringStartsWith": "ข้อความนี้ต้องเริ่มต้นด้วย {0}", - "stringEndsWith": "ข้อความนี้ต้องลงท้ายด้วย {0}", - "stringContains": "ข้อความนี้ต้องประกอบด้วย {0}", - "stringChoices": "ข้อความนี้สามารถเป็นค่าใดค่าหนึ่งจากตัวเลือกต่อไปนี้: {0}", - "stringRegex": "ข้อความนี้ไม่ถูกต้อง", - - "numberTooShort": "หมายเลขนี้ไม่สามารถสั้นกว่า {0} ตัวอักษรได้", - "numberTooLong": "หมายเลขนี้ไม่สามารถยาวกว่า {0} ตัวอักษรได้", - "numberLengthInvalid": "หมายเลขนี้ต้องมีความยาว {0} ตัวอักษร", - "numberTooSmall": "หมายเลขนี้ต้องมีค่าอย่างน้อย {0}", - "numberTooLarge": "หมายเลขนี้ต้องมีค่าไม่เกิน {0}", - "numberNotEqual": "หมายเลขนี้ต้องเป็น {0}", - "numberStep": "หมายเลขนี้ต้องเป็นผลคูณของ {0}", - "numberStepOffset": "หมายเลขนี้ต้องเป็นผลคูณของ {0} โดยเริ่มจาก {1}", - "numberStartsWith": "หมายเลขนี้ต้องเริ่มต้นด้วย {0}", - "numberEndsWith": "หมายเลขนี้ต้องลงท้ายด้วย {0}", - "numberContains": "หมายเลขนี้ต้องประกอบด้วย {0}", - "numberChoices": "หมายเลขนี้สามารถเป็นค่าใดค่าหนึ่งจากตัวเลือกต่อไปนี้: {0}", - "numberFloat": "หมายเลขนี้ไม่สามารถเป็นทศนิยมได้", - "numberNegative": "หมายเลขนี้ไม่สามารถเป็นค่าลบได้", - "numberPositive": "หมายเลขนี้ไม่สามารถเป็นค่าบวกได้", - "numberZero": "หมายเลขนี้ไม่สามารถเป็นศูนย์ได้", - - "booleanTrue": "ค่าบูลีนนี้ไม่สามารถเป็น true ได้", - "booleanFalse": "ค่าบูลีนนี้ไม่สามารถเป็น false ได้", - - "arrayEmptyDisabled": "ค่าตัวแปรนี้ไม่อนุญาตให้ว่างเปล่า", - "arrayEmptyRequired": "ค่าตัวแปรนี้ต้องว่างเปล่า", - "arrayTooShort": "ค่าตัวแปรนี้ต้องมีความยาวอย่างน้อย {0}", - "arrayTooLong": "ค่าตัวแปรนี้ต้องมีความยาวไม่เกิน {0}", - "arrayLengthInvalid": "ค่าตัวแปรนี้ต้องมีความยาว {0}", - "arrayInvalidTypes": "ค่าตัวแปรนี้สามารถประกอบด้วยประเภทเหล่านี้เท่านั้น: {0}", - "arrayDouble": "ค่าตัวแปรนี้ไม่อนุญาตให้มีค่าซ้ำสองครั้ง", - - "discordInvalidId": "นี่คือรหัสไอดี {0} discord ที่ไม่ถูกต้อง", - "discordInvalidIdOptions": "นี่คือรหัสไอดี {0} discord ที่ไม่ถูกต้อง คุณสามารถใช้ตัวเลือกเหล่านี้: {1}", - "discordInvalidToken": "นี่คือโทเค็น discord ที่ไม่ถูกต้อง (จากด้านไวยากรณ์)", - "colorInvalid": "นี่คือสี hex ที่ไม่ถูกต้อง", - "emojiTooShort": "ข้อความนี้ต้องมีอีโมจิอย่างน้อย {0} ตัว", - "emojiTooLong": "ข้อความนี้ต้องมีอีโมจิไม่เกิน {0} ตัว", - "emojiCustom": "อีโมจินี้ไม่สามารถเป็นอีโมจิแบบกำหนดเองของ discord ได้", - "emojiInvalid": "นี่คือลำดับอีโมจิที่ไม่ถูกต้อง", - "urlInvalid": "URL นี้ไม่ถูกต้อง", - "urlInvalidHttp": "URL นี้สามารถใช้แค่โปรโตคอล https:// เท่านั้น", - "urlInvalidProtocol": "URL นี้สามารถใช้โปรโตคอล http:// และ https:// เท่านั้น", - "urlInvalidHostname": "URL นี้มีชื่อโฮสต์ที่ไม่ได้รับอนุญาต", - "urlInvalidExtension": "URL นี้มีนามสกุลไม่ถูกต้อง เลือกระหว่าง: {0}", - "urlInvalidPath": "URL นี้มีเส้นทางไม่ถูกต้อง", - "idNotUnique": "รหัสนี้ไม่เป็นเอกลักษณ์, ใช้รหัสอื่นแทน", - "idNonExistent": "รหัส {0} นี้ไม่มีอยู่", - - "invalidType": "คุณสมบัตินี้ต้องเป็นประเภท: {0}", - "propertyMissing": "คุณสมบัติ {0} หายไปจากอ็อบเจ็กต์นี้", - "propertyOptional": "คุณสมบัติ {0} เป็นตัวเลือกในอ็อบเจ็กต์นี้", - "objectDisabled": "อ็อบเจ็กต์นี้ถูกปิดใช้งาน, เปิดใช้งานโดยใช้ {0}", - "nullInvalid": "คุณสมบัตินี้ไม่สามารถเป็นค่า null ได้", - "switchInvalidType": "ตัวเลือกนี้ต้องเป็นประเภทใดประเภทหนึ่งจากรายการต่อไปนี้: {0}", - "objectSwitchInvalid": "อ็อบเจ็กต์นี้ต้องเป็นประเภทใดประเภทหนึ่งจากรายการต่อไปนี้: {0}", - - "invalidLanguage": "นี่คือภาษาที่ไม่ถูกต้อง", - "invalidButton": "ปุ่มนี้ต้องมีอย่างน้อย {0} หรือ {1}", - "unusedOption": "ตัวเลือก {0} ไม่ได้ใช้งานที่ไหนเลย", - "unusedQuestion": "คำถาม {0} ไม่ได้ใช้งานที่ไหนเลย", - "dropdownOption": "แผงที่เปิดใช้งานตัวเลือกแบบเลื่อนลงสามารถประกอบด้วยตัวเลือกประเภท 'ticket' เท่านั้น" + "stringTooShort":"ข้อความนี้สั้นเกินไปไม่สามารถน้อยกว่า {0} ตัวอักษรได้", + "stringTooLong":"ข้อความนี้ยาวเกินไปไม่สามารถมากกว่า {0} ตัวอักษรได้", + "stringLengthInvalid":"ข้อความนี้ต้องมีความยาว {0} ตัวอักษร", + "stringStartsWith":"ข้อความนี้ต้องเริ่มต้นด้วย {0}", + "stringEndsWith":"ข้อความนี้ต้องลงท้ายด้วย {0}", + "stringContains":"ข้อความนี้ต้องประกอบด้วย {0}", + "stringChoices":"ข้อความนี้สามารถเป็นค่าใดค่าหนึ่งจากตัวเลือกต่อไปนี้: {0}", + "stringRegex":"ข้อความนี้ไม่ถูกต้อง", + "stringInvertedContains":"ข้อความนี้ไม่ได้รับอนุญาตให้มี {0}", + "stringLowercase":"ข้อความนี้ต้องเขียนด้วยตัวพิมพ์เล็กเท่านั้น", + "stringUppercase":"ข้อความนี้ต้องเขียนด้วยตัวพิมพ์ใหญ่เท่านั้น", + "stringSpecialCharacters":"ข้อความนี้ไม่ได้รับอนุญาตให้มีอักขระพิเศษใดๆ (a-z, 0-9 และ เว้นวรรคเท่านั้น)", + "stringNoSpaces":"ข้อความนี้ไม่ได้รับอนุญาตให้มีช่องว่าง", + "stringCapitalWord":"ขอแนะนำให้แต่ละคำในข้อความนี้ขึ้นต้นด้วยตัวพิมพ์ใหญ่", + "stringCapitalSentence":"ดูเหมือนว่าบางประโยคในข้อความนี้ไม่ได้ขึ้นต้นด้วยตัวพิมพ์ใหญ่", + "stringPunctuation":"ดูเหมือนว่าประโยคในข้อความนี้ไม่ได้ลงท้ายด้วยเครื่องหมายวรรคตอน", + + "numberTooShort":"หมายเลขนี้ไม่สามารถสั้นกว่า {0} ตัวอักษรได้", + "numberTooLong":"หมายเลขนี้ไม่สามารถยาวกว่า {0} ตัวอักษรได้", + "numberLengthInvalid":"หมายเลขนี้ต้องมีความยาว {0} ตัวอักษร", + "numberTooSmall":"หมายเลขนี้ต้องมีค่าอย่างน้อย {0}", + "numberTooLarge":"หมายเลขนี้ต้องมีค่าไม่เกิน {0}", + "numberNotEqual":"หมายเลขนี้ต้องเป็น {0}", + "numberStep":"หมายเลขนี้ต้องเป็นผลคูณของ {0}", + "numberStepOffset":"หมายเลขนี้ต้องเป็นผลคูณของ {0} โดยเริ่มจาก {1}", + "numberStartsWith":"หมายเลขนี้ต้องเริ่มต้นด้วย {0}", + "numberEndsWith":"หมายเลขนี้ต้องลงท้ายด้วย {0}", + "numberContains":"หมายเลขนี้ต้องประกอบด้วย {0}", + "numberChoices":"หมายเลขนี้สามารถเป็นค่าใดค่าหนึ่งจากตัวเลือกต่อไปนี้: {0}", + "numberFloat":"หมายเลขนี้ไม่สามารถเป็นทศนิยมได้", + "numberNegative":"หมายเลขนี้ไม่สามารถเป็นค่าลบได้", + "numberPositive":"หมายเลขนี้ไม่สามารถเป็นค่าบวกได้", + "numberZero":"หมายเลขนี้ไม่สามารถเป็นศูนย์ได้", + "numberNan":"ตัวเลขนี้ไม่สามารถเป็น NaN (ไม่ใช่ตัวเลข)", + "numberInvertedContains":"ตัวเลขนี้ไม่ได้รับอนุญาตให้มี {0}", + + "booleanTrue":"ค่าบูลีนนี้ไม่สามารถเป็น true ได้", + "booleanFalse":"ค่าบูลีนนี้ไม่สามารถเป็น false ได้", + + "arrayEmptyDisabled":"ค่าตัวแปรนี้ไม่อนุญาตให้ว่างเปล่า", + "arrayEmptyRequired":"ค่าตัวแปรนี้ต้องว่างเปล่า", + "arrayTooShort":"ค่าตัวแปรนี้ต้องมีความยาวอย่างน้อย {0}", + "arrayTooLong":"ค่าตัวแปรนี้ต้องมีความยาวไม่เกิน {0}", + "arrayLengthInvalid":"ค่าตัวแปรนี้ต้องมีความยาว {0}", + "arrayInvalidTypes":"ค่าตัวแปรนี้สามารถประกอบด้วยประเภทเหล่านี้เท่านั้น: {0}", + "arrayDouble":"ค่าตัวแปรนี้ไม่อนุญาตให้มีค่าซ้ำสองครั้ง", + + "discordInvalidId":"นี่คือรหัสไอดี {0} discord ที่ไม่ถูกต้อง", + "discordInvalidIdOptions":"นี่คือรหัสไอดี {0} discord ที่ไม่ถูกต้อง คุณสามารถใช้ตัวเลือกเหล่านี้: {1}", + "discordInvalidToken":"นี่คือโทเค็น discord ที่ไม่ถูกต้อง (จากด้านไวยากรณ์)", + "colorInvalid":"นี่คือสี hex ที่ไม่ถูกต้อง", + "emojiTooShort":"ข้อความนี้ต้องมีอีโมจิอย่างน้อย {0} ตัว", + "emojiTooLong":"ข้อความนี้ต้องมีอีโมจิไม่เกิน {0} ตัว", + "emojiCustom":"อีโมจินี้ไม่สามารถเป็นอีโมจิแบบกำหนดเองของ discord ได้", + "emojiInvalid":"นี่คือลำดับอีโมจิที่ไม่ถูกต้อง", + "urlInvalid":"URL นี้ไม่ถูกต้อง", + "urlInvalidHttp":"URL นี้สามารถใช้แค่โปรโตคอล https:// เท่านั้น", + "urlInvalidProtocol":"URL นี้สามารถใช้โปรโตคอล http:// และ https:// เท่านั้น", + "urlInvalidHostname":"URL นี้มีชื่อโฮสต์ที่ไม่ได้รับอนุญาต", + "urlInvalidExtension":"URL นี้มีนามสกุลไม่ถูกต้อง เลือกระหว่าง: {0}", + "urlInvalidPath":"URL นี้มีเส้นทางไม่ถูกต้อง", + "idNotUnique":"รหัสนี้ไม่เป็นเอกลักษณ์, ใช้รหัสอื่นแทน", + "idNonExistent":"รหัส {0} นี้ไม่มีอยู่", + + "invalidType":"คุณสมบัตินี้ต้องเป็นประเภท: {0}", + "propertyMissing":"คุณสมบัติ {0} หายไปจากอ็อบเจ็กต์นี้", + "propertyOptional":"คุณสมบัติ {0} เป็นตัวเลือกในอ็อบเจ็กต์นี้", + "objectDisabled":"อ็อบเจ็กต์นี้ถูกปิดใช้งาน, เปิดใช้งานโดยใช้ {0}", + "nullInvalid":"คุณสมบัตินี้ไม่สามารถเป็นค่า null ได้", + "switchInvalidType":"ตัวเลือกนี้ต้องเป็นประเภทใดประเภทหนึ่งจากรายการต่อไปนี้: {0}", + "objectSwitchInvalid":"อ็อบเจ็กต์นี้ต้องเป็นประเภทใดประเภทหนึ่งจากรายการต่อไปนี้: {0}", + + "invalidLanguage":"นี่คือภาษาที่ไม่ถูกต้อง", + "invalidButton":"ปุ่มนี้ต้องมีอย่างน้อย {0} หรือ {1}", + "unusedOption":"ตัวเลือก {0} ไม่ได้ใช้งานที่ไหนเลย", + "unusedQuestion":"คำถาม {0} ไม่ได้ใช้งานที่ไหนเลย", + "dropdownOption":"แผงที่เปิดใช้งานตัวเลือกแบบเลื่อนลงสามารถประกอบด้วยตัวเลือกประเภท 'ticket' เท่านั้น", + "customInvalidVersion":"เวอร์ชันที่ระบุในไฟล์ config ของคุณไม่ตรงกัน ตรวจสอบให้แน่ใจว่าคุณได้อัปเดตไฟล์ config เป็นเวอร์ชันล่าสุดแล้ว" } }, "actions":{ "buttons":{ - "create": "สร้างห้องติดต่อ", - "close": "ปิดห้องติดต่อ", - "delete": "ลบห้องติดต่อ", - "reopen": "เปิดห้องติดต่อใหม่อีกครั้ง", - "claim": "รับห้องติดต่อ", - "unclaim": "ยกเลิกการรับห้องติดต่อ", - "pin": "ปักหมุดห้องติดต่อ", - "unpin": "ยกเลิกการปักหมุดห้องติดต่อ", - "clear": "ลบห้องติดต่อทั้งหมด", - "helpSwitchSlash": "ดูคำสั่งแบบ Slash", - "helpSwitchText": "ดูคำสั่งแบบข้อความ", - "helpPage": "หน้า {0}", - "withReason": "ด้วยเหตุผล", - "withoutTranscript": "ไม่มีบันทึกการสนทนา" + "create":"สร้างห้องติดต่อ", + "close":"ปิดห้องติดต่อ", + "delete":"ลบห้องติดต่อ", + "reopen":"เปิดห้องติดต่อใหม่อีกครั้ง", + "claim":"รับห้องติดต่อ", + "unclaim":"ยกเลิกการรับห้องติดต่อ", + "pin":"ปักหมุดห้องติดต่อ", + "unpin":"ยกเลิกการปักหมุดห้องติดต่อ", + "clear":"ลบห้องติดต่อทั้งหมด", + "helpSwitchSlash":"ดูคำสั่งแบบ Slash", + "helpSwitchText":"ดูคำสั่งแบบข้อความ", + "helpPage":"หน้า {0}", + "withReason":"ด้วยเหตุผล", + "withoutTranscript":"ไม่มีบันทึกการสนทนา" }, "titles":{ - "created": "สร้างห้องติดต่อแล้ว", - "close": "ปิดห้องติดต่อแล้ว", - "delete": "ลบห้องติดต่อแล้ว", - "reopen": "เปิดห้องติดต่อใหม่อีกรอบแล้ว", - "claim": "รับห้องติดต่อแล้ว", - "unclaim": "ยกเลิกการรับห้องติดต่อแล้ว", - "pin": "ปักหมุดห้องติดต่อแล้ว", - "unpin": "ยกเลิกการปักหมุดห้องติดต่อแล้ว", - "rename": "เปลี่ยนชื่อเรื่องห้องติดต่อแล้ว", - "move": "ย้ายห้องติดต่อแล้ว", - "add": "เพิ่มผู้ใช้ในห้องติดต่อแล้ว", - "remove": "ลบผู้ใช้จากห้องติดต่อแล้ว", + "created":"สร้างห้องติดต่อแล้ว", + "close":"ปิดห้องติดต่อแล้ว", + "delete":"ลบห้องติดต่อแล้ว", + "reopen":"เปิดห้องติดต่อใหม่อีกรอบแล้ว", + "claim":"รับห้องติดต่อแล้ว", + "unclaim":"ยกเลิกการรับห้องติดต่อแล้ว", + "pin":"ปักหมุดห้องติดต่อแล้ว", + "unpin":"ยกเลิกการปักหมุดห้องติดต่อแล้ว", + "rename":"เปลี่ยนชื่อเรื่องห้องติดต่อแล้ว", + "move":"ย้ายห้องติดต่อแล้ว", + "add":"เพิ่มผู้ใช้ในห้องติดต่อแล้ว", + "remove":"ลบผู้ใช้จากห้องติดต่อแล้ว", - "help": "คำสั่งที่สามารถใช้งานได้", - "statsReset": "รีเซ็ตสถิติ", - "blacklistAdd": "ผู้ใช้ถูกเพิ่มในบัญชีดำ", - "blacklistRemove": "ผู้ใช้ถูกถอดออกจากบัญชีดำ", - "blacklistGet": "ผู้ใช้ที่อยู่ในบัญชีดำ", - "blacklistView": "บัญชีดำปัจจุบัน", - "blacklistAddDm": "ถูกเพิ่มในบัญชีดำ", - "blacklistRemoveDm": "ถูกลบออกจากบัญชีดำ", - "clear": "ลบห้องติดต่อทั้งหมด", - "roles": "อัปเดตบทบาท", + "help":"คำสั่งที่สามารถใช้งานได้", + "statsReset":"รีเซ็ตสถิติ", + "blacklistAdd":"ผู้ใช้ถูกเพิ่มในบัญชีดำ", + "blacklistRemove":"ผู้ใช้ถูกถอดออกจากบัญชีดำ", + "blacklistGet":"ผู้ใช้ที่อยู่ในบัญชีดำ", + "blacklistView":"บัญชีดำปัจจุบัน", + "blacklistAddDm":"ถูกเพิ่มในบัญชีดำ", + "blacklistRemoveDm":"ถูกลบออกจากบัญชีดำ", + "clear":"ลบห้องติดต่อทั้งหมด", + "clearTickets":"ล้างห้องติดต่อ", + "roles":"อัปเดตบทบาท", - "autoclose": "ห้องติดต่อถูกปิดโดยอัตโนมัติ", - "autocloseEnabled": "เปิดใช้งานการปิดห้องติดต่ออัตโนมัติ", - "autocloseDisabled": "ปิดการใช้งานการปิดห้องติดต่ออัตโนมัติ", - "autodelete": "ห้องติดต่อถูกลบโดยอัตโนมัติ", - "autodeleteEnabled": "เปิดใช้งานการลบห้องติดต่ออัตโนมัติ", - "autodeleteDisabled": "ปิดการใช้งานการลบห้องติดต่ออัตโนมัติ" + "autoclose":"ห้องติดต่อถูกปิดโดยอัตโนมัติ", + "autocloseEnabled":"เปิดใช้งานการปิดห้องติดต่ออัตโนมัติ", + "autocloseDisabled":"ปิดการใช้งานการปิดห้องติดต่ออัตโนมัติ", + "autodelete":"ห้องติดต่อถูกลบโดยอัตโนมัติ", + "autodeleteEnabled":"เปิดใช้งานการลบห้องติดต่ออัตโนมัติ", + "autodeleteDisabled":"ปิดการใช้งานการลบห้องติดต่ออัตโนมัติ", + + "topicSet":"เปลี่ยนหัวข้อแล้ว", + "prioritySet":"เปลี่ยนลำดับความสำคัญแล้ว", + "priorityGet":"ลำดับความสำคัญของห้องติดต่อ", + "transfer":"โอนห้องติดต่อแล้ว" }, "descriptions":{ - "create": "ห้องติดต่อของคุณถูกสร้างแล้ว คลิกปุ่มด้านล่างเพื่อไปยังห้องติดต่อ", - "close": "ห้องติดต่อได้ถูกปิดเรียบร้อยแล้ว", - "delete": "ห้องติดต่อได้ถูกลบเรียบร้อยแล้ว", - "reopen": "ห้องติดต่อได้ถูกเปิดใหม่เรียบร้อยแล้ว", - "claim": "ห้องติดต่อได้ถูกรับเรียบร้อยแล้ว", - "unclaim": "ห้องติดต่อได้ถูกยกเลิกการรับเรียบร้อยแล้ว", - "pin": "ห้องติดต่อได้ถูกปักหมุดเรียบร้อยแล้ว", - "unpin": "ห้องติดต่อได้ถูกยกเลิกการปักหมุดเรียบร้อยแล้ว", - "rename": "ห้องติดต่อได้ถูกเปลี่ยนชื่อเป็น {0} เรียบร้อยแล้ว", - "move": "ห้องติดต่อได้ถูกย้ายไปยัง {0} เรียบร้อยแล้ว", - "add": "{0} ได้ถูกเพิ่มในห้องติดต่อเรียบร้อยแล้ว", - "remove": "{0} ได้ถูกลบออกจากห้องติดต่อเรียบร้อยแล้ว", - - "helpExplanation": "`` => สิ่งที่ต้องการ\n`[name]` => สิ่งที่เลือกใช้ได้", - "statsReset": "สถิติของบอทได้ถูกรีเซ็ตเรียบร้อยแล้ว", - "statsError": "ไม่สามารถดูสถิติห้องติดต่อได้\n{0} ไม่ใช่ห้องติดต่อ", - "blacklistAdd": "{0} ได้ถูกเพิ่มในบัญชีดำเรียบร้อยแล้ว", - "blacklistRemove": "{0} ได้ถูกถอดออกจากบัญชีดำเรียบร้อยแล้ว", - "blacklistGetSuccess": "{0} ตอนนี้อยู่ในบัญชีดำ", - "blacklistGetEmpty": "{0} ตอนนี้ยังไม่อยู่ในบัญชีดำ", - "blacklistViewEmpty": "ยังไม่มีใครถูกเพิ่มในบัญชีดำ", - "blacklistViewTip": "ใช้ \"/blacklist add\" เพื่อเพิ่มผู้ใช้ในบัญชีดำ", - "clearVerify": "คุณแน่ใจหรือไม่ว่าจะลบห้องติดต่อจำนวนมาก?\nการกระทำนี้ไม่สามารถย้อนกลับได้", - "clearReady": "{0} ห้องติดต่อได้ถูกลบเรียบร้อยแล้ว", - "rolesEmpty": "ไม่มีการอัปเดตบทบาทใดๆ", - - "autocloseLeave": "ห้องติดต่อนี้ถูกปิดอัตโนมัติเนื่องจากผู้สร้างห้องติดต่อออกจากเซิร์ฟเวอร์ไปแล้ว", - "autocloseTimeout": "ห้องติดต่อนี้ถูกปิดอัตโนมัติเนื่องจากไม่ได้รับการใช้งานเกิน `{0} ชั่วโมง`", - "autodeleteLeave": "ห้องติดต่อนี้ถูกลบอัตโนมัติเนื่องจากผู้สร้างออกจากเซิร์ฟเวอร์ไปแล้ว", - "autodeleteTimeout": "ห้องติดต่อนี้ถูกลบอัตโนมัติเนื่องจากไม่ได้รับการใช้งานเกิน `{0} วัน`", - "autocloseEnabled": "การปิดอัตโนมัติถูกเปิดใช้งานในห้องติดต่อนี้\nมันจะถูกปิดเมื่อไม่ได้ใช้งานเกิน `{0} ชั่วโมง`", - "autocloseDisabled": "การปิดอัตโนมัติถูกปิดใช้งานในห้องติดต่อนี้\nมันจะไม่ถูกปิดอัตโนมัติอีกต่อไป", - "autodeleteEnabled": "การลบอัตโนมัติถูกเปิดใช้งานในห้องติดต่อนี้\nมันจะถูกลบเมื่อไม่ได้ใช้งานเกิน `{0} วัน`", - "autodeleteDisabled": "การลบอัตโนมัติถูกปิดใช้งานในห้องติดต่อนี้\nมันจะไม่ถูกลบอัตโนมัติอีกต่อไป", - - "ticketMessageLimit": "คุณสามารถสร้างห้องติดต่อได้เพียง {0} ห้องต่อครั้ง", - "ticketMessageAutoclose": "ห้องติดต่อนี้จะถูกปิดอัตโนมัติเมื่อไม่ได้ใช้งานเกิน {0} ชั่วโมง", - "ticketMessageAutodelete": "ห้องติดต่อนี้จะถูกลบอัตโนมัติเมื่อไม่ได้ใช้งานเกิน {0} วัน", - "panelReady": "คุณสามารถดูแผงข้อมูลด้านล่าง\nข้อความนี้สามารถลบได้แล้ว" + "create":"ห้องติดต่อของคุณถูกสร้างแล้ว คลิกปุ่มด้านล่างเพื่อไปยังห้องติดต่อ", + "close":"ห้องติดต่อได้ถูกปิดเรียบร้อยแล้ว", + "delete":"ห้องติดต่อได้ถูกลบเรียบร้อยแล้ว", + "reopen":"ห้องติดต่อได้ถูกเปิดใหม่เรียบร้อยแล้ว", + "claim":"ห้องติดต่อได้ถูกรับเรียบร้อยแล้ว", + "unclaim":"ห้องติดต่อได้ถูกยกเลิกการรับเรียบร้อยแล้ว", + "pin":"ห้องติดต่อได้ถูกปักหมุดเรียบร้อยแล้ว", + "unpin":"ห้องติดต่อได้ถูกยกเลิกการปักหมุดเรียบร้อยแล้ว", + "rename":"ห้องติดต่อได้ถูกเปลี่ยนชื่อเป็น {0} เรียบร้อยแล้ว", + "move":"ห้องติดต่อได้ถูกย้ายไปยัง {0} เรียบร้อยแล้ว", + "add":"{0} ได้ถูกเพิ่มในห้องติดต่อเรียบร้อยแล้ว", + "remove":"{0} ได้ถูกลบออกจากห้องติดต่อเรียบร้อยแล้ว", + + "helpExplanation":"`` => สิ่งที่ต้องการ\n`[name]` => สิ่งที่เลือกใช้ได้", + "statsReset":"สถิติของบอทได้ถูกรีเซ็ตเรียบร้อยแล้ว", + "statsError":"ไม่สามารถดูสถิติห้องติดต่อได้\n{0} ไม่ใช่ห้องติดต่อ", + "blacklistAdd":"{0} ได้ถูกเพิ่มในบัญชีดำเรียบร้อยแล้ว", + "blacklistRemove":"{0} ได้ถูกถอดออกจากบัญชีดำเรียบร้อยแล้ว", + "blacklistGetSuccess":"{0} ตอนนี้อยู่ในบัญชีดำ", + "blacklistGetEmpty":"{0} ตอนนี้ยังไม่อยู่ในบัญชีดำ", + "blacklistViewEmpty":"ยังไม่มีใครถูกเพิ่มในบัญชีดำ", + "blacklistViewTip":"ใช้ \"/blacklist add\" เพื่อเพิ่มผู้ใช้ในบัญชีดำ", + "clearVerify":"คุณแน่ใจหรือไม่ว่าจะลบห้องติดต่อจำนวนมาก?\nการกระทำนี้ไม่สามารถย้อนกลับได้", + "clearReady":"{0} ห้องติดต่อได้ถูกลบเรียบร้อยแล้ว", + "rolesEmpty":"ไม่มีการอัปเดตบทบาทใดๆ", + + "autocloseLeave":"ห้องติดต่อนี้ถูกปิดอัตโนมัติเนื่องจากผู้สร้างห้องติดต่อออกจากเซิร์ฟเวอร์ไปแล้ว", + "autocloseTimeout":"ห้องติดต่อนี้ถูกปิดอัตโนมัติเนื่องจากไม่ได้รับการใช้งานเกิน `{0} ชั่วโมง`", + "autodeleteLeave":"ห้องติดต่อนี้ถูกลบอัตโนมัติเนื่องจากผู้สร้างออกจากเซิร์ฟเวอร์ไปแล้ว", + "autodeleteTimeout":"ห้องติดต่อนี้ถูกลบอัตโนมัติเนื่องจากไม่ได้รับการใช้งานเกิน `{0} วัน`", + "autocloseEnabled":"การปิดอัตโนมัติถูกเปิดใช้งานในห้องติดต่อนี้\nมันจะถูกปิดเมื่อไม่ได้ใช้งานเกิน `{0} ชั่วโมง`", + "autocloseDisabled":"การปิดอัตโนมัติถูกปิดใช้งานในห้องติดต่อนี้\nมันจะไม่ถูกปิดอัตโนมัติอีกต่อไป", + "autodeleteEnabled":"การลบอัตโนมัติถูกเปิดใช้งานในห้องติดต่อนี้\nมันจะถูกลบเมื่อไม่ได้ใช้งานเกิน `{0} วัน`", + "autodeleteDisabled":"การลบอัตโนมัติถูกปิดใช้งานในห้องติดต่อนี้\nมันจะไม่ถูกลบอัตโนมัติอีกต่อไป", + + "ticketMessageLimit":"คุณสามารถสร้างห้องติดต่อได้เพียง {0} ห้องต่อครั้ง", + "ticketMessageAutoclose":"ห้องติดต่อนี้จะถูกปิดอัตโนมัติเมื่อไม่ได้ใช้งานเกิน {0} ชั่วโมง", + "ticketMessageAutodelete":"ห้องติดต่อนี้จะถูกลบอัตโนมัติเมื่อไม่ได้ใช้งานเกิน {0} วัน", + "panelReady":"แผงควบคุมพร้อมใช้งานในข้อความถัดไป\nตอนนี้สามารถลบข้อความนี้ได้แล้ว", + + "topicSet":"หัวข้อของช่องถูกเปลี่ยนโดย {0} เรียบร้อยแล้ว", + "prioritySet":"ลำดับความสำคัญของห้องติดต่อถูกเปลี่ยนเป็น {0} โดย {1} เรียบร้อยแล้ว", + "priorityGet":"ลำดับความสำคัญปัจจุบันของห้องติดต่อคือ {0}", + "transfer":"ความเป็นเจ้าของห้องติดต่อถูกโอนจาก {0} ไปยัง {1} โดย {2} เรียบร้อยแล้ว" }, "modal":{ - "closePlaceholder": "ทำไมคุณถึงปิดห้องติดต่อนี้?", - "deletePlaceholder": "ทำไมคุณถึงลบห้องติดต่อนี้?", - "reopenPlaceholder": "ทำไมคุณถึงเปิดห้องติดต่อนี้ใหม่อีกรอบ?", - "claimPlaceholder": "ทำไมคุณถึงรับห้องติดต่อนี้?", - "unclaimPlaceholder": "ทำไมคุณถึงยกเลิกการรับห้องติดต่อนี้?", - "pinPlaceholder": "ทำไมคุณถึงปักหมุดห้องติดต่อนี้?", - "unpinPlaceholder": "ทำไมคุณถึงยกเลิกการปักหมุดห้องติดต่อนี้?" + "closePlaceholder":"ทำไมคุณถึงปิดห้องติดต่อนี้?", + "deletePlaceholder":"ทำไมคุณถึงลบห้องติดต่อนี้?", + "reopenPlaceholder":"ทำไมคุณถึงเปิดห้องติดต่อนี้ใหม่อีกรอบ?", + "claimPlaceholder":"ทำไมคุณถึงรับห้องติดต่อนี้?", + "unclaimPlaceholder":"ทำไมคุณถึงยกเลิกการรับห้องติดต่อนี้?", + "pinPlaceholder":"ทำไมคุณถึงปักหมุดห้องติดต่อนี้?", + "unpinPlaceholder":"ทำไมคุณถึงยกเลิกการปักหมุดห้องติดต่อนี้?" }, "logs":{ - "createLog": "ห้องติดต่อใหม่ถูกสร้างโดย {0}", - "closeLog": "ห้องติดต่อนี้ถูกปิดโดย {0}", - "closeDm": "ห้องติดต่อของคุณถูกปิดในเซิร์ฟเวอร์ของเรา", - "deleteLog": "ห้องติดต่อนี้ถูกลบโดย {0}", - "deleteDm": "ห้องติดต่อของคุณถูกลบในเซิร์ฟเวอร์ของเรา", - "reopenLog": "ห้องติดต่อนี้ถูกเปิดใหม่โดย {0}", - "reopenDm": "ห้องติดต่อของคุณถูกเปิดใหม่ในเซิร์ฟเวอร์ของเรา", - "claimLog": "ห้องติดต่อนี้ถูกรับโดย {0}", - "claimDm": "ห้องติดต่อของคุณถูกรับในเซิร์ฟเวอร์ของเรา", - "unclaimLog": "ห้องติดต่อนี้ถูกยกเลิกการรับโดย {0}", - "unclaimDm": "ห้องติดต่อของคุณถูกยกเลิกการรับในเซิร์ฟเวอร์ของเรา", - "pinLog": "ห้องติดต่อนี้ถูกปักหมุดโดย {0}", - "pinDm": "ห้องติดต่อของคุณถูกปักหมุดในเซิร์ฟเวอร์ของเรา", - "unpinLog": "ห้องติดต่อนี้ถูกยกเลิกการปักหมุดโดย {0}", - "unpinDm": "ห้องติดต่อของคุณถูกยกเลิกการปักหมุดในเซิร์ฟเวอร์ของเรา", - "renameLog": "ห้องติดต่อนี้ถูกเปลี่ยนชื่อเป็น {0} โดย {1}", - "renameDm": "ห้องติดต่อของคุณถูกเปลี่ยนชื่อเป็น {0} ในเซิร์ฟเวอร์ของเรา", - "moveLog": "ห้องติดต่อนี้ถูกย้ายไปที่ {0} โดย {1}", - "moveDm": "ห้องติดต่อของคุณถูกย้ายไปที่ {0} ในเซิร์ฟเวอร์ของเรา", - "addLog": "{0} ถูกเพิ่มเข้ามาในห้องติดต่อนี้โดย {1}", - "addDm": "{0} ถูกเพิ่มเข้าไปในห้องติดต่อของคุณในเซิร์ฟเวอร์ของเรา", - "removeLog": "{0} ถูกลบออกจากห้องติดต่อนี้โดย {1}", - "removeDm": "{0} ถูกลบออกจากห้องติดต่อของคุณในเซิร์ฟเวอร์ของเรา", + "createLog":"ห้องติดต่อใหม่ถูกสร้างโดย {0}", + "closeLog":"ห้องติดต่อนี้ถูกปิดโดย {0}", + "closeDm":"ห้องติดต่อของคุณถูกปิดในเซิร์ฟเวอร์ของเรา", + "deleteLog":"ห้องติดต่อนี้ถูกลบโดย {0}", + "deleteDm":"ห้องติดต่อของคุณถูกลบในเซิร์ฟเวอร์ของเรา", + "reopenLog":"ห้องติดต่อนี้ถูกเปิดใหม่โดย {0}", + "reopenDm":"ห้องติดต่อของคุณถูกเปิดใหม่ในเซิร์ฟเวอร์ของเรา", + "claimLog":"ห้องติดต่อนี้ถูกรับโดย {0}", + "claimDm":"ห้องติดต่อของคุณถูกรับในเซิร์ฟเวอร์ของเรา", + "unclaimLog":"ห้องติดต่อนี้ถูกยกเลิกการรับโดย {0}", + "unclaimDm":"ห้องติดต่อของคุณถูกยกเลิกการรับในเซิร์ฟเวอร์ของเรา", + "pinLog":"ห้องติดต่อนี้ถูกปักหมุดโดย {0}", + "pinDm":"ห้องติดต่อของคุณถูกปักหมุดในเซิร์ฟเวอร์ของเรา", + "unpinLog":"ห้องติดต่อนี้ถูกยกเลิกการปักหมุดโดย {0}", + "unpinDm":"ห้องติดต่อของคุณถูกยกเลิกการปักหมุดในเซิร์ฟเวอร์ของเรา", + "renameLog":"ห้องติดต่อนี้ถูกเปลี่ยนชื่อเป็น {0} โดย {1}", + "renameDm":"ห้องติดต่อของคุณถูกเปลี่ยนชื่อเป็น {0} ในเซิร์ฟเวอร์ของเรา", + "moveLog":"ห้องติดต่อนี้ถูกย้ายไปที่ {0} โดย {1}", + "moveDm":"ห้องติดต่อของคุณถูกย้ายไปที่ {0} ในเซิร์ฟเวอร์ของเรา", + "addLog":"{0} ถูกเพิ่มเข้ามาในห้องติดต่อนี้โดย {1}", + "addDm":"{0} ถูกเพิ่มเข้าไปในห้องติดต่อของคุณในเซิร์ฟเวอร์ของเรา", + "removeLog":"{0} ถูกลบออกจากห้องติดต่อนี้โดย {1}", + "removeDm":"{0} ถูกลบออกจากห้องติดต่อของคุณในเซิร์ฟเวอร์ของเรา", - "blacklistAddLog": "{0} ถูกเพิ่มในรายชื่อดำโดย {1}", - "blacklistRemoveLog": "{0} ถูกนำออกจากรายชื่อดำโดย {1}", - "blacklistAddDm": "คุณถูกเพิ่มในรายชื่อดำของเรา\nจากนี้ไปคุณจะไม่สามารถสร้างห้องติดต่อได้", - "blacklistRemoveDm": "คุณถูกนำออกจากรายชื่อดำในเซิร์ฟเวอร์ของเรา\nตอนนี้คุณสามารถสร้างห้องติดต่อได้อีกครั้ง", - "clearLog": "{0} ห้องติดต่อถูกลบโดย {1}" + "blacklistAddLog":"{0} ถูกเพิ่มในรายชื่อดำโดย {1}", + "blacklistRemoveLog":"{0} ถูกนำออกจากรายชื่อดำโดย {1}", + "blacklistAddDm":"คุณถูกเพิ่มในรายชื่อดำของเรา\nจากนี้ไปคุณจะไม่สามารถสร้างห้องติดต่อได้", + "blacklistRemoveDm":"คุณถูกนำออกจากรายชื่อดำในเซิร์ฟเวอร์ของเรา\nตอนนี้คุณสามารถสร้างห้องติดต่อได้อีกครั้ง", + "clearLog":"{0} ห้องติดต่อถูกลบโดย {1}", + + "transferLog":"ความเป็นเจ้าของห้องติดต่อนี้ถูกโอนจาก {0} ไปยัง {1} โดย {2}", + "transferDm":"ความเป็นเจ้าของห้องติดต่อของคุณถูกโอนจาก {0} ไปยัง {1} ในเซิร์ฟเวอร์ของเรา", + "prioritySetLog":"ลำดับความสำคัญของห้องติดต่อนี้ถูกเปลี่ยนเป็น {0} โดย {1}", + "prioritySetDm":"ลำดับความสำคัญของห้องติดต่อของคุณถูกเปลี่ยนเป็น {0} ในเซิร์ฟเวอร์ของเรา", + "roleUpdateLog":"{0} ได้อัปเดตบทบาทของพวกเขาแล้ว", + "roleUpdateDm":"บทบาทของคุณในเซิร์ฟเวอร์ของเราได้รับการอัปเดตแล้ว" } }, "transcripts":{ "success":{ - "visit": "เข้าดูตัวเก็บประวัติข้อความ", - "ready": "ตัวเก็บประวัติข้อความถูกสร้างแล้ว", - "textFileDescription": "นี่คือตัวเก็บประวัติข้อความในรูปแบบข้อความของห้องติดต่อที่ถูกลบ", - "htmlProgress": "กรุณารอสักครู่ ขณะที่ตัวเก็บประวัติข้อความในรูปแบบ HTML กำลังถูกประมวลผล...", - - "createdChannel": "ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในเซิร์ฟเวอร์", - "createdCreator": "ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นสำหรับหนึ่งในห้องติดต่อของคุณ", - "createdParticipant": "ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในหนึ่งในห้องติดต่อที่คุณได้เข้าร่วม", - "createdActiveAdmin": "ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในหนึ่งในห้องติดต่อที่คุณได้เข้าร่วมในฐานะผู้ดูแล", - "createdEveryAdmin": "ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในหนึ่งในห้องติดต่อที่คุณเป็นผู้ดูแล", - "createdOther": "ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้น" + "visit":"เข้าดูตัวเก็บประวัติข้อความ", + "ready":"ตัวเก็บประวัติข้อความถูกสร้างแล้ว", + "textFileDescription":"นี่คือตัวเก็บประวัติข้อความในรูปแบบข้อความของห้องติดต่อที่ถูกลบ", + "htmlProgress":"กรุณารอสักครู่ ขณะที่ตัวเก็บประวัติข้อความในรูปแบบ HTML กำลังถูกประมวลผล...", + + "createdChannel":"ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในเซิร์ฟเวอร์", + "createdCreator":"ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นสำหรับหนึ่งในห้องติดต่อของคุณ", + "createdParticipant":"ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในหนึ่งในห้องติดต่อที่คุณได้เข้าร่วม", + "createdActiveAdmin":"ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในหนึ่งในห้องติดต่อที่คุณได้เข้าร่วมในฐานะผู้ดูแล", + "createdEveryAdmin":"ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้นในหนึ่งในห้องติดต่อที่คุณเป็นผู้ดูแล", + "createdOther":"ตัวเก็บประวัติข้อความใหม่ {0} ถูกสร้างขึ้น" }, "errors":{ - "retry": "ลองอีกครั้ง", - "continue": "ลบโดยไม่สร้างตัวเก็บประวัติข้อความ", - "backup": "สร้างสำรองตัวเก็บประวัติข้อความ", - "error": "เกิดข้อผิดพลาดขณะพยายามสร้างตัวเก็บประวัติข้อความ\nคุณต้องการทำอย่างไรต่อไป?\n\nห้องติดต่อนี้จะไม่ถูกลบจนกว่าคุณจะคลิกปุ่มใดปุ่มหนึ่ง" + "retry":"ลองอีกครั้ง", + "continue":"ลบโดยไม่สร้างตัวเก็บประวัติข้อความ", + "backup":"สร้างสำรองตัวเก็บประวัติข้อความ", + "error":"เกิดข้อผิดพลาดขณะพยายามสร้างตัวเก็บประวัติข้อความ\nคุณต้องการทำอย่างไรต่อไป?\n\nห้องติดต่อนี้จะไม่ถูกลบจนกว่าคุณจะคลิกปุ่มใดปุ่มหนึ่ง", + "title":"ข้อผิดพลาดในการบันทึกบทสนทนา" + }, + "text":{ + "messagesTitle":"ข้อความ", + "embedTitle":"ข้อความเอ็มเบด", + "fileTitle":"ไฟล์", + "fieldsTitle":"หัวข้อย่อย", + "reactionsTitle":"รีแอคชั่น", + "statsTitle":"สถิติ", + "emptyContent":"<เนื้อหาว่างเปล่า>", + "noTitle":"<ไม่มีหัวข้อ>", + "noDesc":"<ไม่มีคำอธิบาย>" } }, "errors":{ "titles":{ - "internalError": "ข้อผิดพลาดภายใน", - "optionMissing": "ขาดตัวเลือกคำสั่ง", - "optionInvalid": "ตัวเลือกคำสั่งไม่ถูกต้อง", - "unknownCommand": "คำสั่งที่ไม่รู้จัก", - "noPermissions": "ไม่มีสิทธิ์", - "unknownTicket": "ห้องติดต่อที่ไม่รู้จัก", - "deprecatedTicket": "ห้องติดต่อที่ไม่รองรับแล้ว", - "unknownOption": "ตัวเลือกที่ไม่รู้จัก", - "unknownPanel": "แผงควบคุมที่ไม่รู้จัก", - "notInGuild": "ไม่ได้อยู่ในเซิร์ฟเวอร์", - "channelRename": "ไม่สามารถเปลี่ยนชื่อช่องได้", - "busy": "ห้องติดต่อยุ่งอยู่" + "internalError":"ข้อผิดพลาดภายใน", + "optionMissing":"ขาดตัวเลือกคำสั่ง", + "optionInvalid":"ตัวเลือกคำสั่งไม่ถูกต้อง", + "unknownCommand":"คำสั่งที่ไม่รู้จัก", + "noPermissions":"ไม่มีสิทธิ์", + "unknownTicket":"ห้องติดต่อที่ไม่รู้จัก", + "deprecatedTicket":"ห้องติดต่อที่ไม่รองรับแล้ว", + "unknownOption":"ตัวเลือกที่ไม่รู้จัก", + "unknownPanel":"แผงควบคุมที่ไม่รู้จัก", + "notInGuild":"ไม่ได้อยู่ในเซิร์ฟเวอร์", + "channelRename":"ไม่สามารถเปลี่ยนชื่อช่องได้", + "busy":"ห้องติดต่อยุ่งอยู่", + "permissionError":"ข้อผิดพลาดด้านสิทธิ์" }, "descriptions":{ - "askForInfo": "ติดต่อเจ้าของบอทนี้เพื่อขอข้อมูลเพิ่มเติม", - "askForInfoResolve": "ติดต่อเจ้าของบอทนี้หากปัญหานี้ยังไม่หายหลังจากลองหลายครั้ง", - "internalError": "ไม่สามารถตอบสนองคำสั่งนี้ {0} ได้เนื่องจากเกิดข้อผิดพลาดภายใน", - "optionMissing": "ตัวเลือกที่จำเป็นขาดหายไปในคำสั่งนี้", - "optionInvalid": "ตัวเลือกในคำสั่งนี้ไม่ถูกต้อง", - "optionInvalidChoose": "เลือกระหว่าง", - "unknownCommand": "ลองดูเมนูช่วยเหลือเพื่อข้อมูลเพิ่มเติม", - "noPermissions": "คุณไม่ได้รับอนุญาตให้ใช้ {0} นี้", - "noPermissionsList": "สิทธิ์ที่ต้องการ: (อย่างใดอย่างหนึ่ง)", - "noPermissionsCooldown": "คุณไม่ได้รับอนุญาตให้ใช้ {0} นี้เพราะคุณอยู่ในระยะเวลาคูลดาวน์", - "noPermissionsBlacklist": "คุณไม่ได้รับอนุญาตให้ใช้ {0} นี้เพราะคุณถูกเพิ่มในรายชื่อดำ", - "noPermissionsLimitGlobal": "คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากเซิร์ฟเวอร์ถึงขีดจำกัดจำนวนห้องติดต่อสูงสุด", - "noPermissionsLimitGlobalUser": "คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากคุณถึงขีดจำกัดจำนวนห้องติดต่อสูงสุดแล้ว", - "noPermissionsLimitOption": "คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากเซิร์ฟเวอร์ถึงขีดจำกัดจำนวนห้องติดต่อสูงสุดสำหรับตัวเลือกนี้", - "noPermissionsLimitOptionUser": "คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากคุณถึงขีดจำกัดจำนวนห้องติดต่อสูงสุดสำหรับตัวเลือกนี้แล้ว", - "unknownTicket": "ลองใช้คำสั่งนี้อีกครั้งในห้องติดต่อที่ถูกต้อง", - "deprecatedTicket": "ช่องนี้ไม่ใช่ห้องติดต่อที่ถูกต้อง อาจเป็นห้องติดต่อจากเวอร์ชันเก่าของ Open Ticket", - "notInGuild": "คำสั่ง {0} นี้ใช้ไม่ได้ใน DM กรุณาลองใหม่ในเซิร์ฟเวอร์", - "channelRename": "เนื่องจากข้อจำกัดของ Discord การเปลี่ยนชื่อช่องไม่สามารถทำได้ในขณะนี้ ช่องจะถูกเปลี่ยนชื่อโดยอัตโนมัติในเวลา 10 นาทีหากบอทไม่ถูกรีบูต", - "channelRenameSource": "แหล่งที่มาของข้อผิดพลาดนี้คือ: {0}", - "busy": "ไม่สามารถใช้ {0} นี้ได้\nห้องติดต่อกกำลังถูกประมวลผลโดยบอท\n\nกรุณาลองใหม่ในอีกไม่กี่วินาที" + "askForInfo":"ติดต่อเจ้าของบอทนี้เพื่อขอข้อมูลเพิ่มเติม", + "askForInfoResolve":"ติดต่อเจ้าของบอทนี้หากปัญหานี้ยังไม่หายหลังจากลองหลายครั้ง", + "internalError":"ไม่สามารถตอบสนองคำสั่งนี้ {0} ได้เนื่องจากเกิดข้อผิดพลาดภายใน", + "optionMissing":"ตัวเลือกที่จำเป็นขาดหายไปในคำสั่งนี้", + "optionInvalid":"ตัวเลือกในคำสั่งนี้ไม่ถูกต้อง", + "optionInvalidChoose":"เลือกระหว่าง", + "unknownCommand":"ลองดูเมนูช่วยเหลือเพื่อข้อมูลเพิ่มเติม", + "noPermissions":"คุณไม่ได้รับอนุญาตให้ใช้ {0} นี้", + "noPermissionsList":"สิทธิ์ที่ต้องการ: (อย่างใดอย่างหนึ่ง)", + "noPermissionsCooldown":"คุณไม่ได้รับอนุญาตให้ใช้ {0} นี้เพราะคุณอยู่ในระยะเวลาคูลดาวน์", + "noPermissionsBlacklist":"คุณไม่ได้รับอนุญาตให้ใช้ {0} นี้เพราะคุณถูกเพิ่มในรายชื่อดำ", + "noPermissionsLimitGlobal":"คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากเซิร์ฟเวอร์ถึงขีดจำกัดจำนวนห้องติดต่อสูงสุด", + "noPermissionsLimitGlobalUser":"คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากคุณถึงขีดจำกัดจำนวนห้องติดต่อสูงสุดแล้ว", + "noPermissionsLimitOption":"คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากเซิร์ฟเวอร์ถึงขีดจำกัดจำนวนห้องติดต่อสูงสุดสำหรับตัวเลือกนี้", + "noPermissionsLimitOptionUser":"คุณไม่ได้รับอนุญาตให้สร้างห้องติดต่อเนื่องจากคุณถึงขีดจำกัดจำนวนห้องติดต่อสูงสุดสำหรับตัวเลือกนี้แล้ว", + "unknownTicket":"ลองใช้คำสั่งนี้อีกครั้งในห้องติดต่อที่ถูกต้อง", + "deprecatedTicket":"ช่องนี้ไม่ใช่ห้องติดต่อที่ถูกต้อง อาจเป็นห้องติดต่อจากเวอร์ชันเก่าของ Open Ticket", + "notInGuild":"คำสั่ง {0} นี้ใช้ไม่ได้ใน DM กรุณาลองใหม่ในเซิร์ฟเวอร์", + "channelRename":"เนื่องจากข้อจำกัดของ Discord การเปลี่ยนชื่อช่องไม่สามารถทำได้ในขณะนี้ ช่องจะถูกเปลี่ยนชื่อโดยอัตโนมัติในเวลา 10 นาทีหากบอทไม่ถูกรีบูต", + "channelRenameSource":"แหล่งที่มาของข้อผิดพลาดนี้คือ: {0}", + "busy":"ไม่สามารถใช้ {0} นี้ได้\nห้องติดต่อกกำลังถูกประมวลผลโดยบอท\n\nกรุณาลองใหม่ในอีกไม่กี่วินาที", + "closeBeforeMessage":"ไม่สามารถปิด/ลบห้องติดต่อนี้ได้ก่อนที่ผู้ใช้จะส่งข้อความ", + "closeBeforeAdminMessage":"ไม่สามารถปิด/ลบห้องติดต่อนี้ได้ก่อนที่ผู้ดูแลห้องติดต่อหรือสมาชิกฝ่ายสนับสนุนจะส่งข้อความ", + "unableToCreateTicket":"คุณไม่สามารถสร้างห้องติดต่อได้" }, "optionInvalidReasons":{ - "stringRegex": "ค่าที่ระบุไม่ตรงกับรูปแบบ", - "stringMinLength": "ค่าจะต้องมีความยาวอย่างน้อย {0} ตัวอักษร", - "stringMaxLength": "ค่าจะต้องมีความยาวไม่เกิน {0} ตัวอักษร", - "numberInvalid": "หมายเลขไม่ถูกต้อง", - "numberMin": "หมายเลขจะต้องมีค่ามากกว่าหรือเท่ากับ {0}", - "numberMax": "หมายเลขจะต้องมีค่าน้อยกว่าหรือเท่ากับ {0}", - "numberDecimal": "หมายเลขไม่สามารถเป็นทศนิยมได้", - "numberNegative": "หมายเลขไม่สามารถเป็นลบได้", - "numberPositive": "หมายเลขไม่สามารถเป็นบวกได้", - "numberZero": "หมายเลขไม่สามารถเป็นศูนย์ได้", - "channelNotFound": "ไม่สามารถหาช่องได้", - "userNotFound": "ไม่สามารถหาผู้ใช้ได้", - "roleNotFound": "ไม่สามารถหาบทบาทได้", - "memberNotFound": "ไม่สามารถหาผู้ใช้ได้", - "mentionableNotFound": "ไม่สามารถหาผู้ใช้หรือบทบาทได้", - "channelType": "ประเภทช่องไม่ถูกต้อง", - "notInGuild": "ตัวเลือกนี้ต้องให้คุณอยู่ในเซิร์ฟเวอร์" + "stringRegex":"ค่าที่ระบุไม่ตรงกับรูปแบบ", + "stringMinLength":"ค่าจะต้องมีความยาวอย่างน้อย {0} ตัวอักษร", + "stringMaxLength":"ค่าจะต้องมีความยาวไม่เกิน {0} ตัวอักษร", + "numberInvalid":"หมายเลขไม่ถูกต้อง", + "numberMin":"หมายเลขจะต้องมีค่ามากกว่าหรือเท่ากับ {0}", + "numberMax":"หมายเลขจะต้องมีค่าน้อยกว่าหรือเท่ากับ {0}", + "numberDecimal":"หมายเลขไม่สามารถเป็นทศนิยมได้", + "numberNegative":"หมายเลขไม่สามารถเป็นลบได้", + "numberPositive":"หมายเลขไม่สามารถเป็นบวกได้", + "numberZero":"หมายเลขไม่สามารถเป็นศูนย์ได้", + "channelNotFound":"ไม่สามารถหาช่องได้", + "userNotFound":"ไม่สามารถหาผู้ใช้ได้", + "roleNotFound":"ไม่สามารถหาบทบาทได้", + "memberNotFound":"ไม่สามารถหาผู้ใช้ได้", + "mentionableNotFound":"ไม่สามารถหาผู้ใช้หรือบทบาทได้", + "channelType":"ประเภทช่องไม่ถูกต้อง", + "notInGuild":"ตัวเลือกนี้ต้องให้คุณอยู่ในเซิร์ฟเวอร์" }, "permissions":{ - "developer": "คุณต้องเป็นนักพัฒนาของบอทนี้", - "owner": "คุณต้องเป็นเจ้าของเซิร์ฟเวอร์", - "admin": "คุณต้องเป็นผู้ดูแลเซิร์ฟเวอร์", - "moderator": "คุณต้องเป็นผู้ดูแล", - "support": "คุณต้องเป็นสมาชิกทีม", - "member": "คุณต้องเป็นสมาชิก", - "discord-administrator": "คุณต้องมีสิทธิ์ `ADMINISTRATOR`" + "developer":"คุณต้องเป็นนักพัฒนาของบอทนี้", + "owner":"คุณต้องเป็นเจ้าของเซิร์ฟเวอร์", + "admin":"คุณต้องเป็นผู้ดูแลเซิร์ฟเวอร์", + "moderator":"คุณต้องเป็นผู้ดูแล", + "support":"คุณต้องเป็นสมาชิกทีม", + "member":"คุณต้องเป็นสมาชิก", + "discord-administrator":"คุณต้องมีสิทธิ์ `ADMINISTRATOR`" }, "actionInvalid":{ - "close": "ห้องติดต่อนี้ถูกปิดแล้ว", - "reopen": "ห้องติดต่อนี้ยังไม่ถูกปิด", - "claim": "ห้องติดต่อนี้รับแล้ว", - "unclaim": "ห้องติดต่อนี้ยังไม่ได้ถูกรับ", - "pin": "ห้องติดต่อนี้ถูกปักหมุดแล้ว", - "unpin": "ห้องติดต่อนี้ยังไม่ได้ถูกปักหมุด", - "add": "ผู้ใช้นี้สามารถเข้าถึงห้องติดต่อได้แล้ว", - "remove": "ไม่สามารถลบผู้ใช้นี้ออกจากห้องติดต่อได้" + "close":"ห้องติดต่อนี้ถูกปิดแล้ว", + "reopen":"ห้องติดต่อนี้ยังไม่ถูกปิด", + "claim":"ห้องติดต่อนี้รับแล้ว", + "unclaim":"ห้องติดต่อนี้ยังไม่ได้ถูกรับ", + "pin":"ห้องติดต่อนี้ถูกปักหมุดแล้ว", + "unpin":"ห้องติดต่อนี้ยังไม่ได้ถูกปักหมุด", + "add":"ผู้ใช้นี้สามารถเข้าถึงห้องติดต่อได้แล้ว", + "remove":"ไม่สามารถลบผู้ใช้นี้ออกจากห้องติดต่อได้" } }, "params":{ "uppercase":{ - "ticket": "ห้องติดต่อ", - "tickets": "ห้องติดต่อ", - "reason": "เหตุผล", - "creator": "ผู้สร้าง", - "remaining": "เวลาที่เหลือ", - "added": "เพิ่มแล้ว", - "removed": "ลบแล้ว", - "filter": "ตัวกรอง", - "claimedBy": "รับโดย {0}", - "method": "วิธีการ", - "type": "ประเภท", - "blacklisted": "ถูกใส่ในรายชื่อดำ", - "panel": "แผงควบคุม", - "command": "คำสั่ง", - "system": "ระบบ", - "true": "จริง", - "false": "เท็จ", - "syntax": "ไวยากรณ์", - "originalName": "ชื่อเดิม", - "newName": "ชื่อใหม่", - "until": "จนถึง", - "validOptions": "ตัวเลือกที่ถูกต้อง", - "validPanels": "แผงควบคุมที่ถูกต้อง", - "autoclose": "ปิดอัตโนมัติ", - "autodelete": "ลบอัตโนมัติ", - "startupDate": "วันที่เริ่มต้น", - "version": "เวอร์ชัน", - "name": "ชื่อ", - "role": "บทบาท", - "status": "สถานะ", - "claimed": "รับแล้ว", - "pinned": "ถูกปักหมุด", - "creationDate": "วันที่สร้าง" + "ticket":"ห้องติดต่อ", + "tickets":"ห้องติดต่อ", + "reason":"เหตุผล", + "creator":"ผู้สร้าง", + "remaining":"เวลาที่เหลือ", + "added":"เพิ่มแล้ว", + "removed":"ลบแล้ว", + "filter":"ตัวกรอง", + "method":"วิธีการ", + "type":"ประเภท", + "blacklisted":"ถูกใส่ในรายชื่อดำ", + "panel":"แผงควบคุม", + "command":"คำสั่ง", + "system":"ระบบ", + "true":"จริง", + "false":"เท็จ", + "syntax":"ไวยากรณ์", + "originalName":"ชื่อเดิม", + "newName":"ชื่อใหม่", + "until":"จนถึง", + "validOptions":"ตัวเลือกที่ถูกต้อง", + "validPanels":"แผงควบคุมที่ถูกต้อง", + "autoclose":"ปิดอัตโนมัติ", + "autodelete":"ลบอัตโนมัติ", + "startupDate":"วันที่เริ่มต้น", + "version":"เวอร์ชัน", + "name":"ชื่อ", + "role":"บทบาท", + "status":"สถานะ", + "claimed":"รับแล้ว", + "pinned":"ถูกปักหมุด", + "creationDate":"วันที่สร้าง", + + "noone":"ไม่มีใคร", + "open":"เปิด", + "closed":"ปิด", + "priority":"ลำดับความสำคัญ", + "participants":"ผู้มีส่วนร่วม", + "yes":"ใช่", + "no":"ไม่", + "option":"ตัวเลือก", + "topic":"หัวข้อ", + "uptime":"เวลาทำงานของระบบ", + "messages":"ข้อความ", + "embeds":"ข้อความเอ็มเบด", + "files":"หัวข้อย่อย", + "components":"ส่วนประกอบ", + "cooldown":"คูลดาวน์", + "maxTickets":"ห้องติดต่อสูงสุด", + "admins":"ผู้ดูแลระบบ", + "roles":"บทบาท", + "size":"ขนาด" }, "lowercase":{ - "text": "ข้อความ", - "html": "HTML", - "command": "คำสั่ง", - "modal": "หน้าต่าง", - "button": "ปุ่ม", - "dropdown": "ตัวเลือก", - "method": "วิธีการ" + "text":"ข้อความ", + "html":"HTML", + "command":"คำสั่ง", + "modal":"หน้าต่าง", + "button":"ปุ่ม", + "dropdown":"ตัวเลือก", + "method":"วิธีการ" } }, "commands":{ - "reason": "ระบุเหตุผลที่ไม่บังคับ ซึ่งจะปรากฏในบันทึก", - "help": "รับรายชื่อคำสั่งทั้งหมดที่มี", - "panel": "สร้างข้อความที่มีตัวเลือกหรือปุ่ม (สำหรับการสร้างห้องติดต่อ)", - "panelId": "ตัวระบุของแผงควบคุมที่คุณต้องการสร้าง", - "panelAutoUpdate": "คุณต้องการให้แผงนี้อัปเดตอัตโนมัติเมื่อมีการแก้ไขหรือไม่?", - "ticket": "สร้างห้องติดต่อทันที", - "ticketId": "ตัวระบุของห้องติดต่อที่คุณต้องการสร้าง", - "close": "ปิดห้องติดต่อ", - "delete": "ลบห้องติดต่อ", - "deleteNoTranscript": "ลบห้องติดต่อนี้โดยไม่สร้างตัวเก็บข้อความ", - "reopen": "เปิดห้องติดต่อใหม่", - "claim": "รับห้องติดต่อ", - "claimUser": "รับห้องติดต่อนี้ให้กับผู้อื่นแทนตัวคุณ", - "unclaim": "ยกเลิกการรับห้องติดต่อ", - "pin": "ปักหมุดห้องติดต่อ", - "unpin": "ยกเลิกการปักหมุดห้องติดต่อ", - "move": "ย้ายห้องติดต่อ", - "moveId": "ตัวระบุของตัวเลือกที่คุณต้องการย้ายไป", - "rename": "เปลี่ยนชื่อห้องติดต่อ", - "renameName": "ชื่อใหม่สำหรับห้องติดต่อนี้", - "add": "เพิ่มผู้ใช้ในห้องติดต่อ", - "addUser": "ผู้ใช้ที่ต้องการเพิ่ม", - "remove": "ลบผู้ใช้จากห้องติดต่อ", - "removeUser": "ผู้ใช้ที่ต้องการลบ", - "blacklist": "จัดการรายชื่อดำของห้องติดต่อ", - "blacklistView": "ดูรายชื่อของผู้ที่อยู่ในรายชื่อดำ", - "blacklistAdd": "เพิ่มผู้ใช้ในรายชื่อดำ", - "blacklistRemove": "ลบผู้ใช้จากรายชื่อดำ", - "blacklistGet": "ดึงข้อมูลจากผู้ใช้ที่อยู่ในรายชื่อดำ", - "blacklistGetUser": "ผู้ใช้ที่ต้องการดึงข้อมูล", - "stats": "ดูสถิติเกี่ยวกับบอท, สมาชิก หรือ ห้องติดต่อ", - "statsReset": "รีเซ็ตสถิติทั้งหมดของบอท (และเริ่มนับจากศูนย์)", - "statsGlobal": "ดูสถิติทั้งหมด", - "statsUser": "ดูสถิติจากผู้ใช้ในเซิร์ฟเวอร์", - "statsUserUser": "ผู้ใช้ที่ต้องการดูสถิติ", - "statsTicket": "ดูสถิติของห้องติดต่อในเซิร์ฟเวอร์", - "statsTicketTicket": "ห้องติดต่อที่ต้องการดูสถิติ", - "clear": "ลบห้องติดต่อจำนวนมากในคราวเดียว", - "clearFilter": "ตัวกรองสำหรับการลบห้องติดต่อ", + "reason":"ระบุเหตุผลที่ไม่บังคับ ซึ่งจะปรากฏในบันทึก", + "help":"รับรายชื่อคำสั่งทั้งหมดที่มี", + "panel":"สร้างข้อความที่มีตัวเลือกหรือปุ่ม (สำหรับการสร้างห้องติดต่อ)", + "panelId":"ตัวระบุของแผงควบคุมที่คุณต้องการสร้าง", + "panelAutoUpdate":"คุณต้องการให้แผงนี้อัปเดตอัตโนมัติเมื่อมีการแก้ไขหรือไม่?", + "ticket":"สร้างห้องติดต่อทันที", + "ticketId":"ตัวระบุของห้องติดต่อที่คุณต้องการสร้าง", + "close":"ปิดห้องติดต่อ", + "delete":"ลบห้องติดต่อ", + "deleteNoTranscript":"ลบห้องติดต่อนี้โดยไม่สร้างตัวเก็บข้อความ", + "reopen":"เปิดห้องติดต่อใหม่", + "claim":"รับห้องติดต่อ", + "claimUser":"รับห้องติดต่อนี้ให้กับผู้อื่นแทนตัวคุณ", + "unclaim":"ยกเลิกการรับห้องติดต่อ", + "pin":"ปักหมุดห้องติดต่อ", + "unpin":"ยกเลิกการปักหมุดห้องติดต่อ", + + "move":"ย้ายห้องติดต่อ", + "moveId":"ตัวระบุของตัวเลือกที่คุณต้องการย้ายไป", + "rename":"เปลี่ยนชื่อห้องติดต่อ", + "renameName":"ชื่อใหม่สำหรับห้องติดต่อนี้", + "add":"เพิ่มผู้ใช้ในห้องติดต่อ", + "addUser":"ผู้ใช้ที่ต้องการเพิ่ม", + "remove":"ลบผู้ใช้จากห้องติดต่อ", + "removeUser":"ผู้ใช้ที่ต้องการลบ", + + "blacklist":"จัดการรายชื่อดำของห้องติดต่อ", + "blacklistView":"ดูรายชื่อของผู้ที่อยู่ในรายชื่อดำ", + "blacklistAdd":"เพิ่มผู้ใช้ในรายชื่อดำ", + "blacklistRemove":"ลบผู้ใช้จากรายชื่อดำ", + "blacklistGet":"ดึงข้อมูลจากผู้ใช้ที่อยู่ในรายชื่อดำ", + "blacklistGetUser":"ผู้ใช้ที่ต้องการดึงข้อมูล", + "stats":"ดูสถิติเกี่ยวกับบอท, สมาชิก หรือ ห้องติดต่อ", + "statsReset":"รีเซ็ตสถิติทั้งหมดของบอท (และเริ่มนับจากศูนย์)", + "statsGlobal":"ดูสถิติทั้งหมด", + "statsUser":"ดูสถิติจากผู้ใช้ในเซิร์ฟเวอร์", + "statsUserUser":"ผู้ใช้ที่ต้องการดูสถิติ", + "statsTicket":"ดูสถิติของห้องติดต่อในเซิร์ฟเวอร์", + "statsTicketTicket":"ห้องติดต่อที่ต้องการดูสถิติ", + + "clear":"ลบห้องติดต่อจำนวนมากในคราวเดียว", + "clearFilter":"ตัวกรองสำหรับการลบห้องติดต่อ", "clearFilters":{ - "all": "ทั้งหมด", - "open": "เปิด", - "close": "ปิด", - "claim": "ถูกรับ", - "unclaim": "ยังไม่ได้รับ", - "pin": "ถูกปักหมุด", - "unpin": "ยังไม่ได้ถูกปักหมุด", - "autoclose": "ปิดอัตโนมัติ" + "all":"ทั้งหมด", + "open":"เปิด", + "close":"ปิด", + "claim":"ถูกรับ", + "unclaim":"ยังไม่ได้รับ", + "pin":"ถูกปักหมุด", + "unpin":"ยังไม่ได้ถูกปักหมุด", + "autoclose":"ปิดอัตโนมัติ" }, - "autoclose": "จัดการการปิดอัตโนมัติในห้องติดต่อ", - "autocloseDisable": "ปิดการปิดอัตโนมัติในห้องติดต่อนี้", - "autocloseEnable": "เปิดการปิดอัตโนมัติในห้องติดต่อนี้", - "autocloseEnableTime": "จำนวนชั่วโมงที่ห้องติดต่อนี้ต้องไม่เคลื่อนไหวเพื่อปิดมัน", - "autodelete": "จัดการการลบอัตโนมัติในห้องติดต่อ", - "autodeleteDisable": "ปิดการลบอัตโนมัติในห้องติดต่อนี้", - "autodeleteEnable": "เปิดการลบอัตโนมัติในห้องติดต่อนี้", - "autodeleteEnableTime": "จำนวนวันที่ห้องติดต่อนี้ต้องไม่เคลื่อนไหวเพื่อที่จะลบมัน" + + "autoclose":"จัดการการปิดอัตโนมัติในห้องติดต่อ", + "autocloseDisable":"ปิดการปิดอัตโนมัติในห้องติดต่อนี้", + "autocloseEnable":"เปิดการปิดอัตโนมัติในห้องติดต่อนี้", + "autocloseEnableTime":"จำนวนชั่วโมงที่ห้องติดต่อนี้ต้องไม่เคลื่อนไหวเพื่อปิดมัน", + "autodelete":"จัดการการลบอัตโนมัติในห้องติดต่อ", + "autodeleteDisable":"ปิดการลบอัตโนมัติในห้องติดต่อนี้", + "autodeleteEnable":"เปิดการลบอัตโนมัติในห้องติดต่อนี้", + "autodeleteEnableTime":"จำนวนวันที่ห้องติดต่อนี้ต้องไม่เคลื่อนไหวเพื่อที่จะลบมัน", + + "topic":"จัดการหัวข้อของช่องห้องติดต่อ", + "topicSet":"ตั้งค่าหัวข้อของช่องห้องติดต่อ", + "topicValue":"หัวข้อใหม่ของช่อง", + "topicList":"รายการห้องติดต่อทั้งหมดพร้อมหัวข้อและสถิติ", + "priority":"จัดการลำดับความสำคัญของห้องติดต่อ", + "prioritySet":"ตั้งค่าลำดับความสำคัญของห้องติดต่อ", + "priorityValue":"ลำดับความสำคัญของช่อง", + "priorityGet":"รับลำดับความสำคัญของห้องติดต่อ", + "priorityList":"รายการห้องติดต่อทั้งหมดพร้อมสถานะลำดับความสำคัญ", + "transfer":"โอนความเป็นเจ้าของห้องติดต่อจากผู้ใช้หนึ่งไปยังอีกคนหนึ่ง", + "transferUser":"ผู้ใช้ที่จะโอนไป" }, "helpMenu":{ - "help": "รับรายชื่อคำสั่งทั้งหมดที่มี", - "ticket": "สร้างห้องติดต่อทันที", - "close": "ปิดห้องติดต่อ การเขียนในช่องนี้จะถูกปิด", - "delete": "ลบห้องติดต่อ จะสร้างตัวเก็บข้อมูลข้อความเมื่อเปิดใช้งาน", - "reopen": "เปิดห้องติดต่อใหม่, การเขียนในช่องนี้จะถูกเปิดใช้อีกครั้ง", - "pin": "ปักหมุดห้องติดต่อ ห้องติดต่อนี้จะย้ายไปอยู่ด้านบนและจะเพิ่มอิโมจิ '📌' ในชื่อ", - "unpin": "ยกเลิกการปักหมุดห้องติดต่อ ห้องติดต่อจะยังคงอยู่ในตำแหน่งเดิมแต่จะสูญเสียอิโมจิ '📌'", - "move": "ย้ายห้องติดต่อ การย้ายนี้จะเปลี่ยนประเภทของห้องติดต่อนี้", - "rename": "เปลี่ยนชื่อห้องติดต่อ การเปลี่ยนนี้จะเปลี่ยนชื่อช่องของห้องติดต่อนี้", - "claim": "รับห้องติดต่อ ด้วยคำสั่งนี้คุณสามารถแจ้งให้ทีมของคุณทราบว่าคุณกำลังจัดการห้องติดต่อนี้", - "unclaim": "ยกเลิกการรับห้องติดต่อ ด้วยคำสั่งนี้คุณสามารถแจ้งให้ทีมของคุณทราบว่าห้องติดต่อนี้ว่างแล้ว", - "add": "เพิ่มผู้ใช้ในห้องติดต่อ การทำเช่นนี้จะอนุญาตให้ผู้ใช้ที่ถูกเพิ่มเข้ามาสามารถอ่านและเขียนในห้องติดต่อนี้ได้", - "remove": "ลบผู้ใช้จากห้องติดต่อ การทำเช่นนี้จะลบสิทธิ์ในการอ่านและเขียนสำหรับผู้ใช้ในห้องติดต่อนี้", - "panel": "สร้างข้อความที่มีตัวเลือกหรือปุ่ม (สำหรับการสร้างห้องติดต่อ)", - "blacklistView": "ดูรายชื่อของผู้ที่อยู่ในรายชื่อดำ", - "blacklistAdd": "เพิ่มผู้ใช้ในรายชื่อดำ", - "blacklistRemove": "ลบผู้ใช้จากรายชื่อดำ", - "blacklistGet": "ดึงข้อมูลจากผู้ใช้ที่อยู่ในรายชื่อดำ", - "statsGlobal": "ดูสถิติรวม", - "statsTicket": "ดูสถิติของห้องติดต่อในเซิร์ฟเวอร์", - "statsUser": "ดูสถิติจากผู้ใช้ในเซิร์ฟเวอร์", - "statsReset": "รีเซ็ตสถิติทั้งหมดของบอท (และเริ่มนับจากศูนย์)", - "autocloseDisable": "ปิดการปิดอัตโนมัติในห้องติดต่อนี้", - "autocloseEnable": "เปิดการปิดอัตโนมัติในห้องติดต่อนี้", - "autodeleteDisable": "ปิดการลบอัตโนมัติในห้องติดต่อนี้", - "autodeleteEnable": "เปิดการลบอัตโนมัติในห้องติดต่อนี้" + "help":"รับรายชื่อคำสั่งทั้งหมดที่มี", + "ticket":"สร้างห้องติดต่อทันที", + "close":"ปิดห้องติดต่อ การเขียนในช่องนี้จะถูกปิด", + "delete":"ลบห้องติดต่อ จะสร้างตัวเก็บข้อมูลข้อความเมื่อเปิดใช้งาน", + "reopen":"เปิดห้องติดต่อใหม่, การเขียนในช่องนี้จะถูกเปิดใช้อีกครั้ง", + "pin":"ปักหมุดห้องติดต่อ ห้องติดต่อนี้จะย้ายไปอยู่ด้านบนและจะเพิ่มอิโมจิ '📌' ในชื่อ", + "unpin":"ยกเลิกการปักหมุดห้องติดต่อ ห้องติดต่อจะยังคงอยู่ในตำแหน่งเดิมแต่จะสูญเสียอิโมจิ '📌'", + "move":"ย้ายห้องติดต่อ การย้ายนี้จะเปลี่ยนประเภทของห้องติดต่อนี้", + "rename":"เปลี่ยนชื่อห้องติดต่อ การเปลี่ยนนี้จะเปลี่ยนชื่อช่องของห้องติดต่อนี้", + "claim":"รับห้องติดต่อ ด้วยคำสั่งนี้คุณสามารถแจ้งให้ทีมของคุณทราบว่าคุณกำลังจัดการห้องติดต่อนี้", + "unclaim":"ยกเลิกการรับห้องติดต่อ ด้วยคำสั่งนี้คุณสามารถแจ้งให้ทีมของคุณทราบว่าห้องติดต่อนี้ว่างแล้ว", + "add":"เพิ่มผู้ใช้ในห้องติดต่อ การทำเช่นนี้จะอนุญาตให้ผู้ใช้ที่ถูกเพิ่มเข้ามาสามารถอ่านและเขียนในห้องติดต่อนี้ได้", + "remove":"ลบผู้ใช้จากห้องติดต่อ การทำเช่นนี้จะลบสิทธิ์ในการอ่านและเขียนสำหรับผู้ใช้ในห้องติดต่อนี้", + "panel":"สร้างข้อความที่มีตัวเลือกหรือปุ่ม (สำหรับการสร้างห้องติดต่อ)", + "blacklistView":"ดูรายชื่อของผู้ที่อยู่ในรายชื่อดำ", + "blacklistAdd":"เพิ่มผู้ใช้ในรายชื่อดำ", + "blacklistRemove":"ลบผู้ใช้จากรายชื่อดำ", + "blacklistGet":"ดึงข้อมูลจากผู้ใช้ที่อยู่ในรายชื่อดำ", + "statsGlobal":"ดูสถิติรวม", + "statsTicket":"ดูสถิติของห้องติดต่อในเซิร์ฟเวอร์", + "statsUser":"ดูสถิติจากผู้ใช้ในเซิร์ฟเวอร์", + "statsReset":"รีเซ็ตสถิติทั้งหมดของบอท (และเริ่มนับจากศูนย์)", + "autocloseDisable":"ปิดการปิดอัตโนมัติในห้องติดต่อนี้", + "autocloseEnable":"เปิดการปิดอัตโนมัติในห้องติดต่อนี้", + "autodeleteDisable":"ปิดการลบอัตโนมัติในห้องติดต่อนี้", + "autodeleteEnable":"เปิดการลบอัตโนมัติในห้องติดต่อนี้", + "categories":{ + "general":"คำสั่งทั่วไป", + "basicTicket":"คำสั่งห้องติดต่อทั่วไป", + "advancedTicket":"คำสั่งห้องติดต่อขั้นสูง", + "userTicket":"คำสั่งห้องติดต่อผู้ใช้", + "admin":"คำสั่งผู้ดูแลระบบ", + "advanced":"คำสั่งขั้นสูง", + "extra":"คำสั่งพิเศษ" + } }, "stats":{ "scopes":{ - "global": "สถิติรวม", - "system": "สถิติเกี่ยวกับระบบ", - "user": "สถิติเกี่ยวกับผู้ใช้", - "ticket": "สถิติห้องติดต่อ", - "participants": "ผู้มีส่วนร่วม" + "global":"สถิติรวม", + "system":"สถิติเกี่ยวกับระบบ", + "user":"สถิติเกี่ยวกับผู้ใช้", + "ticket":"สถิติห้องติดต่อ", + "participants":"ผู้มีส่วนร่วม", + "messages":"ข้อความ" }, "properties":{ - "ticketsCreated": "ห้องติดต่อที่สร้าง", - "ticketsClosed": "ห้องติดต่อที่ปิด", - "ticketsDeleted": "ห้องติดต่อที่ลบ", - "ticketsReopened": "ห้องติดต่อที่เปิดใหม่", - "ticketsAutoclosed": "ห้องติดต่อที่ปิดอัตโนมัติ", - "ticketsClaimed": "ห้องติดต่อที่ถูกรับ", - "ticketsPinned": "ห้องติดต่อที่ถูกปักหมุด", - "ticketsMoved": "ห้องติดต่อที่ย้าย", - "usersBlacklisted": "ผู้ใช้ที่ถูกใส่ในรายชื่อดำ", - "transcriptsCreated": "บันทึกการสนทนาที่สร้าง" + "ticketsCreated":"ห้องติดต่อที่สร้าง", + "ticketsClosed":"ห้องติดต่อที่ปิด", + "ticketsDeleted":"ห้องติดต่อที่ลบ", + "ticketsReopened":"ห้องติดต่อที่เปิดใหม่", + "ticketsAutoclosed":"ห้องติดต่อที่ปิดอัตโนมัติ", + "ticketsClaimed":"ห้องติดต่อที่ถูกรับ", + "ticketsPinned":"ห้องติดต่อที่ถูกปักหมุด", + "ticketsMoved":"ห้องติดต่อที่ย้าย", + "usersBlacklisted":"ผู้ใช้ที่ถูกใส่ในรายชื่อดำ", + "transcriptsCreated":"บันทึกการสนทนาที่สร้าง", + "ticketsAutodeleted":"ห้องติดต่อที่ถูกลบอัตโนมัติ", + "ticketsTransferred":"ห้องติดต่อที่โอนแล้ว", + "ticketVolume":"ปริมาณห้องติดต่อ", + "averageTickets":"ห้องติดต่อเฉลี่ย/ผู้ใช้", + "currentTickets":"ห้องติดต่อปัจจุบัน", + "age":"อายุห้องติดต่อ", + "responseTime":"เวลาที่ใช้ตอบกลับ", + "resolutionTime":"เวลาที่ใช้แก้ไขปัญหา", + "createdOn":"สร้างเมื่อ", + "createdBy":"สร้างโดย", + "closedOn":"ปิดเมื่อ", + "closedBy":"ปิดโดย", + "claimedOn":"รับเรื่องเมื่อ", + "claimedBy":"รับเรื่องโดย", + "pinnedOn":"ปักหมุดเมื่อ", + "pinnedBy":"ปักหมุดโดย", + "deletedOn":"ลบเมื่อ", + "deletedBy":"ลบโดย" + }, + "roles":{ + "developer":"ผู้พัฒนา", + "serverOwner":"เจ้าของเซิร์ฟเวอร์", + "serverAdmin":"ผู้ดูแลเซิร์ฟเวอร์", + "moderator":"ทีมผู้ดูแล", + "support":"ทีมสนับสนุน", + "member":"สมาชิก" } + }, + "panel":{ + "selectTicket":"เลือกห้องติดต่อของคุณ", + "selectRole":"เลือกบทบาทของคุณ", + "selectOption":"เลือกตัวเลือกของคุณ" + }, + "priorities":{ + "urgent":"เร่งด่วน", + "veryHigh":"สูงมาก", + "high":"สูง", + "normal":"ปกติ", + "low":"ต่ำ", + "veryLow":"ต่ำมาก", + "none":"ไม่มี" } } \ No newline at end of file From e1a6feea6e079861e1a3b8cc6eea08c4853fd816 Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:05:53 +0100 Subject: [PATCH 78/78] Updated README.md language list --- README.md | 79 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index aaaf611..def8dbc 100644 --- a/README.md +++ b/README.md @@ -137,45 +137,46 @@ With the amazing support of our translators, we've been able to translate Open T - **🟠 Incomplete** - **🔴 Unavailable/Outdated** -|🔍 |Languages (36) |Maintainer (Github/Discord) | -|---|---------------------|--------------------------------| -|🟢 |🇬🇧 English |djj123dj | -|🟢 |🇳🇱 Dutch |djj123dj | -|🟢 |🇩🇪 German |benzorich | -|🟢 |🇪🇸 Spanish |redactado & josuens | -|🟢 |🇵🇹 Portuguese |quiradon | -|🟢 |❓ Catalan |guillee3 | -|🟢 |🇨🇿 Czech |spyeye_ | -|🟢 |🇭🇺 Hungarian |kornel0706 | -|🟢 |🇷🇴 Romanian |sankedev | -|🟢 |🇺🇦 Ukrainian |anderskiy | -|🟢 |🇮🇩 Indonesian |erxg | -|🟢 |🇮🇹 Italian |fraden1mvp. | -|🟢 |🇩🇰 Danish |the_gamer | -|🟢 |🇹🇭 Thai |modshd | -|🟢 |🇹🇷 Turkish |palestinian | -|🟢 |🇫🇷 French |guillee.3 | -|🟢 |🇦🇪 Arabic |palestinian | -|🟢 |🇮🇳 Hindi |an_developer | -|🟢 |🇱🇹 Lithuanian |tsgindrius | -|🟢 |🇵🇱 Polish |danoglez | -|🟢 |🇳🇴 Norwegian |NoOneNook | -|🟢 |🇸🇪 Swedish |NoOneNook | -|🟢 |🇮🇷 Persian |dysashop & zhavis | -|🟢 |🇧🇩 Bengali |HanumeshGupta | -|🤖 |🇪🇪 Estonian |iamnotmega | -|🤖 |🇫🇮 Finnish |iamnotmega | -|🤖 |🇷🇺 Russian |NoOneNook | -|🤖 |🇱🇻 Latvian |NoOneNook | -|🤖 |🇻🇳 Vietnamese |ngocdiep2006 | -|🤖 |🇯🇵 Japanese |HanumeshGupta | -|🤖 |🇬🇷 Greek |HanumeshGupta | -|🤖 |🇸🇮 Slovenian |HanumeshGupta | -|🤖 |🇰🇷 Korean |HanumeshGupta | -|🤖 |🇮🇳 Tamil |HanumeshGupta | -|🤖 |🇨🇳 Simplified Chinese |HanumeshGupta | -|🤖 |❓ Kurdish |HanumeshGupta | -|🔴 |🇨🇳 Traditional Chinese|[⭐ Contribute!](.github/CONTRIBUTING.md)| +|🔍 |Languages (36) |Maintainer (Github/Discord) | +|----|---------------------|--------------------------------| +|🟢 |🇬🇧 English |djj123dj | +|🟢 |🇳🇱 Dutch |djj123dj | +|🟢 |❓ Catalan |guillee3 | +|🟢 |🇮🇩 Indonesian |erxg | +|🟢 |🇮🇳 Hindi |challenger_nova | +|🟢⏳ |🇩🇪 German |benzorich | +|🟢⏳ |🇪🇸 Spanish |redactado & josuens | +|🟢⏳ |🇫🇷 French |guillee.3 | +|🟢⏳ |🇵🇹 Portuguese |quiradon | +|🟢⏳ |🇨🇿 Czech |spyeye_ | +|🟢⏳ |🇭🇺 Hungarian |kornel0706 | +|🟢⏳ |🇷🇴 Romanian |sankedev | +|🟢⏳ |🇺🇦 Ukrainian |anderskiy | +|🟢⏳ |🇮🇹 Italian |fraden1mvp. | +|🟢⏳ |🇩🇰 Danish |the_gamer | +|🟢⏳ |🇹🇭 Thai |modshd | +|🟢⏳ |🇹🇷 Turkish |palestinian | +|🟢⏳ |🇦🇪 Arabic |palestinian | +|🟢⏳ |🇱🇹 Lithuanian |tsgindrius | +|🟢⏳ |🇵🇱 Polish |danoglez | +|🟢⏳ |🇳🇴 Norwegian |NoOneNook | +|🟢⏳ |🇸🇪 Swedish |NoOneNook | +|🟢⏳ |🇮🇷 Persian |dysashop & zhavis | +|🟢⏳ |🇧🇩 Bengali |HanumeshGupta | +|🤖 |🇪🇪 Estonian |iamnotmega | +|🤖 |🇫🇮 Finnish |iamnotmega | +|🤖⏳ |🇷🇺 Russian |NoOneNook | +|🤖⏳ |🇱🇻 Latvian |NoOneNook | +|🤖⏳ |🇻🇳 Vietnamese |ngocdiep2006 | +|🤖⏳ |🇯🇵 Japanese |HanumeshGupta | +|🤖⏳ |🇬🇷 Greek |HanumeshGupta | +|🤖⏳ |🇸🇮 Slovenian |HanumeshGupta | +|🤖⏳ |🇰🇷 Korean |HanumeshGupta | +|🤖⏳ |🇮🇳 Tamil |HanumeshGupta | +|🤖⏳ |🇨🇳 Simplified Chinese |HanumeshGupta | +|🤖⏳ |❓ Kurdish |HanumeshGupta | +|🔴 |🇨🇳 Traditional Chinese|[⭐ Contribute!](.github/CONTRIBUTING.md)| + ## ⭐️ Star History If you enjoy using Open ticket, **consider starring** this repository.