Open Ticket v4 Beta
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
///////////////////////////////////////
|
||||
//ACTION MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODValidId, ODSystemError, ODManagerData } from "./base"
|
||||
import { ODWorkerManager, ODWorkerCallback, ODWorker } from "./worker"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
/**## ODActionImplementation `class`
|
||||
* This is an open ticket action implementation.
|
||||
*
|
||||
* It is a basic implementation of the `ODWorkerManager` used by all `ODAction` classes.
|
||||
*
|
||||
* This class can't be used stand-alone & needs to be extended from!
|
||||
*/
|
||||
export class ODActionImplementation<Source extends string,Params extends object,Result extends object> extends ODManagerData {
|
||||
/**The manager that has all workers of this implementation */
|
||||
workers: ODWorkerManager<object,Source,Params>
|
||||
|
||||
constructor(id:ODValidId, callback?:ODWorkerCallback<object,Source,Params>, priority?:number, callbackId?:ODValidId){
|
||||
super(id)
|
||||
this.workers = new ODWorkerManager("descending")
|
||||
if (callback) this.workers.add(new ODWorker(callbackId ? callbackId : id,priority ?? 0,callback))
|
||||
}
|
||||
/**Execute all workers & return the result. */
|
||||
async run(source:Source, params:Params): Promise<Partial<Result>> {
|
||||
throw new ODSystemError("Tried to build an unimplemented ODResponderImplementation")
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODActionManager `class`
|
||||
* This is an open ticket action manager.
|
||||
*
|
||||
* It contains all Open Ticket actions. You can compare actions with some sort of "procedure".
|
||||
* It's a complicated task that is divided into multiple functions.
|
||||
*
|
||||
* Some examples are `ticket-creation`, `ticket-closing`, `ticket-claiming`, ...
|
||||
*
|
||||
* It's recommended to use this system in combination with Open Ticket responders!
|
||||
*/
|
||||
export class ODActionManager extends ODManager<ODAction<string,{},{}>> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"action")
|
||||
}
|
||||
}
|
||||
|
||||
export class ODAction<Source extends string,Params extends object,Result extends object> extends ODActionImplementation<Source,Params,Result> {
|
||||
/**Run this action */
|
||||
async run(source:Source, params:Params): Promise<Partial<Result>> {
|
||||
//create instance
|
||||
const instance = {}
|
||||
|
||||
//wait for workers to finish
|
||||
await this.workers.executeWorkers(instance,source,params)
|
||||
|
||||
//return data generated by workers
|
||||
return instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,749 @@
|
||||
///////////////////////////////////////
|
||||
//BASE MODULE
|
||||
///////////////////////////////////////
|
||||
import * as fs from "fs"
|
||||
import { ODConsoleWarningMessage, ODDebugger } from "./console"
|
||||
|
||||
/**## ODPromiseVoid `type`
|
||||
* This is a simple type to represent a callback return value that could be a promise or not.
|
||||
*/
|
||||
export type ODPromiseVoid = void|Promise<void>
|
||||
|
||||
/**## ODValidButtonColor `type`
|
||||
* This is a collection of all the possible button colors.
|
||||
*/
|
||||
export type ODValidButtonColor = "gray"|"red"|"green"|"blue"
|
||||
|
||||
/**## ODValidId `type`
|
||||
* This is a valid open ticket identifier. It can be an `ODId` or `string`!
|
||||
*
|
||||
* You will see this type in many functions from open ticket.
|
||||
*/
|
||||
export type ODValidId = string|ODId
|
||||
|
||||
/**## ODValidJsonType `type`
|
||||
* This is a collection of all types that can be stored in a JSON file!
|
||||
*
|
||||
* list: `string`, `number`, `boolean`, `array`, `object`, `null`
|
||||
*/
|
||||
export type ODValidJsonType = string|number|boolean|object|ODValidJsonType[]|null
|
||||
|
||||
|
||||
/**## ODInterfaceWithPartialProperty `type`
|
||||
* This is a utility type to create an interface where some properties are optional!
|
||||
*/
|
||||
export type ODInterfaceWithPartialProperty<Interface,Key extends keyof Interface> = Omit<Interface,Key> & Partial<Pick<Interface,Key>>
|
||||
|
||||
/**## ODId `class`
|
||||
* This is an open ticket identifier.
|
||||
*
|
||||
* It can only contain the following characters: `a-z`, `A-Z`, `0-9`, `:`, `-` & `_`
|
||||
*
|
||||
* You will use this class to assign a unique id when creating configs, databases, languages & more!
|
||||
* @example
|
||||
* const id = new api.ODId("openticket:test-id") //this is valid
|
||||
* const id = new api.ODId("example%id?") //this is invalid
|
||||
*/
|
||||
export class ODId {
|
||||
/**The full value of this `ODId` as a `string` */
|
||||
value: string
|
||||
/**The source of the id (part before `:`). For example `openticket` for all built-in ids! */
|
||||
source: string
|
||||
/**The identifier of the id (part after `:`). */
|
||||
identifier: string
|
||||
|
||||
constructor(id:ODValidId){
|
||||
if (typeof id != "string" && !(id instanceof ODId)) throw new ODSystemError("Invalid constructor parameter => id:ODValidId")
|
||||
|
||||
if (typeof id == "string"){
|
||||
//id is string
|
||||
const result: string[] = []
|
||||
const charregex = /[a-zA-Z0-9éèçàêâôûî\:\-\_]/
|
||||
|
||||
id.split("").forEach((char) => {
|
||||
if (charregex.test(char)){
|
||||
result.push(char)
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**Returns a string representation of this id. (same as `this.value`) */
|
||||
toString(){
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODManagerChangeHelper `class`
|
||||
* This is an open ticket manager change helper.
|
||||
*
|
||||
* It is used to let the "onChange" event in the `ODManager` class work.
|
||||
* You will probably use this class when extending your own `ODManager`
|
||||
*/
|
||||
export class ODManagerChangeHelper {
|
||||
#change: (() => void)|null = null
|
||||
|
||||
protected _change(){
|
||||
if (this.#change){
|
||||
try{
|
||||
this.#change()
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
throw new ODSystemError("Failed to execute _change() callback!")
|
||||
}
|
||||
}
|
||||
}
|
||||
/****(❌ SYSTEM ONLY!!)** Set the callback executed when a value inside this class changes */
|
||||
changed(callback:(() => void)|null){
|
||||
this.#change = callback
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODManagerRedirectHelper `class`
|
||||
* 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.
|
||||
*
|
||||
* It provides a template for all classes that are used in the `ODManager`.
|
||||
*
|
||||
* There is an `id:ODId` property & also some events used in the manager.
|
||||
*/
|
||||
export class ODManagerData extends ODManagerChangeHelper {
|
||||
/**The id of this data */
|
||||
id: ODId
|
||||
|
||||
constructor(id:ODValidId){
|
||||
if (typeof id != "string" && !(id instanceof ODId)) throw new ODSystemError("Invalid constructor parameter => id:ODValidId")
|
||||
super()
|
||||
this.id = new ODId(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODManagerCallback `type`
|
||||
* This is a callback for the `onChange` and `onRemove` events in the `ODManager`
|
||||
*/
|
||||
export type ODManagerCallback<DataType extends ODManagerData> = (data:DataType) => void
|
||||
/**## ODManagerAddCallback `type`
|
||||
* This is a callback for the `onAdd` event in the `ODManager`
|
||||
*/
|
||||
export type ODManagerAddCallback<DataType extends ODManagerData> = (data:DataType, overwritten:boolean) => void
|
||||
|
||||
/**## ODManager `class`
|
||||
* This is an open ticket manager.
|
||||
*
|
||||
* It can be used to store & manage different aspects of the bot!
|
||||
* You will probably to extend this class when creating your own classes & managers.
|
||||
*
|
||||
* This class has many useful functions based on `ODId` (add, get, remove, getAll, getFiltered, exists)
|
||||
*/
|
||||
export class ODManager<DataType extends ODManagerData> extends ODManagerChangeHelper {
|
||||
/**Alias to open ticket debugger. */
|
||||
#debug?: ODDebugger
|
||||
/**The message to send when debugging this manager. */
|
||||
#debugname?: string
|
||||
/**An array storing all data classes ❌ **(don't edit unless really needed!)***/
|
||||
#data: DataType[] = []
|
||||
/**An array storing all listeners when data is added. */
|
||||
#addListeners: ODManagerAddCallback<DataType>[] = []
|
||||
/**An array storing all listeners when data has changed. */
|
||||
#changeListeners: ODManagerCallback<DataType>[] = []
|
||||
/**An array storing all listeners when data is removed. */
|
||||
#removeListeners: ODManagerCallback<DataType>[] = []
|
||||
/**Handle all redirects in this `ODManager` */
|
||||
redirects: ODManagerRedirectHelper = new ODManagerRedirectHelper()
|
||||
|
||||
constructor(debug?:ODDebugger, debugname?:string){
|
||||
super()
|
||||
this.#debug = debug
|
||||
this.#debugname = debugname
|
||||
}
|
||||
|
||||
/**Add data to the manager. The id will be fetched from the data class! You can optionally select to overwrite existing data!*/
|
||||
add(data:DataType|DataType[], overwrite?:boolean): boolean {
|
||||
//repeat same command when data is an array
|
||||
if (Array.isArray(data)){
|
||||
data.forEach((arrayData) => {
|
||||
this.add(arrayData,overwrite)
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
//add data
|
||||
const existIndex = this.#data.findIndex((d) => d.id.value === data.id.value)
|
||||
if (existIndex < 0){
|
||||
this.#data.push(data)
|
||||
if (this.#debug) this.#debug.debug("Added new "+this.#debugname+" to manager",[{key:"id",value:data.id.value},{key:"overwrite",value:"false"}])
|
||||
|
||||
//change listeners
|
||||
data.changed(() => {
|
||||
//notify change in upper-manager (because data in this manager changed)
|
||||
this._change()
|
||||
this.#changeListeners.forEach((cb) => {
|
||||
try{
|
||||
cb(data)
|
||||
}catch(err){
|
||||
throw new ODSystemError("Failed to run manager onChange() listener.\n"+err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
//add listeners
|
||||
this.#addListeners.forEach((cb) => {
|
||||
try{
|
||||
cb(data,false)
|
||||
}catch(err){
|
||||
throw new ODSystemError("Failed to run manager onAdd() listener.\n"+err)
|
||||
}
|
||||
})
|
||||
|
||||
//notify change in upper-manager (because data added)
|
||||
this._change()
|
||||
|
||||
return false
|
||||
}else{
|
||||
if (!overwrite) throw new ODSystemError("Id '"+data.id.value+"' already exists in "+this.#debugname+" manager. Use 'overwrite:true' to allow overwriting!")
|
||||
this.#data[existIndex] = data
|
||||
if (this.#debug) this.#debug.debug("Added new "+this.#debugname+" to manager",[{key:"id",value:data.id.value},{key:"overwrite",value:"true"}])
|
||||
|
||||
//change listeners
|
||||
data.changed(() => {
|
||||
//notify change in upper-manager (because data in this manager changed)
|
||||
this._change()
|
||||
this.#changeListeners.forEach((cb) => {
|
||||
try{
|
||||
cb(data)
|
||||
}catch(err){
|
||||
throw new ODSystemError("Failed to run manager onChange() listener.\n"+err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
//add listeners
|
||||
this.#addListeners.forEach((cb) => {
|
||||
try{
|
||||
cb(data,true)
|
||||
}catch(err){
|
||||
throw new ODSystemError("Failed to run manager onAdd() listener.\n"+err)
|
||||
}
|
||||
})
|
||||
|
||||
//notify change in upper-manager (because data added)
|
||||
this._change()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
/**Get data that matches the `ODId`. Returns the found data.*/
|
||||
get(id:ODValidId): DataType|null {
|
||||
const newId = new ODId(id)
|
||||
const d = this.#data.find((a) => a.id.value == newId.value)
|
||||
if (d) return d
|
||||
else{
|
||||
const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.source)
|
||||
if (!redirect) return null
|
||||
else{
|
||||
const redirectId = new ODId(redirect.toSource+":"+newId.identifier)
|
||||
return this.get(redirectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**Remove data that matches the `ODId`. Returns the removed data.*/
|
||||
remove(id:ODValidId): DataType|null {
|
||||
const newId = new ODId(id)
|
||||
const index = this.#data.findIndex((a) => a.id.value == newId.value)
|
||||
if (index < 0){
|
||||
const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.source)
|
||||
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.identifier)
|
||||
return this.remove(redirectId)
|
||||
}
|
||||
}
|
||||
if (this.#debug) this.#debug.debug("Removed "+this.#debugname+" from manager",[{key:"id",value:newId.value},{key:"found",value:"true"}])
|
||||
const data = this.#data.splice(index,1)[0]
|
||||
|
||||
//change listeners
|
||||
data.changed(null)
|
||||
|
||||
//remove listeners
|
||||
this.#removeListeners.forEach((cb) => {
|
||||
try{
|
||||
cb(data)
|
||||
}catch(err){
|
||||
throw new ODSystemError("Failed to run manager onRemove() listener.\n"+err)
|
||||
}
|
||||
})
|
||||
|
||||
//notify change in upper-manager (because data removed)
|
||||
this._change()
|
||||
|
||||
return data
|
||||
}
|
||||
/**Check if data that matches the `ODId` exists. Returns a boolean.*/
|
||||
exists(id:ODValidId): boolean {
|
||||
const newId = new ODId(id)
|
||||
const d = this.#data.find((a) => a.id.value == newId.value)
|
||||
if (d) return true
|
||||
else{
|
||||
const redirect = this.redirects.list().find((redirect) => redirect.fromSource === newId.source)
|
||||
if (!redirect) return false
|
||||
else{
|
||||
const redirectId = new ODId(redirect.toSource+":"+newId.identifier)
|
||||
return this.exists(redirectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**Get all data inside this manager*/
|
||||
getAll(): DataType[] {
|
||||
return [...this.#data]
|
||||
}
|
||||
/**Get all data that matches inside the filter function*/
|
||||
getFiltered(predicate:(value:DataType, index:number, array:DataType[]) => unknown): DataType[] {
|
||||
return this.#data.filter(predicate)
|
||||
}
|
||||
/**Get all data that matches the regex*/
|
||||
getRegex(regex:RegExp): DataType[] {
|
||||
return this.#data.filter((data) => regex.test(data.id.value))
|
||||
}
|
||||
/**Get the length of the data inside this manager*/
|
||||
getLength(){
|
||||
return this.#data.length
|
||||
}
|
||||
/**Get a list of all the ids inside this manager*/
|
||||
getIds(): ODId[] {
|
||||
return this.#data.map((d) => d.id)
|
||||
}
|
||||
/**Use the open ticket debugger in this manager for logs*/
|
||||
useDebug(debug?:ODDebugger, debugname?:string){
|
||||
this.#debug = debug
|
||||
this.#debugname = debugname
|
||||
}
|
||||
/**Listen for when data is added to this manager. */
|
||||
onAdd(callback:ODManagerAddCallback<DataType>){
|
||||
this.#addListeners.push(callback)
|
||||
}
|
||||
/**Listen for when data is changed in this manager. */
|
||||
onChange(callback:ODManagerCallback<DataType>){
|
||||
this.#changeListeners.push(callback)
|
||||
}
|
||||
/**Listen for when data is removed from this manager. */
|
||||
onRemove(callback:ODManagerCallback<DataType>){
|
||||
this.#removeListeners.push(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODManagerWithSafety `class`
|
||||
* This is an open ticket safe manager.
|
||||
*
|
||||
* It functions exactly the same as a normal `ODManager`, but it has 1 function extra!
|
||||
* The `getSafe()` function will always return data, because when it doesn't find an id, it returns pre-configured backup data.
|
||||
*/
|
||||
export class ODManagerWithSafety<DataType extends ODManagerData> extends ODManager<DataType> {
|
||||
/**The function that creates backup data returned in `getSafe()` when an id is missing in this manager. */
|
||||
#backupCreator: () => DataType
|
||||
/** Temporary storage for manager debug name. */
|
||||
#debugname: string
|
||||
|
||||
constructor(backupCreator:() => DataType, debug?:ODDebugger, debugname?:string){
|
||||
super(debug,debugname)
|
||||
this.#backupCreator = backupCreator
|
||||
this.#debugname = debugname ?? "unknown"
|
||||
}
|
||||
|
||||
/**Get data that matches the `ODId`. Returns the backup data when not found.
|
||||
*
|
||||
* ### ⚠️ This should only be used when the data doesn't need to be written/edited
|
||||
*/
|
||||
getSafe(id:ODValidId): DataType {
|
||||
const data = super.get(id)
|
||||
if (!data){
|
||||
process.emit("uncaughtException",new ODSystemError("ODManagerWithSafety:getSafe(\""+id+"\") => Unknown Id => Used backup data ("+this.#debugname+" manager)"))
|
||||
return this.#backupCreator()
|
||||
}
|
||||
else return data
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODVersionManager `class`
|
||||
* A open ticket version manager.
|
||||
*
|
||||
* It is used to manage different `ODVersion`'s from the bot. You will use it to check which version of the bot is used.
|
||||
*/
|
||||
export class ODVersionManager extends ODManager<ODVersion> {
|
||||
constructor(){
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODVersion `class`
|
||||
* This is an open ticket version.
|
||||
*
|
||||
* It has many features like comparing versions & checking if they are compatible.
|
||||
*
|
||||
* You can use it in your own plugin, but most of the time you will use it to check the open ticket version!
|
||||
*/
|
||||
export class ODVersion extends ODManagerData {
|
||||
/**The first number of the version (example: `v1.2.3` => `1`) */
|
||||
primary: number
|
||||
/**The second number of the version (example: `v1.2.3` => `2`) */
|
||||
secondary: number
|
||||
/**The third number of the version (example: `v1.2.3` => `3`) */
|
||||
tertiary: number
|
||||
|
||||
constructor(id:ODValidId, primary:number, secondary:number, tertiary:number){
|
||||
super(id)
|
||||
if (typeof primary != "number") throw new ODSystemError("Invalid constructor parameter => primary:number")
|
||||
if (typeof secondary != "number") throw new ODSystemError("Invalid constructor parameter => secondary:number")
|
||||
if (typeof tertiary != "number") throw new ODSystemError("Invalid constructor parameter => tertiary:number")
|
||||
|
||||
this.primary = primary
|
||||
this.secondary = secondary
|
||||
this.tertiary = tertiary
|
||||
}
|
||||
|
||||
/**Get the version from a string (also possible with `v` prefix)
|
||||
* @example const version = api.ODVersion.fromString("id","v1.2.3") //creates version 1.2.3
|
||||
*/
|
||||
static fromString(id:ODValidId, version:string){
|
||||
if (typeof id != "string" && !(id instanceof ODId)) throw new ODSystemError("Invalid function parameter => id:ODValidId")
|
||||
if (typeof version != "string") throw new ODSystemError("Invalid function parameter => version:string")
|
||||
|
||||
const versionCheck = (version.startsWith("v")) ? version.substring(1) : version
|
||||
const splittedVersion = versionCheck.split(".")
|
||||
|
||||
return new this(id,Number(splittedVersion[0]),Number(splittedVersion[1]),Number(splittedVersion[2]))
|
||||
}
|
||||
/**Get the version as a string (`noprefix:true` => with `v` prefix)
|
||||
* @example
|
||||
* new api.ODVersion(1,0,0).toString(false) //returns "v1.0.0"
|
||||
* new api.ODVersion(1,0,0).toString(true) //returns "1.0.0"
|
||||
*/
|
||||
toString(noprefix?:boolean){
|
||||
const prefix = noprefix ? "" : "v"
|
||||
return prefix+[this.primary,this.secondary,this.tertiary].join(".")
|
||||
}
|
||||
/**Compare this version with another version and returns the result: `higher`, `lower` or `equal`
|
||||
* @example
|
||||
* new api.ODVersion(1,0,0).compare(new api.ODVersion(1,2,0)) //returns "lower"
|
||||
* new api.ODVersion(1,3,0).compare(new api.ODVersion(1,2,0)) //returns "higher"
|
||||
* new api.ODVersion(1,2,0).compare(new api.ODVersion(1,2,0)) //returns "equal"
|
||||
*/
|
||||
compare(comparator:ODVersion): "higher"|"lower"|"equal" {
|
||||
if (!(comparator instanceof ODVersion)) throw new ODSystemError("Invalid function parameter => comparator:ODVersion")
|
||||
|
||||
if (this.primary < comparator.primary) return "lower"
|
||||
else if (this.primary > comparator.primary) return "higher"
|
||||
else {
|
||||
if (this.secondary < comparator.secondary) return "lower"
|
||||
else if (this.secondary > comparator.secondary) return "higher"
|
||||
else {
|
||||
if (this.tertiary < comparator.tertiary) return "lower"
|
||||
else if (this.tertiary > comparator.tertiary) return "higher"
|
||||
else return "equal"
|
||||
}
|
||||
}
|
||||
}
|
||||
/**Check if this version is included in the list
|
||||
* @example
|
||||
* const list = [
|
||||
* new api.ODVersion(1,0,0),
|
||||
* new api.ODVersion(1,0,1),
|
||||
* new api.ODVersion(1,0,2)
|
||||
* ]
|
||||
* new api.ODVersion(1,0,0).compatible(list) //returns true
|
||||
* new api.ODVersion(1,0,1).compatible(list) //returns true
|
||||
* new api.ODVersion(1,0,3).compatible(list) //returns false
|
||||
*/
|
||||
compatible(list:ODVersion[]): boolean {
|
||||
if (!Array.isArray(list)) throw new ODSystemError("Invalid function parameter => list:ODVersion[]")
|
||||
if (!list.every((v) => (v instanceof ODVersion))) throw new ODSystemError("Invalid function parameter => list:ODVersion[]")
|
||||
|
||||
return list.some((v) => {
|
||||
return (v.toString() === this.toString())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODHTTPGetRequest `class`
|
||||
* This is a class that can help you with creating simple HTTP GET requests.
|
||||
*
|
||||
* It works using the native node.js fetch() method. You can configure all options in the constructor!
|
||||
* @example
|
||||
* const request = new api.ODHTTPGetRequest("https://www.example.com/abc.txt",false,{})
|
||||
*
|
||||
* const result = await request.run()
|
||||
* result.body //the response body (string)
|
||||
* result.status //the response code (number)
|
||||
* result.response //the full response (object)
|
||||
*/
|
||||
export class ODHTTPGetRequest {
|
||||
/**The url used in the request */
|
||||
url: string
|
||||
/**The request config for additional options */
|
||||
config: RequestInit
|
||||
/**Throw on error OR return http code 500 */
|
||||
throwOnError: boolean
|
||||
|
||||
constructor(url:string,throwOnError:boolean,config?:RequestInit){
|
||||
if (typeof url != "string") throw new ODSystemError("Invalid constructor parameter => url:string")
|
||||
if (typeof throwOnError != "boolean") throw new ODSystemError("Invalid constructor parameter => throwOnError:boolean")
|
||||
if (typeof config != "undefined" && typeof config != "object") throw new ODSystemError("Invalid constructor parameter => config?:RequestInit")
|
||||
|
||||
this.url = url
|
||||
this.throwOnError = throwOnError
|
||||
const newConfig = config ?? {}
|
||||
newConfig.method = "GET"
|
||||
this.config = newConfig
|
||||
}
|
||||
|
||||
/**Execute the GET request.*/
|
||||
run(): Promise<{status:number, body:string, response?:Response}> {
|
||||
return new Promise(async (resolve,reject) => {
|
||||
try{
|
||||
const response = await fetch(this.url,this.config)
|
||||
resolve({
|
||||
status:response.status,
|
||||
body:(await response.text()),
|
||||
response:response
|
||||
})
|
||||
}catch(err){
|
||||
if (this.throwOnError) return reject("[OPENTICKET ERROR]: ODHTTPGetRequest => Unknown fetch() error: "+err)
|
||||
else return resolve({
|
||||
status:500,
|
||||
body:"Open Ticket Error: Unknown fetch() error: "+err,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODHTTPPostRequest `class`
|
||||
* This is a class that can help you with creating simple HTTP POST requests.
|
||||
*
|
||||
* It works using the native node.js fetch() method. You can configure all options in the constructor!
|
||||
* @example
|
||||
* const request = new api.ODHTTPPostRequest("https://www.example.com/abc.txt",false,{})
|
||||
*
|
||||
* const result = await request.run()
|
||||
* result.body //the response body (string)
|
||||
* result.status //the response code (number)
|
||||
* result.response //the full response (object)
|
||||
*/
|
||||
export class ODHTTPPostRequest {
|
||||
/**The url used in the request */
|
||||
url: string
|
||||
/**The request config for additional options */
|
||||
config: RequestInit
|
||||
/**Throw on error OR return http code 500 */
|
||||
throwOnError: boolean
|
||||
|
||||
constructor(url:string,throwOnError:boolean,config?:RequestInit){
|
||||
if (typeof url != "string") throw new ODSystemError("Invalid constructor parameter => url:string")
|
||||
if (typeof throwOnError != "boolean") throw new ODSystemError("Invalid constructor parameter => throwOnError:boolean")
|
||||
if (typeof config != "undefined" && typeof config != "object") throw new ODSystemError("Invalid constructor parameter => config?:RequestInit")
|
||||
|
||||
this.url = url
|
||||
this.throwOnError = throwOnError
|
||||
const newConfig = config ?? {}
|
||||
newConfig.method = "POST"
|
||||
this.config = newConfig
|
||||
}
|
||||
|
||||
/**Execute the POST request.*/
|
||||
run(): Promise<{status:number, body:string, response?:Response}> {
|
||||
return new Promise(async (resolve,reject) => {
|
||||
try{
|
||||
const response = await fetch(this.url,this.config)
|
||||
resolve({
|
||||
status:response.status,
|
||||
body:(await response.text()),
|
||||
response:response
|
||||
})
|
||||
}catch(err){
|
||||
if (this.throwOnError) return reject("[OPENTICKET ERROR]: ODHTTPPostRequest => Unknown fetch() error: "+err)
|
||||
else return resolve({
|
||||
status:500,
|
||||
body:"Open Ticket Error: Unknown fetch() error!",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODEnvHelper `class`
|
||||
* This is a utility class that helps you with reading the ENV.
|
||||
*
|
||||
* It has support for the built-in `process.env` & `.env` file
|
||||
* @example
|
||||
* const envHelper = new api.ODEnvHelper()
|
||||
*
|
||||
* const variableA = envHelper.getVariable("value-a")
|
||||
* const variableB = envHelper.getVariable("value-b","dotenv") //only get from .env
|
||||
* const variableA = envHelper.getVariable("value-c","env") //only get from process.env
|
||||
*/
|
||||
export class ODEnvHelper {
|
||||
/**All variables found in the `.env` file */
|
||||
dotenv: object
|
||||
/**All variables found in `process.env` */
|
||||
env: object
|
||||
|
||||
constructor(customEnvPath?:string){
|
||||
if (typeof customEnvPath != "undefined" && typeof customEnvPath != "string") throw new ODSystemError("Invalid constructor parameter => customEnvPath?:string")
|
||||
|
||||
const path = customEnvPath ? customEnvPath : ".env"
|
||||
this.dotenv = fs.existsSync(path) ? this.#readDotEnv(fs.readFileSync(path)) : {}
|
||||
this.env = process.env
|
||||
}
|
||||
|
||||
/**Get a variable from the env */
|
||||
getVariable(name:string,source?:"dotenv"|"env"): any|undefined {
|
||||
if (typeof name != "string") throw new ODSystemError("Invalid function parameter => name:string")
|
||||
if ((typeof source != "undefined" && typeof source != "string") || (source && !["env","dotenv"].includes(source))) throw new ODSystemError("Invalid function parameter => source:'dotenv'|'env'")
|
||||
|
||||
if (source == "dotenv"){
|
||||
return this.dotenv[name]
|
||||
}else if (source == "env"){
|
||||
return this.env[name]
|
||||
}else{
|
||||
//when no source specified => .env has priority over process.env
|
||||
if (this.dotenv[name]) return this.dotenv[name]
|
||||
else return this.env[name]
|
||||
}
|
||||
}
|
||||
|
||||
//THIS CODE IS COPIED FROM THE DODENV-LIB
|
||||
//Repo: https://github.com/motdotla/dotenv
|
||||
//Source: https://github.com/motdotla/dotenv/blob/master/lib/main.js#L12
|
||||
#readDotEnv(src:Buffer){
|
||||
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
|
||||
const obj = {}
|
||||
|
||||
// Convert buffer to string
|
||||
let lines = src.toString()
|
||||
|
||||
// Convert line breaks to same format
|
||||
lines = lines.replace(/\r\n?/mg, '\n')
|
||||
|
||||
let match
|
||||
while ((match = LINE.exec(lines)) != null) {
|
||||
const key = match[1]
|
||||
|
||||
// Default undefined or null to empty string
|
||||
let value = (match[2] || '')
|
||||
|
||||
// Remove whitespace
|
||||
value = value.trim()
|
||||
|
||||
// Check if double quoted
|
||||
const maybeQuote = value[0]
|
||||
|
||||
// Remove surrounding quotes
|
||||
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
|
||||
|
||||
// Expand newlines if double quoted
|
||||
if (maybeQuote === '"') {
|
||||
value = value.replace(/\\n/g, '\n')
|
||||
value = value.replace(/\\r/g, '\r')
|
||||
}
|
||||
|
||||
// Add to object
|
||||
obj[key] = value
|
||||
}
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODSystemError `class`
|
||||
* A wrapper for the node.js `Error` class that makes the error look better in the console!
|
||||
*
|
||||
* This wrapper is made for open ticket system errors! **It can only be used by open ticket itself!**
|
||||
*/
|
||||
export class ODSystemError extends Error {
|
||||
/**This variable gets detected by the error handling system to know how to render it */
|
||||
_ODErrorType = "system"
|
||||
|
||||
/**Create an `ODSystemError` directly from an `Error` class */
|
||||
static fromError(err:Error){
|
||||
err["_ODErrorType"] = "system"
|
||||
return err as ODSystemError
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODPluginError `class`
|
||||
* A wrapper for the node.js `Error` class that makes the error look better in the console!
|
||||
*
|
||||
* This wrapper is made for open ticket plugin errors! **It can only be used by plugins!**
|
||||
*/
|
||||
export class ODPluginError extends Error {
|
||||
/**This variable gets detected by the error handling system to know how to render it */
|
||||
_ODErrorType = "plugin"
|
||||
|
||||
/**Create an `ODPluginError` directly from an `Error` class */
|
||||
static fromError(err:Error){
|
||||
err["_ODErrorType"] = "plugin"
|
||||
return err as ODPluginError
|
||||
}
|
||||
}
|
||||
|
||||
/**Oh, what could this be `¯\_(ツ)_/¯` */
|
||||
export interface ODEasterEggs {
|
||||
creator:string,
|
||||
translators:string[]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
///////////////////////////////////////
|
||||
//CODE MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
|
||||
/**## ODCode `class`
|
||||
* This is an open ticket code runner.
|
||||
*
|
||||
* It is just a function that will run just before the bot has started completely!
|
||||
* This can be used for code that needs to run at startup, but isn't really time dependent.
|
||||
*
|
||||
* - It has an `id` for identification of the function
|
||||
* - A `priority` to know when to execute this function (related to others)
|
||||
*/
|
||||
export class ODCode extends ODManagerData {
|
||||
/**The priority of this code */
|
||||
priority: number
|
||||
/**The main function of this code */
|
||||
func: () => void|Promise<void>
|
||||
|
||||
constructor(id:ODValidId, priority:number, func:() => void|Promise<void>){
|
||||
super(id)
|
||||
this.priority = priority
|
||||
this.func = func
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODCodeManager `class`
|
||||
* This is an open ticket code manager.
|
||||
*
|
||||
* It manages & executes `ODCode`'s in the correct order.
|
||||
*
|
||||
* You will probably register a function/code in this class for something that doesn't really need to be timed.
|
||||
*/
|
||||
export class ODCodeManager extends ODManager<ODCode> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"code")
|
||||
}
|
||||
|
||||
/**Execute all functions or code. */
|
||||
async execute(){
|
||||
const derefArray = [...this.getAll()]
|
||||
const workers = derefArray.sort((a,b) => b.priority-a.priority)
|
||||
|
||||
for (const worker of workers){
|
||||
try {
|
||||
await worker.func()
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
///////////////////////////////////////
|
||||
//CONFIG MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base"
|
||||
import nodepath from "path"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
/**## ODConfigManager `class`
|
||||
* This is an open ticket config manager.
|
||||
*
|
||||
* It manages all config files in the bot and allows plugins to access config files from open ticket & other plugins!
|
||||
*
|
||||
* You will use this class to get/add a config file (`ODConfig`) in your plugin!
|
||||
* @example
|
||||
* //get ./config/general.json => ODConfig class
|
||||
* const generalConfig = openticket.configs.get("openticket:general")
|
||||
*
|
||||
* //add a new config with id "test" => ./config/test.json
|
||||
* const testConfig = new api.ODConfig("test","test.json")
|
||||
* openticket.configs.add(testConfig)
|
||||
*/
|
||||
export class ODConfigManager extends ODManager<ODConfig> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"config")
|
||||
}
|
||||
|
||||
/**Add data to the manager. The id will be fetched from the data class! You can optionally select to overwrite existing data!
|
||||
* @example
|
||||
* //add a new config with id "test" => ./config/test.json
|
||||
* const testConfig = new api.ODConfig("test","test.json")
|
||||
* openticket.configs.add(testConfig)
|
||||
*/
|
||||
add(data:ODConfig, overwrite?:boolean): boolean {
|
||||
return super.add(data,overwrite)
|
||||
}
|
||||
/**Get data that matches the `ODId`. Returns the found data.
|
||||
* @example
|
||||
* //get "./config/general.json" (ot-general) => ODConfig class
|
||||
* const generalConfig = openticket.configs.get("openticket:general")
|
||||
*/
|
||||
get(id:ODValidId): ODConfig|null {
|
||||
return super.get(id)
|
||||
}
|
||||
/**Remove data that matches the `ODId`. Returns the removed data.
|
||||
* @example
|
||||
* //remove the "test" config
|
||||
* openticket.configs.remove("test") //returns null if non-existing
|
||||
*/
|
||||
remove(id:ODValidId): ODConfig|null {
|
||||
return super.remove(id)
|
||||
}
|
||||
/**Check if data that matches the `ODId` exists. Returns a boolean.
|
||||
* @example
|
||||
* //check if "./config/general.json" (ot-general) exists => boolean
|
||||
* const exists = openticket.configs.exists("openticket:general")
|
||||
*/
|
||||
exists(id:ODValidId): boolean {
|
||||
return super.exists(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConfig `class`
|
||||
* This is an open ticket config helper.
|
||||
* This class doesn't do anything at all, it just gives a template & basic methods for a config. Use `ODJsonConfig` instead!
|
||||
*
|
||||
* You will only use this class if you want to create your own config implementation (e.g. `yml`, `xml`,...)!
|
||||
*/
|
||||
export class ODConfig extends ODManagerData {
|
||||
/**The full path to this config with extension */
|
||||
file: string = ""
|
||||
/**An object/array of the entire config file! Variables inside it can be edited while the bot is running! */
|
||||
data: any = undefined
|
||||
}
|
||||
|
||||
/**## ODJsonConfig `class`
|
||||
* This is an open ticket config helper.
|
||||
* You will use this class to get & edit variables from the config files or to create your own config!
|
||||
* @example
|
||||
* //create a config from: ./config/test.json with the id "some-config"
|
||||
* const config = new api.ODJsonConfig("some-config","test.json")
|
||||
*
|
||||
* //create a config with custom dir: ./plugins/testplugin/test.json
|
||||
* const config = new api.ODJsonConfig("plugin-config","test.json","./plugins/testplugin/")
|
||||
*/
|
||||
export class ODJsonConfig extends ODConfig {
|
||||
constructor(id:ODValidId, file:string, customPath?:string){
|
||||
super(id)
|
||||
try {
|
||||
const filename = (file.endsWith(".json")) ? file : file+".json"
|
||||
this.file = customPath ? nodepath.join("./",customPath,filename) : nodepath.join("./config/",filename)
|
||||
this.data = customPath ? require(nodepath.join("../../../../",customPath,filename)) : require(nodepath.join("../../../../config",filename))
|
||||
}catch{
|
||||
throw new ODSystemError("config \""+nodepath.join("./",customPath ?? "",file)+"\" doesn't exist!")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
///////////////////////////////////////
|
||||
//CONSOLE MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODHTTPGetRequest, ODVersion, ODSystemError, ODPluginError, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
import { ODMain } from "../main"
|
||||
import nodepath from "path"
|
||||
import fs from "fs"
|
||||
import ansis from "ansis"
|
||||
|
||||
/**## ODValidConsoleColor `type`
|
||||
* This is a collection of all the supported console colors within Open Ticket.
|
||||
*/
|
||||
export type ODValidConsoleColor = "white"|"red"|"yellow"|"green"|"blue"|"gray"|"cyan"|"magenta"
|
||||
|
||||
/**## ODConsoleMessageParam `type`
|
||||
* This interface contains all data required for a console log parameter within Open Ticket.
|
||||
*/
|
||||
export interface ODConsoleMessageParam {
|
||||
/**The key of this parameter. */
|
||||
key:string,
|
||||
/**The value of this parameter. */
|
||||
value:string,
|
||||
/**When enabled, this parameter will only be shown in the debug file. */
|
||||
hidden?:boolean
|
||||
}
|
||||
|
||||
/**## ODConsoleMessage `class`
|
||||
* This is an open ticket console message.
|
||||
*
|
||||
* It is used to create beautiful & styled logs in the console with a prefix, message & parameters.
|
||||
* It also has full color support using `ansis` and parameters are parsed for you!
|
||||
*/
|
||||
export class ODConsoleMessage {
|
||||
/**The main message sent in the console */
|
||||
message: string
|
||||
/**An array of all the parameters in this message */
|
||||
params: ODConsoleMessageParam[]
|
||||
/**The prefix of this message (!uppercase recommended!) */
|
||||
prefix: string
|
||||
/**The color of the prefix of this message */
|
||||
color: ODValidConsoleColor
|
||||
|
||||
constructor(message:string, prefix:string, color:ODValidConsoleColor, params?:ODConsoleMessageParam[]){
|
||||
this.message = message
|
||||
this.params = params ? params : []
|
||||
this.prefix = prefix
|
||||
|
||||
if (["white","red","yellow","green","blue","gray","cyan","magenta"].includes(color)){
|
||||
this.color = color
|
||||
}else{
|
||||
this.color = "white"
|
||||
}
|
||||
}
|
||||
/**Render this message to the console using `console.log`! Returns `false` when something went wrong. */
|
||||
render(){
|
||||
try {
|
||||
const prefixcolor = ansis[this.color]
|
||||
|
||||
const paramsstring = " "+this.createParamsString("gray")
|
||||
const message = prefixcolor("["+this.prefix+"] ")+this.message
|
||||
|
||||
console.log(message+paramsstring)
|
||||
return true
|
||||
}catch{
|
||||
return false
|
||||
}
|
||||
}
|
||||
/**Create a more-detailed, non-colored version of this message to store it in the `otdebug.txt` file! */
|
||||
toDebugString(){
|
||||
const pstrings: string[] = []
|
||||
this.params.forEach((p) => {
|
||||
pstrings.push(p.key+": "+p.value)
|
||||
})
|
||||
const pstring = (pstrings.length > 0) ? " ("+pstrings.join(", ")+")" : ""
|
||||
const date = new Date()
|
||||
const dstring = `${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
|
||||
return `[${dstring} ${this.prefix}] ${this.message}${pstring}`
|
||||
}
|
||||
/**Render the parameters of this message in a specific color. */
|
||||
createParamsString(color:ODValidConsoleColor){
|
||||
let validcolor: ODValidConsoleColor = "white"
|
||||
if (["white","red","yellow","green","blue","gray","cyan","magenta"].includes(color)){
|
||||
validcolor = color
|
||||
}
|
||||
|
||||
const pstrings: string[] = []
|
||||
this.params.forEach((p) => {
|
||||
if (!p.hidden) pstrings.push(p.key+": "+p.value)
|
||||
})
|
||||
|
||||
return (pstrings.length > 0) ? ansis[validcolor](" ("+pstrings.join(", ")+")") : ""
|
||||
}
|
||||
/**Set the message */
|
||||
setMessage(message:string){
|
||||
this.message = message
|
||||
return this
|
||||
}
|
||||
/**Set the params */
|
||||
setParams(params:ODConsoleMessageParam[]){
|
||||
this.params = params
|
||||
return this
|
||||
}
|
||||
/**Set the prefix */
|
||||
setPrefix(prefix:string){
|
||||
this.prefix = prefix
|
||||
return this
|
||||
}
|
||||
/**Set the prefix color */
|
||||
setColor(color:ODValidConsoleColor){
|
||||
if (["white","red","yellow","green","blue","gray","cyan","magenta"].includes(color)){
|
||||
this.color = color
|
||||
}else{
|
||||
this.color = "white"
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConsoleInfoMessage `class`
|
||||
* This is an open ticket console info message.
|
||||
*
|
||||
* It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "INFO" messages!
|
||||
*/
|
||||
export class ODConsoleInfoMessage extends ODConsoleMessage {
|
||||
constructor(message:string,params?:ODConsoleMessageParam[]){
|
||||
super(message,"INFO","blue",params)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConsoleSystemMessage `class`
|
||||
* This is an open ticket console system message.
|
||||
*
|
||||
* It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "SYSTEM" messages!
|
||||
*/
|
||||
export class ODConsoleSystemMessage extends ODConsoleMessage {
|
||||
constructor(message:string,params?:ODConsoleMessageParam[]){
|
||||
super(message,"SYSTEM","green",params)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConsolePluginMessage `class`
|
||||
* This is an open ticket console plugin message.
|
||||
*
|
||||
* It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "PLUGIN" messages!
|
||||
*/
|
||||
export class ODConsolePluginMessage extends ODConsoleMessage {
|
||||
constructor(message:string,params?:ODConsoleMessageParam[]){
|
||||
super(message,"PLUGIN","magenta",params)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConsoleDebugMessage `class`
|
||||
* This is an open ticket console debug message.
|
||||
*
|
||||
* It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "DEBUG" messages!
|
||||
*/
|
||||
export class ODConsoleDebugMessage extends ODConsoleMessage {
|
||||
constructor(message:string,params?:ODConsoleMessageParam[]){
|
||||
super(message,"DEBUG","cyan",params)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConsoleWarningMessage `class`
|
||||
* This is an open ticket console warning message.
|
||||
*
|
||||
* It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "WARNING" messages!
|
||||
*/
|
||||
export class ODConsoleWarningMessage extends ODConsoleMessage {
|
||||
constructor(message:string,params?:ODConsoleMessageParam[]){
|
||||
super(message,"WARNING","yellow",params)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConsoleErrorMessage `class`
|
||||
* This is an open ticket console error message.
|
||||
*
|
||||
* It is the same as a normal `ODConsoleMessage`, but it has a predefined prefix & color scheme for the "ERROR" messages!
|
||||
*/
|
||||
export class ODConsoleErrorMessage extends ODConsoleMessage {
|
||||
constructor(message:string,params?:ODConsoleMessageParam[]){
|
||||
super(message,"ERROR","red",params)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODError `class`
|
||||
* This is an open ticket error.
|
||||
*
|
||||
* It is used to render and log Node.js errors & crashes in a styled way to the console & `otdebug.txt` file!
|
||||
*/
|
||||
export class ODError {
|
||||
/**The original error that this class wraps around */
|
||||
error: Error|ODSystemError|ODPluginError
|
||||
/**The origin of the original error */
|
||||
origin: NodeJS.UncaughtExceptionOrigin
|
||||
|
||||
constructor(error:Error|ODSystemError|ODPluginError, origin:NodeJS.UncaughtExceptionOrigin){
|
||||
this.error = error
|
||||
this.origin = origin
|
||||
}
|
||||
|
||||
/**Render this error to the console using `console.log`! Returns `false` when something went wrong. */
|
||||
render(){
|
||||
try {
|
||||
let prefix = (this.error["_ODErrorType"] == "plugin") ? "PLUGIN ERROR" : ((this.error["_ODErrorType"] == "system") ? "OPENTICKET ERROR" : "UNKNOWN ERROR")
|
||||
//title
|
||||
console.log(ansis.red("["+prefix+"]: ")+this.error.message+" | origin: "+this.origin)
|
||||
//stack trace
|
||||
if (this.error.stack) console.log(ansis.gray(this.error.stack))
|
||||
//additional message
|
||||
if (this.error["_ODErrorType"] == "plugin") console.log(ansis.red.bold("\nPlease report this error to the plugin developer and help us create a more stable plugin!"))
|
||||
else console.log(ansis.red.bold("\nPlease report this error to our discord server and help us create a more stable ticket bot!"))
|
||||
console.log(ansis.red("Also send the "+ansis.cyan.bold("otdebug.txt")+" file! It would help a lot!\n"))
|
||||
return true
|
||||
}catch{
|
||||
return false
|
||||
}
|
||||
}
|
||||
/**Create a more-detailed, non-colored version of this error to store it in the `otdebug.txt` file! */
|
||||
toDebugString(){
|
||||
return "[UNKNOWN OD ERROR]: "+this.error.message+" | origin: "+this.origin+"\n"+this.error.stack
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODConsoleMessageTypes `type`
|
||||
* This is a collection of all the default console message types within Open Ticket.
|
||||
*/
|
||||
export type ODConsoleMessageTypes = "info"|"system"|"plugin"|"debug"|"warning"|"error"
|
||||
|
||||
/**## ODConsoleManager `class`
|
||||
* This is the open ticket console manager.
|
||||
*
|
||||
* It handles the entire console system of Open Ticket. It's also the place where you need to log `ODConsoleMessage`'s.
|
||||
* This manager keeps a short history of messages sent to the console which is configurable by plugins.
|
||||
*
|
||||
* The debug file (`otdebug.txt`) is handled in a sub-manager!
|
||||
*/
|
||||
export class ODConsoleManager {
|
||||
/**The history of `ODConsoleMessage`'s and `ODError`'s since startup */
|
||||
history: (ODConsoleMessage|ODError)[] = []
|
||||
/**The max length of the history. The oldest messages will be removed when over the limit */
|
||||
historylength = 100
|
||||
/**An alias to the debugfile manager. (`otdebug.txt`) */
|
||||
debugfile: ODDebugFileManager
|
||||
|
||||
constructor(historylength:number, debugfile:ODDebugFileManager){
|
||||
this.historylength = historylength
|
||||
this.debugfile = debugfile
|
||||
}
|
||||
|
||||
/**Log a message to the console ... But in the Open Ticket way :) */
|
||||
log(message:ODConsoleMessage): void
|
||||
log(message:ODError): void
|
||||
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.debugfile) this.debugfile.writeConsoleMessage(message)
|
||||
this.history.push(message)
|
||||
|
||||
}else if (message instanceof ODError){
|
||||
message.render()
|
||||
if (this.debugfile) this.debugfile.writeErrorMessage(message)
|
||||
this.history.push(message)
|
||||
|
||||
}else if (["string","number","boolean","object"].includes(typeof message)){
|
||||
let newMessage: ODConsoleMessage
|
||||
if (type == "info") newMessage = new ODConsoleInfoMessage(message,params)
|
||||
else if (type == "system") newMessage = new ODConsoleSystemMessage(message,params)
|
||||
else if (type == "plugin") newMessage = new ODConsolePluginMessage(message,params)
|
||||
else if (type == "debug") newMessage = new ODConsoleDebugMessage(message,params)
|
||||
else if (type == "warning") newMessage = new ODConsoleWarningMessage(message,params)
|
||||
else if (type == "error") newMessage = new ODConsoleErrorMessage(message,params)
|
||||
else newMessage = new ODConsoleSystemMessage(message,params)
|
||||
|
||||
newMessage.render()
|
||||
if (this.debugfile) this.debugfile.writeConsoleMessage(newMessage)
|
||||
this.history.push(newMessage)
|
||||
}
|
||||
this.#purgeHistory()
|
||||
}
|
||||
/**Shorten the history when it exceeds the max history length! */
|
||||
#purgeHistory(){
|
||||
if (this.history.length > this.historylength) this.history.shift()
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODDebugFileManager `class`
|
||||
* This is the open ticket debug file manager.
|
||||
*
|
||||
* It manages the Open Ticket debug file (`otdebug.txt`) which keeps a history of all system logs.
|
||||
* There are even internal logs that aren't logged to the console which are available in this file!
|
||||
*
|
||||
* Using this class, you can change the max length of this file and some other cool things!
|
||||
*/
|
||||
export class ODDebugFileManager {
|
||||
/**The path to the debugfile (`./otdebug.txt` by default) */
|
||||
path: string
|
||||
/**The filename of the debugfile (`otdebug.txt` by default) */
|
||||
filename: string
|
||||
/**The current version of the bot used in the debug file. */
|
||||
version: ODVersion
|
||||
/**The max length of the debug file. */
|
||||
maxlines: number
|
||||
|
||||
constructor(path:string, filename:string, maxlines:number, version:ODVersion){
|
||||
this.path = nodepath.join(path,filename)
|
||||
this.filename = filename
|
||||
this.version = version
|
||||
this.maxlines = maxlines
|
||||
|
||||
this.#writeStartupStats()
|
||||
}
|
||||
|
||||
/**Check if the debug file exists */
|
||||
#existsDebugFile(){
|
||||
return fs.existsSync(this.path)
|
||||
}
|
||||
/**Read from the debug file */
|
||||
#readDebugFile(){
|
||||
if (this.#existsDebugFile()){
|
||||
try {
|
||||
return fs.readFileSync(this.path).toString()
|
||||
}catch{
|
||||
return false
|
||||
}
|
||||
}else{
|
||||
return false
|
||||
}
|
||||
}
|
||||
/**Write to the debug file and shorten it when needed. */
|
||||
#writeDebugFile(text:string){
|
||||
const currenttext = this.#readDebugFile()
|
||||
if (currenttext){
|
||||
const splitted = currenttext.split("\n")
|
||||
|
||||
if (splitted.length+text.split("\n").length > this.maxlines){
|
||||
splitted.splice(7,(text.split("\n").length))
|
||||
}
|
||||
|
||||
splitted.push(text)
|
||||
fs.writeFileSync(this.path,splitted.join("\n"))
|
||||
}else{
|
||||
//write new file:
|
||||
const newtext = this.#createStatsText()+text
|
||||
fs.writeFileSync(this.path,newtext)
|
||||
}
|
||||
}
|
||||
/**Generate the stats/header of the debug file (containing the version) */
|
||||
#createStatsText(){
|
||||
const date = new Date()
|
||||
const dstring = `${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
|
||||
return [
|
||||
"=========================",
|
||||
"OPEN TICKET DEBUG FILE:",
|
||||
"version: "+this.version.toString(),
|
||||
"last startup: "+dstring,
|
||||
"=========================\n\n"
|
||||
].join("\n")
|
||||
}
|
||||
/**Write the stats/header to the debug file on startup */
|
||||
#writeStartupStats(){
|
||||
const currenttext = this.#readDebugFile()
|
||||
if (currenttext){
|
||||
//edit previous file:
|
||||
const splitted = currenttext.split("\n")
|
||||
splitted.splice(0,7)
|
||||
|
||||
if (splitted.length+11 > this.maxlines){
|
||||
splitted.splice(0,((splitted.length+11) - this.maxlines))
|
||||
}
|
||||
|
||||
splitted.unshift(this.#createStatsText())
|
||||
splitted.push("\n---------------------------------------------------------------------\n---------------------------------------------------------------------\n")
|
||||
|
||||
fs.writeFileSync(this.path,splitted.join("\n"))
|
||||
}else{
|
||||
//write new file:
|
||||
const newtext = this.#createStatsText()
|
||||
fs.writeFileSync(this.path,newtext)
|
||||
}
|
||||
}
|
||||
/**Write an `ODConsoleMessage` to the debug file */
|
||||
writeConsoleMessage(message:ODConsoleMessage){
|
||||
this.#writeDebugFile(message.toDebugString())
|
||||
}
|
||||
/**Write an `ODError` to the debug file */
|
||||
writeErrorMessage(error:ODError){
|
||||
this.#writeDebugFile(error.toDebugString())
|
||||
}
|
||||
/**Write custom text to the debug file */
|
||||
writeText(text:string){
|
||||
this.#writeDebugFile(text)
|
||||
}
|
||||
/**Write a custom note to the debug file (starting with `[NODE]:`) */
|
||||
writeNote(text:string){
|
||||
this.#writeDebugFile("[NODE]: "+text)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODDebugger `class`
|
||||
* This is the open ticket debugger.
|
||||
*
|
||||
* It is a simple wrapper around the `ODConsoleManager` to handle debugging (primarily for `ODManagers`).
|
||||
* Messages created using this debugger are only logged to the debug file unless specified otherwise.
|
||||
*
|
||||
* You will probably notice this class being used in the `ODManager` constructor.
|
||||
*
|
||||
* Using this system, all additions & removals inside a manager are logged to the debug file. This makes searching for errors a lot easier!
|
||||
*/
|
||||
export class ODDebugger {
|
||||
/**An alias to the open ticket console manager. */
|
||||
console: ODConsoleManager
|
||||
/**When enabled, debug logs are also shown in the console. */
|
||||
visible: boolean = false
|
||||
|
||||
constructor(console:ODConsoleManager){
|
||||
this.console = console
|
||||
}
|
||||
|
||||
/**Create a debug message. This will always be logged to `otdebug.txt` & sometimes to the console (when enabled). Returns `true` when visible */
|
||||
debug(message:string, params?:{key:string,value:string}[]): boolean {
|
||||
if (this.visible){
|
||||
this.console.log(new ODConsoleDebugMessage(message,params))
|
||||
return true
|
||||
}else{
|
||||
this.console.debugfile.writeConsoleMessage(new ODConsoleDebugMessage(message,params))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODLivestatusColor `type`
|
||||
* This is a collection of all the colors available within the LiveStatus system.
|
||||
*/
|
||||
export type ODLiveStatusColor = "normal"|"red"|"green"|"blue"|"yellow"|"white"|"gray"|"magenta"|"cyan"
|
||||
|
||||
/**## ODLiveStatusSourceData `interface`
|
||||
* This is an interface containing all raw data received from the LiveStatus system.
|
||||
*/
|
||||
export interface ODLiveStatusSourceData {
|
||||
/**The message to display */
|
||||
message:{
|
||||
/**The title of the message to display */
|
||||
title:string,
|
||||
/**The title color of the message to display */
|
||||
titleColor:ODLiveStatusColor,
|
||||
/**The description of the message to display */
|
||||
description:string,
|
||||
/**The description color of the message to display */
|
||||
descriptionColor:ODLiveStatusColor
|
||||
},
|
||||
/**The message will only be shown when the bot matches all statements */
|
||||
active:{
|
||||
/**A list of versions to match */
|
||||
versions:string[],
|
||||
/**A list of languages to match */
|
||||
languages:string[],
|
||||
/**All languages should match */
|
||||
allLanguages:boolean,
|
||||
/**Match when the bot is using plugins */
|
||||
usingPlugins:boolean,
|
||||
/**Match when the bot is not using plugins */
|
||||
notUsingPlugins:boolean,
|
||||
/**Match when the bot is using slash commands */
|
||||
usingSlashCommands:boolean,
|
||||
/**Match when the bot is not using slash commands */
|
||||
notUsingSlashCommands:boolean,
|
||||
/**Match when the bot is not using transcripts */
|
||||
notUsingTranscripts:boolean,
|
||||
/**Match when the bot is using text transcripts */
|
||||
usingTextTranscripts:boolean,
|
||||
/**Match when the bot is using html transcripts */
|
||||
usingHtmlTranscripts:boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODLiveStatusSource `class`
|
||||
* This is the open ticket livestatus source.
|
||||
*
|
||||
* It is an empty template for a livestatus source.
|
||||
* By default, you should use `ODLiveStatusUrlSource` or `ODLiveStatusFileSource`,
|
||||
* unless you want to create one on your own!
|
||||
*
|
||||
* This class doesn't do anything on it's own! It's just a template!
|
||||
*/
|
||||
export class ODLiveStatusSource extends ODManagerData {
|
||||
/**The raw data of this source */
|
||||
data: ODLiveStatusSourceData[]
|
||||
|
||||
constructor(id:ODValidId, data:ODLiveStatusSourceData[]){
|
||||
super(id)
|
||||
this.data = data
|
||||
}
|
||||
|
||||
/**Change the current data using this method! */
|
||||
setData(data:ODLiveStatusSourceData[]){
|
||||
this.data = data
|
||||
}
|
||||
/**Get all messages relevant to the bot based on some parameters. */
|
||||
async getMessages(main:ODMain): Promise<ODLiveStatusSourceData[]> {
|
||||
const validMessages: ODLiveStatusSourceData[] = []
|
||||
|
||||
//parse data from ODMain
|
||||
const currentVersion: string = main.versions.get("openticket:version").toString(true)
|
||||
const usingSlashCommands: boolean = main.configs.get("openticket:general").data.slashCommands
|
||||
const usingTranscripts: false|"text"|"html" = false as false|"text"|"html" //TODO
|
||||
const currentLanguage: string = main.languages.getCurrentLanguageId()
|
||||
const usingPlugins: boolean = (main.plugins.getLength() > 0)
|
||||
|
||||
//check data for each message
|
||||
this.data.forEach((msg) => {
|
||||
const {active} = msg
|
||||
|
||||
const correctVersion = active.versions.includes(currentVersion)
|
||||
const correctSlashMode = (usingSlashCommands && active.usingSlashCommands) || (!usingSlashCommands && active.notUsingSlashCommands)
|
||||
const correctTranscriptMode = (usingTranscripts == "text" && active.usingTextTranscripts) || (usingTranscripts == "html" && active.usingHtmlTranscripts) || (!usingTranscripts && active.notUsingTranscripts)
|
||||
const correctLanguage = active.languages.includes(currentLanguage) || active.allLanguages
|
||||
const correctPlugins = (usingPlugins && active.usingPlugins) || (!usingPlugins && active.notUsingPlugins)
|
||||
|
||||
if (correctVersion && correctLanguage && correctPlugins && correctSlashMode && correctTranscriptMode) validMessages.push(msg)
|
||||
})
|
||||
|
||||
//return the valid messages
|
||||
return validMessages
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODLiveStatusFileSource `class`
|
||||
* This is the open ticket livestatus file source.
|
||||
*
|
||||
* It is a LiveStatus source that will read the data from a local file.
|
||||
*
|
||||
* This can be used for testing/extending the LiveStatus system!
|
||||
*/
|
||||
export class ODLiveStatusFileSource extends ODLiveStatusSource {
|
||||
/**The path to the source file */
|
||||
path: string
|
||||
|
||||
constructor(id:ODValidId, path:string){
|
||||
if (fs.existsSync(path)){
|
||||
super(id,JSON.parse(fs.readFileSync(path).toString()))
|
||||
}else throw new ODSystemError("LiveStatus source file doesn't exist!")
|
||||
this.path = path
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODLiveStatusUrlSource `class`
|
||||
* This is the open ticket livestatus url source.
|
||||
*
|
||||
* It is a LiveStatus source that will read the data from a http URL (json file).
|
||||
*
|
||||
* This is the default way of receiving LiveStatus messages!
|
||||
*/
|
||||
export class ODLiveStatusUrlSource extends ODLiveStatusSource {
|
||||
/**The url used in the request */
|
||||
url: string
|
||||
/**The `ODHTTPGetRequest` helper to fetch the url! */
|
||||
request: ODHTTPGetRequest
|
||||
|
||||
constructor(id:ODValidId, url:string){
|
||||
super(id,[])
|
||||
this.url = url
|
||||
this.request = new ODHTTPGetRequest(url,false)
|
||||
}
|
||||
async getMessages(main:ODMain): Promise<ODLiveStatusSourceData[]> {
|
||||
//additional setup
|
||||
this.request.url = this.url
|
||||
const rawRes = await this.request.run()
|
||||
if (rawRes.status != 200) throw new ODSystemError("ODLiveStatusUrlSource => Request Failed!")
|
||||
try{
|
||||
this.setData(JSON.parse(rawRes.body))
|
||||
}catch{
|
||||
throw new ODSystemError("ODLiveStatusUrlSource => Request Failed!")
|
||||
}
|
||||
|
||||
//default
|
||||
return super.getMessages(main)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODLiveStatusManager `class`
|
||||
* This is the open ticket livestatus manager.
|
||||
*
|
||||
* It manages all LiveStatus sources and has the renderer for all LiveStatus messages.
|
||||
*
|
||||
* You will probably use this to customise or add stuff to the LiveStatus system.
|
||||
* Access it in the global `openticket.startscreen.livestatus` variable!
|
||||
*/
|
||||
export class ODLiveStatusManager extends ODManager<ODLiveStatusSource> {
|
||||
/**The class responsible for rendering the livestatus messages. */
|
||||
renderer: ODLiveStatusRenderer
|
||||
/**A reference to the ODMain or "openticket" global variable */
|
||||
#main: ODMain
|
||||
|
||||
constructor(debug:ODDebugger, main:ODMain){
|
||||
super(debug,"livestatus source")
|
||||
this.renderer = new ODLiveStatusRenderer(main.console)
|
||||
this.#main = main
|
||||
}
|
||||
|
||||
/**Get the messages from all sources combined! */
|
||||
async getAllMessages(): Promise<ODLiveStatusSourceData[]> {
|
||||
const messages: ODLiveStatusSourceData[] = []
|
||||
for (const source of this.getAll()){
|
||||
try {
|
||||
messages.push(...(await source.getMessages(this.#main)))
|
||||
}catch{}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODLiveStatusRenderer `class`
|
||||
* This is the open ticket livestatus renderer.
|
||||
*
|
||||
* It's responsible for rendering all LiveStatus messages to the console.
|
||||
*/
|
||||
export class ODLiveStatusRenderer {
|
||||
/**A reference to the ODConsoleManager or "openticket.console" global variable */
|
||||
#console: ODConsoleManager
|
||||
|
||||
constructor(console:ODConsoleManager){
|
||||
this.#console = console
|
||||
}
|
||||
|
||||
/**Render all messages */
|
||||
render(messages:ODLiveStatusSourceData[]): string {
|
||||
try {
|
||||
//process data
|
||||
const final: string[] = []
|
||||
messages.forEach((msg) => {
|
||||
const titleColor = msg.message.titleColor
|
||||
const title = "["+msg.message.title+"] "
|
||||
|
||||
const descriptionColor = msg.message.descriptionColor
|
||||
const description = msg.message.description.split("\n").map((text,row) => {
|
||||
//first row row doesn't need prefix
|
||||
if (row < 1) return text
|
||||
//other rows do need a prefix
|
||||
let text2 = text
|
||||
for (const i of title){
|
||||
text2 = " "+text2
|
||||
}
|
||||
return text2
|
||||
}).join("\n")
|
||||
|
||||
|
||||
if (!["red","yellow","green","blue","gray","magenta","cyan"].includes(titleColor)) var finalTitle = ansis.white(title)
|
||||
else var finalTitle = ansis[titleColor](title)
|
||||
if (!["red","yellow","green","blue","gray","magenta","cyan"].includes(descriptionColor)) var finalDescription = ansis.white(description)
|
||||
else var finalDescription = ansis[descriptionColor](description)
|
||||
|
||||
final.push(finalTitle+finalDescription)
|
||||
})
|
||||
|
||||
//return all messages
|
||||
return final.join("\n")
|
||||
}catch{
|
||||
this.#console.log("Failed to render LiveStatus messages!","error")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
///////////////////////////////////////
|
||||
//COOLDOWN MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODValidId, ODManager, ODSystemError, ODManagerData } from "./base"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
/**## ODCooldownManager `class`
|
||||
* This is an open ticket cooldown manager.
|
||||
*
|
||||
* It is responsible for managing all cooldowns in Open Ticket. An example of this is the ticket creation cooldown.
|
||||
*
|
||||
* There are many types of cooldowns available, but you can also create your own!
|
||||
*/
|
||||
export class ODCooldownManager extends ODManager<ODCooldown<object>> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"cooldown")
|
||||
}
|
||||
/**Initiate all cooldowns in this manager. */
|
||||
async init(){
|
||||
for (const cooldown of this.getAll()){
|
||||
await cooldown.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODCooldownData `class`
|
||||
* This is open ticket cooldown data.
|
||||
*
|
||||
* It contains the instance of an active cooldown (e.g. for a user). It is handled by the cooldown itself.
|
||||
*/
|
||||
export class ODCooldownData<Data extends object> extends ODManagerData {
|
||||
/**Is this cooldown active? */
|
||||
active: boolean
|
||||
/**Additional data of this cooldown instance. (different for each cooldown type) */
|
||||
data: Data
|
||||
|
||||
constructor(id:ODValidId,active:boolean,data:Data){
|
||||
super(id)
|
||||
this.active = active
|
||||
this.data = data
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODCooldown `class`
|
||||
* This is an open ticket cooldown.
|
||||
*
|
||||
* It doesn't do anything on it's own, but it provides the methods that are used to interact with a cooldown.
|
||||
* This class can be extended from to create a working cooldown.
|
||||
*
|
||||
* There are also premade cooldowns available in the bot!
|
||||
*/
|
||||
export class ODCooldown<Data extends object> extends ODManagerData {
|
||||
data: ODManager<ODCooldownData<Data>> = new ODManager()
|
||||
/**Is this cooldown already initialized? */
|
||||
ready: boolean = false
|
||||
|
||||
constructor(id:ODValidId){
|
||||
super(id)
|
||||
}
|
||||
|
||||
/**Check this id and start cooldown when it exeeds the limit! Returns `true` when on cooldown! */
|
||||
use(id:string): boolean {
|
||||
throw new ODSystemError("Tried to use an unimplemented ODCooldown!")
|
||||
}
|
||||
/**Check this id without starting or updating the cooldown. Returns `true` when on cooldown! */
|
||||
check(id:string): boolean {
|
||||
throw new ODSystemError("Tried to use an unimplemented ODCooldown!")
|
||||
}
|
||||
/**Remove the cooldown for an id when available.*/
|
||||
delete(id:string){
|
||||
throw new ODSystemError("Tried to use an unimplemented ODCooldown!")
|
||||
}
|
||||
/**Initialize the internal systems of this cooldown. */
|
||||
async init(){
|
||||
throw new ODSystemError("Tried to use an unimplemented ODCooldown!")
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODCounterCooldown `class`
|
||||
* This is an open ticket counter cooldown.
|
||||
*
|
||||
* It is is a cooldown based on a counter. When the number exceeds the limit, the cooldown is activated.
|
||||
* The number will automatically be decreased with a set amount & interval.
|
||||
*/
|
||||
export class ODCounterCooldown extends ODCooldown<{value:number}> {
|
||||
/**The cooldown will activate when exceeding this limit. */
|
||||
activeLimit: number
|
||||
/**The cooldown will deactivate when below this limit. */
|
||||
cancelLimit: number
|
||||
/**The amount to increase the counter with everytime the cooldown is triggered/updated. */
|
||||
increment: number
|
||||
/**The amount to decrease the counter over time. */
|
||||
decrement: number
|
||||
/**The interval between decrements in milliseconds. */
|
||||
invervalMs: number
|
||||
|
||||
constructor(id:ODValidId, activeLimit:number, cancelLimit:number, increment:number, decrement:number, intervalMs:number){
|
||||
super(id)
|
||||
this.activeLimit = activeLimit
|
||||
this.cancelLimit = cancelLimit
|
||||
this.increment = increment
|
||||
this.decrement = decrement
|
||||
this.invervalMs = intervalMs
|
||||
}
|
||||
|
||||
use(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
if (cooldown.active){
|
||||
return true
|
||||
|
||||
}else if (cooldown.data.value < this.activeLimit){
|
||||
cooldown.data.value = cooldown.data.value + this.increment
|
||||
return false
|
||||
|
||||
}else{
|
||||
cooldown.active = true
|
||||
return false
|
||||
}
|
||||
}else{
|
||||
//cooldown for this id doesn't exist
|
||||
this.data.add(new ODCooldownData(id,(this.increment >= this.activeLimit),{
|
||||
value:this.increment
|
||||
}))
|
||||
return false
|
||||
}
|
||||
}
|
||||
check(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
return cooldown.active
|
||||
}else return false
|
||||
}
|
||||
delete(id:string): void {
|
||||
this.data.remove(id)
|
||||
}
|
||||
async init(){
|
||||
if (this.ready) return
|
||||
setInterval(() => {
|
||||
this.data.getAll().forEach((cooldown) => {
|
||||
cooldown.data.value = cooldown.data.value - this.decrement
|
||||
if (cooldown.data.value <= this.cancelLimit){
|
||||
cooldown.active = false
|
||||
}
|
||||
if (cooldown.data.value <= 0){
|
||||
this.data.remove(cooldown.id)
|
||||
}
|
||||
})
|
||||
},this.invervalMs)
|
||||
this.ready = true
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODIncrementalCounterCooldown `class`
|
||||
* This is an open ticket incremental counter cooldown.
|
||||
*
|
||||
* It is is a cooldown based on an incremental counter. It is exactly the same as the normal counter,
|
||||
* with the only difference being that it still increments when the limit is already exeeded.
|
||||
*/
|
||||
export class ODIncrementalCounterCooldown extends ODCooldown<{value:number}> {
|
||||
/**The cooldown will activate when exceeding this limit. */
|
||||
activeLimit: number
|
||||
/**The cooldown will deactivate when below this limit. */
|
||||
cancelLimit: number
|
||||
/**The amount to increase the counter with everytime the cooldown is triggered/updated. */
|
||||
increment: number
|
||||
/**The amount to decrease the counter over time. */
|
||||
decrement: number
|
||||
/**The interval between decrements in milliseconds. */
|
||||
invervalMs: number
|
||||
|
||||
constructor(id:ODValidId, activeLimit:number, cancelLimit:number, increment:number, decrement:number, intervalMs:number){
|
||||
super(id)
|
||||
this.activeLimit = activeLimit
|
||||
this.cancelLimit = cancelLimit
|
||||
this.increment = increment
|
||||
this.decrement = decrement
|
||||
this.invervalMs = intervalMs
|
||||
}
|
||||
|
||||
use(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
if (cooldown.active){
|
||||
cooldown.data.value = cooldown.data.value + this.increment
|
||||
return true
|
||||
|
||||
}else if (cooldown.data.value < this.activeLimit){
|
||||
cooldown.data.value = cooldown.data.value + this.increment
|
||||
return false
|
||||
|
||||
}else{
|
||||
cooldown.active = true
|
||||
return false
|
||||
}
|
||||
}else{
|
||||
//cooldown for this id doesn't exist
|
||||
this.data.add(new ODCooldownData(id,(this.increment >= this.activeLimit),{
|
||||
value:this.increment
|
||||
}))
|
||||
return false
|
||||
}
|
||||
}
|
||||
check(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
return cooldown.active
|
||||
}else return false
|
||||
}
|
||||
delete(id:string): void {
|
||||
this.data.remove(id)
|
||||
}
|
||||
async init(){
|
||||
if (this.ready) return
|
||||
setInterval(() => {
|
||||
this.data.getAll().forEach((cooldown) => {
|
||||
cooldown.data.value = cooldown.data.value - this.decrement
|
||||
if (cooldown.data.value <= this.cancelLimit){
|
||||
cooldown.active = false
|
||||
}
|
||||
if (cooldown.data.value <= 0){
|
||||
this.data.remove(cooldown.id)
|
||||
}
|
||||
})
|
||||
},this.invervalMs)
|
||||
this.ready = true
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODTimeoutCooldown `class`
|
||||
* This is an open ticket timeout cooldown.
|
||||
*
|
||||
* It is a cooldown based on a timer. When triggered/updated, the cooldown is activated for the set amount of time.
|
||||
* After the timer has timed out, the cooldown will be deleted.
|
||||
*/
|
||||
export class ODTimeoutCooldown extends ODCooldown<{date:number}> {
|
||||
/**The amount of milliseconds before the cooldown times-out */
|
||||
timeoutMs: number
|
||||
|
||||
constructor(id:ODValidId, timeoutMs:number){
|
||||
super(id)
|
||||
this.timeoutMs = timeoutMs
|
||||
}
|
||||
|
||||
use(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
if ((new Date().getTime() - cooldown.data.date) > this.timeoutMs){
|
||||
this.data.remove(id)
|
||||
return false
|
||||
}else{
|
||||
return true
|
||||
}
|
||||
}else{
|
||||
//cooldown for this id doesn't exist
|
||||
this.data.add(new ODCooldownData(id,true,{
|
||||
date:new Date().getTime()
|
||||
}))
|
||||
return false
|
||||
}
|
||||
}
|
||||
check(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
return true
|
||||
}else return false
|
||||
}
|
||||
delete(id:string): void {
|
||||
this.data.remove(id)
|
||||
}
|
||||
/**Get the remaining amount of milliseconds before the timeout stops. */
|
||||
remaining(id:string): number|null {
|
||||
const cooldown = this.data.get(id)
|
||||
if (!cooldown) return null
|
||||
const rawResult = this.timeoutMs - (new Date().getTime() - cooldown.data.date)
|
||||
return (rawResult > 0) ? rawResult : 0
|
||||
}
|
||||
async init(){
|
||||
if (this.ready) return
|
||||
this.ready = true
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODIncrementalTimeoutCooldown `class`
|
||||
* This is an open ticket incremental timeout cooldown.
|
||||
*
|
||||
* It is is a cooldown based on an incremental timer. It is exactly the same as the normal timer,
|
||||
* with the only difference being that it adds additional time when triggered/updated while the cooldown is already active.
|
||||
*/
|
||||
export class ODIncrementalTimeoutCooldown extends ODCooldown<{date:number}> {
|
||||
/**The amount of milliseconds before the cooldown times-out */
|
||||
timeoutMs: number
|
||||
/**The amount of milliseconds to add when triggered/updated while the cooldown is already active. */
|
||||
incrementMs: number
|
||||
|
||||
constructor(id:ODValidId, timeoutMs:number, incrementMs:number){
|
||||
super(id)
|
||||
this.timeoutMs = timeoutMs
|
||||
this.incrementMs = incrementMs
|
||||
}
|
||||
|
||||
use(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
if ((new Date().getTime() - cooldown.data.date) > this.timeoutMs){
|
||||
this.data.remove(id)
|
||||
return false
|
||||
}else{
|
||||
cooldown.data.date = cooldown.data.date + this.incrementMs
|
||||
return true
|
||||
}
|
||||
}else{
|
||||
//cooldown for this id doesn't exist
|
||||
this.data.add(new ODCooldownData(id,true,{
|
||||
date:new Date().getTime()
|
||||
}))
|
||||
return false
|
||||
}
|
||||
}
|
||||
check(id:string): boolean {
|
||||
const cooldown = this.data.get(id)
|
||||
if (cooldown){
|
||||
//cooldown for this id already exists
|
||||
return true
|
||||
}else return false
|
||||
}
|
||||
delete(id:string): void {
|
||||
this.data.remove(id)
|
||||
}
|
||||
/**Get the remaining amount of milliseconds before the timeout stops. */
|
||||
remaining(id:string): number|null {
|
||||
const cooldown = this.data.get(id)
|
||||
if (!cooldown) return null
|
||||
const rawResult = this.timeoutMs - (new Date().getTime() - cooldown.data.date)
|
||||
return (rawResult > 0) ? rawResult : 0
|
||||
}
|
||||
async init(){
|
||||
if (this.ready) return
|
||||
this.ready = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
///////////////////////////////////////
|
||||
//DATABASE MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId, ODValidJsonType } from "./base"
|
||||
import fs from "fs"
|
||||
import nodepath from "path"
|
||||
import { ODDebugger } from "./console"
|
||||
import * as fjs from "formatted-json-stringify"
|
||||
|
||||
/**## ODDatabaseManager `class`
|
||||
* This is an open ticket database manager.
|
||||
*
|
||||
* It manages all databases in the bot and allows to permanently store data from the bot!
|
||||
*
|
||||
* You will use this class to get/add a database (`ODDatabase`) in your plugin!
|
||||
* @example
|
||||
* //get ./database/ot-global.json => ODDatabase class
|
||||
* const globalDB = openticket.databases.get("openticket:global")
|
||||
*
|
||||
* //add a new database with id "test" => ./database/idk-test.json
|
||||
* const testDatabase = new api.ODDatabase("test","idk-test.json")
|
||||
* openticket.databases.add(testDatabase)
|
||||
*/
|
||||
export class ODDatabaseManager extends ODManager<ODDatabase> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"database")
|
||||
}
|
||||
|
||||
/**Add data to the manager. The id will be fetched from the data class! You can optionally select to overwrite existing data!
|
||||
* @example
|
||||
* //add a new database with id "test" => ./database/idk-test.json
|
||||
* const testDatabase = new api.ODDatabase("test","idk-test.json")
|
||||
* openticket.databases.add(testDatabase)
|
||||
*/
|
||||
add(data:ODDatabase, overwrite?:boolean): boolean {
|
||||
return super.add(data,overwrite)
|
||||
}
|
||||
/**Get data that matches the `ODId`. Returns the found data.
|
||||
* @example
|
||||
* //get ./database/ot-global.json => ODDatabase class
|
||||
* const globalDB = openticket.databases.get("openticket:global")
|
||||
*/
|
||||
get(id:ODValidId): ODDatabase|null {
|
||||
return super.get(id)
|
||||
}
|
||||
/**Remove data that matches the `ODId`. Returns the removed data.
|
||||
* @example
|
||||
* //remove the "test" database
|
||||
* openticket.databases.remove("test") //returns null if non-existing
|
||||
*/
|
||||
remove(id:ODValidId): ODDatabase|null {
|
||||
return super.remove(id)
|
||||
}
|
||||
/**Check if data that matches the `ODId` exists. Returns a boolean.
|
||||
* @example
|
||||
* //check if "./database/idk-test.json" (test) exists => boolean
|
||||
* const exists = openticket.databases.exists("test")
|
||||
*/
|
||||
exists(id:ODValidId): boolean {
|
||||
return super.exists(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODDatabase `class`
|
||||
* This is an open ticket database template.
|
||||
* This class doesn't do anything at all, it just gives a template & basic methods for a database. Use `ODJsonDatabase` instead!
|
||||
*
|
||||
* You will only use this class if you want to create your own database implementation (e.g. `mongodb`, `mysql`,...)!
|
||||
* @example
|
||||
* class SomeDatabase extends ODDatabase {
|
||||
* //override this method
|
||||
* setData(category:string, key:string, value:ODValidJsonType): boolean {
|
||||
* return false
|
||||
* }
|
||||
* //override this method
|
||||
* getData(category:string, key:string): ODValidJsonType|undefined {
|
||||
* return undefined
|
||||
* }
|
||||
* //override this method
|
||||
* deleteData(category:string, key:string): boolean {
|
||||
* return false
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export class ODDatabase extends ODManagerData {
|
||||
/**The full path to this database with extension */
|
||||
file: string = ""
|
||||
|
||||
/**Add/Overwrite a specific category & key in the database. Returns `true` when overwritten. */
|
||||
set(category:string, key:string, value:ODValidJsonType): boolean {
|
||||
return false
|
||||
}
|
||||
/**Get a specific category & key in the database */
|
||||
get(category:string, key:string): ODValidJsonType|undefined {
|
||||
return undefined
|
||||
}
|
||||
/**Delete a specific category & key in the database */
|
||||
delete(category:string, key:string): boolean {
|
||||
return false
|
||||
}
|
||||
/**Check if a specific category & key exists in the database */
|
||||
exists(category:string, key:string): boolean {
|
||||
return false
|
||||
}
|
||||
/**Get a specific category in the database */
|
||||
getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined {
|
||||
return undefined
|
||||
}
|
||||
/**Get all values in the database */
|
||||
getAll(): ODJsonDatabaseStructure {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODJsonDatabaseStructure `type`
|
||||
* This is the structure of how a JSON database file!
|
||||
*/
|
||||
export type ODJsonDatabaseStructure = {category:string, key:string, value:ODValidJsonType}[]
|
||||
|
||||
/**## ODJsonDatabase `class`
|
||||
* This is an open ticket JSON database.
|
||||
* It stores data in a `json` file as a large `Array` using the `category`, `key`, `value` strategy.
|
||||
* You can store the following types: `string`, `number`, `boolean`, `array`, `object` & `null`!
|
||||
*
|
||||
* You will only use this class if you want to create your own database or use an existing one!
|
||||
* @example
|
||||
* //get,set & delete data
|
||||
* const data = database.getData("category","key") //data will be the value
|
||||
* const didOverwrite = database.setData("category","key","value") //value can be any of the valid types
|
||||
* const didExist = database.deleteData("category","key") //delete this value
|
||||
* //You need an ODJsonDatabase class named "database" for this example to work!
|
||||
*/
|
||||
export class ODJsonDatabase extends ODDatabase {
|
||||
constructor(id:ODValidId, file:string, customPath?:string){
|
||||
super(id)
|
||||
const filename = (file.endsWith(".json")) ? file : file+".json"
|
||||
this.file = customPath ? nodepath.join("./",customPath,filename) : nodepath.join("./database/",filename)
|
||||
|
||||
//init file if it doesn't exist yet
|
||||
this.#system.getData()
|
||||
}
|
||||
|
||||
/**Set/overwrite the value of `category` & `key`. Returns `true` when overwritten!
|
||||
* @example
|
||||
* const didOverwrite = database.setData("category","key","value") //value can be any of the valid types
|
||||
* //You need an ODJsonDatabase class named "database" for this example to work!
|
||||
*/
|
||||
set(category:string, key:string, value:ODValidJsonType): boolean {
|
||||
const currentList = this.#system.getData()
|
||||
const currentData = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
|
||||
//overwrite when already present
|
||||
if (currentData){
|
||||
currentList[currentList.indexOf(currentData)].value = value
|
||||
}else{
|
||||
currentList.push({category,key,value})
|
||||
}
|
||||
|
||||
this.#system.setData(currentList)
|
||||
return currentData ? true : false
|
||||
}
|
||||
/**Get the value of `category` & `key`. Returns `undefined` when non-existent!
|
||||
* @example
|
||||
* const data = database.getData("category","key") //data will be the value
|
||||
* //You need an ODJsonDatabase class named "database" for this example to work!
|
||||
*/
|
||||
get(category:string, key:string): ODValidJsonType|undefined {
|
||||
const currentList = this.#system.getData()
|
||||
const tempresult = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
return tempresult ? tempresult.value : undefined
|
||||
}
|
||||
/**Remove the value of `category` & `key`. Returns `undefined` when non-existent!
|
||||
* @example
|
||||
* const didExist = database.deleteData("category","key") //delete this value
|
||||
* //You need an ODJsonDatabase class named "database" for this example to work!
|
||||
*/
|
||||
delete(category:string, key:string): boolean {
|
||||
const currentList = this.#system.getData()
|
||||
const currentData = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
if (currentData) currentList.splice(currentList.indexOf(currentData),1)
|
||||
|
||||
this.#system.setData(currentList)
|
||||
return currentData ? true : false
|
||||
}
|
||||
/**Check if a value of `category` & `key` exists. Returns `false` when non-existent! */
|
||||
exists(category:string, key:string): boolean {
|
||||
const currentList = this.#system.getData()
|
||||
const tempresult = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
return tempresult ? true : false
|
||||
}
|
||||
/**Get all values in `category`. Returns `undefined` when non-existent! */
|
||||
getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined {
|
||||
const currentList = this.#system.getData()
|
||||
const tempresult = currentList.filter((d) => (d.category === category))
|
||||
return tempresult ? tempresult.map((data) => {return {key:data.key,value:data.value}}) : undefined
|
||||
}
|
||||
/**Get all values in `category`. */
|
||||
getAll(): ODJsonDatabaseStructure {
|
||||
return this.#system.getData()
|
||||
}
|
||||
|
||||
#system = {
|
||||
/**Read parsed data from the json file */
|
||||
getData: (): ODJsonDatabaseStructure => {
|
||||
if (fs.existsSync(this.file)){
|
||||
return JSON.parse(fs.readFileSync(this.file).toString())
|
||||
}else{
|
||||
fs.writeFileSync(this.file,"[]")
|
||||
return []
|
||||
}
|
||||
},
|
||||
/**Write parsed data to the json file */
|
||||
setData: (data:ODJsonDatabaseStructure) => {
|
||||
fs.writeFileSync(this.file,JSON.stringify(data,null,"\t"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**## ODFormattedJsonDatabase `class`
|
||||
* This is an open ticket Formatted JSON database.
|
||||
* It stores data in a `json` file as a large `Array` using the `category`, `key`, `value` strategy.
|
||||
* You can store the following types: `string`, `number`, `boolean`, `array`, `object` & `null`!
|
||||
*
|
||||
* This one is exactly the same as `ODJsonDatabase`, but it has a formatter from the `formatted-json-stringify` package.
|
||||
* This can help you organise it a little bit better!
|
||||
*/
|
||||
export class ODFormattedJsonDatabase extends ODDatabase {
|
||||
/**The formatter to use on the database array */
|
||||
formatter: fjs.ArrayFormatter
|
||||
|
||||
constructor(id:ODValidId, file:string, formatter:fjs.ArrayFormatter, customPath?:string){
|
||||
super(id)
|
||||
const filename = (file.endsWith(".json")) ? file : file+".json"
|
||||
this.file = customPath ? nodepath.join("./",customPath,filename) : nodepath.join("./database/",filename)
|
||||
this.formatter = formatter
|
||||
|
||||
//init file if it doesn't exist yet
|
||||
this.#system.getData()
|
||||
}
|
||||
|
||||
/**Set/overwrite the value of `category` & `key`. Returns `true` when overwritten!
|
||||
* @example
|
||||
* const didOverwrite = database.setData("category","key","value") //value can be any of the valid types
|
||||
* //You need an ODFormattedJsonDatabase class named "database" for this example to work!
|
||||
*/
|
||||
set(category:string, key:string, value:ODValidJsonType): boolean {
|
||||
const currentList = this.#system.getData()
|
||||
const currentData = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
|
||||
//overwrite when already present
|
||||
if (currentData){
|
||||
currentList[currentList.indexOf(currentData)].value = value
|
||||
}else{
|
||||
currentList.push({category,key,value})
|
||||
}
|
||||
|
||||
this.#system.setData(currentList)
|
||||
return currentData ? true : false
|
||||
}
|
||||
/**Get the value of `category` & `key`. Returns `undefined` when non-existent!
|
||||
* @example
|
||||
* const data = database.getData("category","key") //data will be the value
|
||||
* //You need an ODFormattedJsonDatabase class named "database" for this example to work!
|
||||
*/
|
||||
get(category:string, key:string): ODValidJsonType|undefined {
|
||||
const currentList = this.#system.getData()
|
||||
const tempresult = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
return tempresult ? tempresult.value : undefined
|
||||
}
|
||||
/**Remove the value of `category` & `key`. Returns `undefined` when non-existent!
|
||||
* @example
|
||||
* const didExist = database.deleteData("category","key") //delete this value
|
||||
* //You need an ODFormattedJsonDatabase class named "database" for this example to work!
|
||||
*/
|
||||
delete(category:string, key:string): boolean {
|
||||
const currentList = this.#system.getData()
|
||||
const currentData = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
if (currentData) currentList.splice(currentList.indexOf(currentData),1)
|
||||
|
||||
this.#system.setData(currentList)
|
||||
return currentData ? true : false
|
||||
}
|
||||
/**Check if a value of `category` & `key` exists. Returns `false` when non-existent! */
|
||||
exists(category:string, key:string): boolean {
|
||||
const currentList = this.#system.getData()
|
||||
const tempresult = currentList.find((d) => (d.category === category) && (d.key === key))
|
||||
return tempresult ? true : false
|
||||
}
|
||||
/**Get all values in `category`. Returns `undefined` when non-existent! */
|
||||
getCategory(category:string): {key:string, value:ODValidJsonType}[]|undefined {
|
||||
const currentList = this.#system.getData()
|
||||
const tempresult = currentList.filter((d) => (d.category === category))
|
||||
return tempresult ? tempresult.map((data) => {return {key:data.key,value:data.value}}) : undefined
|
||||
}
|
||||
/**Get all values in `category`. */
|
||||
getAll(): ODJsonDatabaseStructure {
|
||||
return this.#system.getData()
|
||||
}
|
||||
|
||||
#system = {
|
||||
/**Read parsed data from the json file */
|
||||
getData: (): ODJsonDatabaseStructure => {
|
||||
if (fs.existsSync(this.file)){
|
||||
return JSON.parse(fs.readFileSync(this.file).toString())
|
||||
}else{
|
||||
fs.writeFileSync(this.file,"[]")
|
||||
return []
|
||||
}
|
||||
},
|
||||
/**Write parsed data to the json file */
|
||||
setData: (data:ODJsonDatabaseStructure) => {
|
||||
fs.writeFileSync(this.file,this.formatter.stringify(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
///////////////////////////////////////
|
||||
//DEFAULTS MODULE
|
||||
///////////////////////////////////////
|
||||
|
||||
/**## ODDefaults `interface`
|
||||
* This type is a list of all defaults available in the `ODDefaultsManager` class.
|
||||
* It's used to generate typescript declarations for this class.
|
||||
*/
|
||||
export interface ODDefaults {
|
||||
/**Enable the default error handling system. */
|
||||
errorHandling:boolean,
|
||||
/**Crash when there is an unknown bot error. */
|
||||
crashOnError:boolean,
|
||||
/**Enable the system responsible for the `--debug` flag. */
|
||||
debugLoading:boolean,
|
||||
/**Enable loading all Open Ticket plugins, sadly enough is only useful for the system :) */
|
||||
pluginLoading:boolean,
|
||||
/**Don't crash the bot when a plugin crashes! */
|
||||
softPluginLoading:boolean,
|
||||
|
||||
/**Load the default open ticket plugin classes. */
|
||||
pluginClassLoading:boolean,
|
||||
/**Load the default open ticket plugin events. */
|
||||
pluginEventLoading:boolean,
|
||||
|
||||
/**Load the default open ticket flags. */
|
||||
flagLoading:boolean,
|
||||
/**Enable the default initializer for open ticket flags. */
|
||||
flagInitiating:boolean,
|
||||
/**Load the default open ticket configs. */
|
||||
configLoading:boolean,
|
||||
/**Load the default open ticket databases. */
|
||||
databaseLoading:boolean,
|
||||
/**Load the default open ticket sessions. */
|
||||
sessionLoading:boolean,
|
||||
|
||||
/**Load the default open ticket languages. */
|
||||
languageLoading:boolean,
|
||||
/**Enable selecting the current language from `config/general.json`. */
|
||||
languageSelection:boolean,
|
||||
/**Set the backup language when the primary language is missing a property. */
|
||||
backupLanguage:string,
|
||||
/****[NOD FOR PLUGIN TRANSLATIONS]** The full list of available languages (used in the default config checker). */
|
||||
languageList:string[],
|
||||
|
||||
/**Load the default open ticket config checker. */
|
||||
checkerLoading:boolean,
|
||||
/**Load the default open ticket config checker functions. */
|
||||
checkerFunctionLoading:boolean,
|
||||
/**Enable the default execution of the config checkers. */
|
||||
checkerExecution:boolean,
|
||||
/**Load the default open ticket config checker translations. */
|
||||
checkerTranslationLoading:boolean,
|
||||
/**Enable the default rendering of the config checkers. */
|
||||
checkerRendering:boolean,
|
||||
/**Enable the default quit action when there is an error in the config checker. */
|
||||
checkerQuit:boolean,
|
||||
/**Render the checker even when there are no errors & warnings. */
|
||||
checkerRenderEmpty:boolean,
|
||||
|
||||
/**Load the default open ticket client configuration. */
|
||||
clientLoading:boolean,
|
||||
/**Load the default open ticket client initialization. */
|
||||
clientInitiating:boolean,
|
||||
/**Load the default open ticket client ready actions (status, commands, permissions, ...). */
|
||||
clientReady:boolean,
|
||||
/**Create a warning when the bot is present in multiple guilds. */
|
||||
clientMultiGuildWarning:boolean,
|
||||
/**Load the default open ticket client activity (from `config/general.json`). */
|
||||
clientActivityLoading:boolean,
|
||||
/**Load the default open ticket client activity initialization (& status refresh). */
|
||||
clientActivityInitiating:boolean,
|
||||
|
||||
/**Load the default open ticket slash commands. */
|
||||
slashCommandLoading:boolean,
|
||||
/**Load the default open ticket slash command registerer (register slash cmds in discord). */
|
||||
slashCommandRegistering:boolean,
|
||||
/**When enabled, the bot is forced to re-register all slash commands in the server. This can be used in case of a auto-update malfunction. */
|
||||
forceSlashCommandRegistration:boolean,
|
||||
/**Load the default open ticket text commands. */
|
||||
textCommandLoading:boolean,
|
||||
|
||||
/**Load the default open ticket questions (from `config/questions.json`) */
|
||||
questionLoading:boolean,
|
||||
/**Load the default open ticket options (from `config/options.json`) */
|
||||
optionLoading:boolean,
|
||||
/**Load the default open ticket panels (from `config/panels.json`) */
|
||||
panelLoading:boolean,
|
||||
/**Load the default open ticket tickets (from `database/tickets.json`) */
|
||||
ticketLoading:boolean,
|
||||
/**Load the default open ticket reaction roles (from `config/options.json`) */
|
||||
roleLoading:boolean,
|
||||
/**Load the default open ticket blacklist (from `database/users.json`) */
|
||||
blacklistLoading:boolean,
|
||||
/**Load the default open ticket transcript compilers. */
|
||||
transcriptCompilerLoading:boolean,
|
||||
/**Load the default open ticket transcript history (from `database/transcripts.json`) */
|
||||
transcriptHistoryLoading:boolean,
|
||||
|
||||
/**Load the default open ticket button builders. */
|
||||
buttonBuildersLoading:boolean,
|
||||
/**Load the default open ticket dropdown builders. */
|
||||
dropdownBuildersLoading:boolean,
|
||||
/**Load the default open ticket file builders. */
|
||||
fileBuildersLoading:boolean,
|
||||
/**Load the default open ticket embed builders. */
|
||||
embedBuildersLoading:boolean,
|
||||
/**Load the default open ticket message builders. */
|
||||
messageBuildersLoading:boolean,
|
||||
/**Load the default open ticket modal builders. */
|
||||
modalBuildersLoading:boolean,
|
||||
|
||||
/**Load the default open ticket command responders. */
|
||||
commandRespondersLoading:boolean,
|
||||
/**Load the default open ticket button responders. */
|
||||
buttonRespondersLoading:boolean,
|
||||
/**Load the default open ticket dropdown responders. */
|
||||
dropdownRespondersLoading:boolean,
|
||||
/**Load the default open ticket modal responders. */
|
||||
modalRespondersLoading:boolean,
|
||||
/**Set the time (in ms) before open ticket sends an error message when no reply is sent in a responder. */
|
||||
responderTimeoutMs:number,
|
||||
|
||||
/**Load the default open ticket actions. */
|
||||
actionsLoading:boolean,
|
||||
|
||||
/**Load the default open ticket verify bars. */
|
||||
verifyBarsLoading:boolean,
|
||||
/**Load the default open ticket permissions. */
|
||||
permissionsLoading:boolean,
|
||||
/**Load the default open ticket posts. */
|
||||
postsLoading:boolean,
|
||||
/**Initiate the default open ticket posts. */
|
||||
postsInitiating:boolean,
|
||||
/**Load the default open ticket cooldowns. */
|
||||
cooldownsLoading:boolean,
|
||||
/**Initiate the default open ticket cooldowns. */
|
||||
cooldownsInitiating:boolean,
|
||||
/**Load the default open ticket help menu categories. */
|
||||
helpMenuCategoryLoading:boolean,
|
||||
/**Load the default open ticket help menu components. */
|
||||
helpMenuComponentLoading:boolean,
|
||||
|
||||
/**Load the default open ticket stat scopes. */
|
||||
statScopesLoading:boolean,
|
||||
/**Load the default open ticket stats. */
|
||||
statLoading:boolean,
|
||||
/**Initiate the default open ticket stats. */
|
||||
statInitiating:boolean,
|
||||
|
||||
/**Load the default open ticket code/functions. */
|
||||
codeLoading:boolean,
|
||||
/**Execute the default open ticket code/functions. */
|
||||
codeExecution:boolean,
|
||||
|
||||
/**Load the default open ticket livestatus. */
|
||||
liveStatusLoading:boolean,
|
||||
/**Load the default open ticket startscreen. */
|
||||
startScreenLoading:boolean,
|
||||
/**Render the default open ticket startscreen. */
|
||||
startScreenRendering:boolean,
|
||||
|
||||
/**The emoji style to use in embed & message titles using `utilities.emoijTitle()` */
|
||||
emojiTitleStyle:"disabled"|"before"|"after"|"double",
|
||||
/**The emoji divider to use in embed & message titles using `utilities.emoijTitle()` */
|
||||
emojiTitleDivider:string
|
||||
/**The interval in milliseconds that are between autoclose timeout checkers. */
|
||||
autocloseCheckInterval:number
|
||||
/**The interval in milliseconds that are between autodelete timeout checkers. */
|
||||
autodeleteCheckInterval:number
|
||||
}
|
||||
|
||||
/**## ODDefaultsBooleans `type`
|
||||
* This type is a list of boolean defaults available in the `ODDefaultsManager` class.
|
||||
* It's used to generate typescript declarations for this class.
|
||||
*/
|
||||
export type ODDefaultsBooleans = {
|
||||
[Key in keyof ODDefaults]: ODDefaults[Key] extends boolean ? Key : never
|
||||
}[keyof ODDefaults]
|
||||
|
||||
/**## ODDefaultsStrings `type`
|
||||
* This type is a list of string defaults available in the `ODDefaultsManager` class.
|
||||
* It's used to generate typescript declarations for this class.
|
||||
*/
|
||||
export type ODDefaultsStrings = {
|
||||
[Key in keyof ODDefaults]: ODDefaults[Key] extends string ? Key : never
|
||||
}[keyof ODDefaults]
|
||||
|
||||
/**## ODDefaultsNumbers `type`
|
||||
* This type is a list of number defaults available in the `ODDefaultsManager` class.
|
||||
* It's used to generate typescript declarations for this class.
|
||||
*/
|
||||
export type ODDefaultsNumbers = {
|
||||
[Key in keyof ODDefaults]: ODDefaults[Key] extends number ? Key : never
|
||||
}[keyof ODDefaults]
|
||||
|
||||
/**## ODDefaultsStringArray `type`
|
||||
* This type is a list of string[] defaults available in the `ODDefaultsManager` class.
|
||||
* It's used to generate typescript declarations for this class.
|
||||
*/
|
||||
export type ODDefaultsStringArray = {
|
||||
[Key in keyof ODDefaults]: ODDefaults[Key] extends string[] ? Key : never
|
||||
}[keyof ODDefaults]
|
||||
|
||||
/**## ODDefaultsManager `class`
|
||||
* This is an open ticket defaults manager.
|
||||
*
|
||||
* It manages all settings in open ticket that are not meant to be in the config.
|
||||
* Here you can disable certain default features to replace them or to specifically enable them!
|
||||
*
|
||||
* You are unable to add your own defaults, you can only edit Open Ticket defaults!
|
||||
*/
|
||||
export class ODDefaultsManager {
|
||||
/**A list of all the defaults */
|
||||
#defaults: ODDefaults
|
||||
|
||||
constructor(){
|
||||
this.#defaults = {
|
||||
errorHandling:true,
|
||||
crashOnError:false,
|
||||
debugLoading:true,
|
||||
pluginLoading:true,
|
||||
softPluginLoading:false,
|
||||
|
||||
pluginClassLoading:true,
|
||||
pluginEventLoading:true,
|
||||
|
||||
flagLoading:true,
|
||||
flagInitiating:true,
|
||||
configLoading:true,
|
||||
databaseLoading:true,
|
||||
sessionLoading:true,
|
||||
|
||||
languageLoading:true,
|
||||
languageSelection:true,
|
||||
backupLanguage:"openticket:english",
|
||||
languageList:["custom","english","dutch","portuguese","czech"],
|
||||
|
||||
checkerLoading:true,
|
||||
checkerFunctionLoading:true,
|
||||
checkerExecution:true,
|
||||
checkerTranslationLoading:true,
|
||||
checkerRendering:true,
|
||||
checkerQuit:true,
|
||||
checkerRenderEmpty:false,
|
||||
|
||||
clientLoading:true,
|
||||
clientInitiating:true,
|
||||
clientReady:true,
|
||||
clientMultiGuildWarning:true,
|
||||
clientActivityLoading:true,
|
||||
clientActivityInitiating:true,
|
||||
|
||||
slashCommandLoading:true,
|
||||
slashCommandRegistering:true,
|
||||
forceSlashCommandRegistration:false,
|
||||
textCommandLoading:true,
|
||||
|
||||
questionLoading:true,
|
||||
optionLoading:true,
|
||||
panelLoading:true,
|
||||
ticketLoading:true,
|
||||
roleLoading:true,
|
||||
blacklistLoading:true,
|
||||
transcriptCompilerLoading:true,
|
||||
transcriptHistoryLoading:true,
|
||||
|
||||
buttonBuildersLoading:true,
|
||||
dropdownBuildersLoading:true,
|
||||
fileBuildersLoading:true,
|
||||
embedBuildersLoading:true,
|
||||
messageBuildersLoading:true,
|
||||
modalBuildersLoading:true,
|
||||
|
||||
commandRespondersLoading:true,
|
||||
buttonRespondersLoading:true,
|
||||
dropdownRespondersLoading:true,
|
||||
modalRespondersLoading:true,
|
||||
responderTimeoutMs:2500,
|
||||
|
||||
actionsLoading:true,
|
||||
|
||||
verifyBarsLoading:true,
|
||||
permissionsLoading:true,
|
||||
postsLoading:true,
|
||||
postsInitiating:true,
|
||||
cooldownsLoading:true,
|
||||
cooldownsInitiating:true,
|
||||
helpMenuCategoryLoading:true,
|
||||
helpMenuComponentLoading:true,
|
||||
|
||||
statScopesLoading:true,
|
||||
statLoading:true,
|
||||
statInitiating:true,
|
||||
|
||||
codeLoading:true,
|
||||
codeExecution:true,
|
||||
|
||||
liveStatusLoading:true,
|
||||
startScreenLoading:true,
|
||||
startScreenRendering:true,
|
||||
|
||||
emojiTitleStyle:"before",
|
||||
emojiTitleDivider:" ",
|
||||
autocloseCheckInterval:300000, //5 minutes
|
||||
autodeleteCheckInterval:300000 //5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
/**Set a default to a specific value. Remember! All plugins can edit these values, so your value could be overwritten! */
|
||||
setDefault<DefaultName extends keyof ODDefaults>(key:DefaultName, value:ODDefaults[DefaultName]): void {
|
||||
this.#defaults[key] = value
|
||||
}
|
||||
|
||||
/**Get a default. Remember! All plugins can edit these values, so this value could be overwritten! */
|
||||
getDefault<DefaultName extends keyof ODDefaults>(key:DefaultName): ODDefaults[DefaultName] {
|
||||
return this.#defaults[key]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
///////////////////////////////////////
|
||||
//EVENT MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODManagerData, ODManager, ODValidId } from "./base"
|
||||
import { ODConsoleWarningMessage, ODDebugger } from "./console"
|
||||
|
||||
/**## ODEvent `class`
|
||||
* This is an open ticket event.
|
||||
*
|
||||
* This class is made to work with the `ODEventManager` to handle events.
|
||||
* The function of this specific class is to manage all listeners for a specifc event!
|
||||
*/
|
||||
export class ODEvent extends ODManagerData {
|
||||
/**Alias to open ticket debugger. */
|
||||
#debug?: ODDebugger
|
||||
/**The list of permanent listeners. */
|
||||
listeners: Function[] = []
|
||||
/**The list of one-time listeners. List is cleared every time the event is emitted. */
|
||||
oncelisteners: Function[] = []
|
||||
/**The max listener limit before a possible memory leak will be announced */
|
||||
listenerLimit: number = 25
|
||||
|
||||
/**Use the open ticket debugger in this manager for logs*/
|
||||
useDebug(debug:ODDebugger|null){
|
||||
this.#debug = debug ?? undefined
|
||||
}
|
||||
/**Get a collection of listeners combined from both types. Also clears the one-time listeners array! */
|
||||
#getCurrentListeners(){
|
||||
const final: Function[] = []
|
||||
this.oncelisteners.forEach((l) => final.push(l))
|
||||
this.listeners.forEach((l) => final.push(l))
|
||||
|
||||
this.oncelisteners = []
|
||||
return final
|
||||
}
|
||||
/**Edit the listener limit */
|
||||
setListenerLimit(limit:number){
|
||||
this.listenerLimit = limit
|
||||
}
|
||||
/**Add a permanent callback to this event. This will stay as long as the bot is running! */
|
||||
listen(callback:Function){
|
||||
this.listeners.push(callback)
|
||||
|
||||
if (this.listeners.length > this.listenerLimit){
|
||||
if (this.#debug) this.#debug.console.log(new ODConsoleWarningMessage("Possible event memory leak detected!",[
|
||||
{key:"event",value:this.id.value},
|
||||
{key:"listeners",value:this.listeners.length.toString()}
|
||||
]))
|
||||
}
|
||||
}
|
||||
/**Add a one-time-only callback to this event. This will only trigger the callback once! */
|
||||
listenOnce(callback:Function){
|
||||
this.oncelisteners.push(callback)
|
||||
}
|
||||
/**Wait until this event is fired! Be carefull with it, because it could block the entire bot when wrongly used! */
|
||||
async wait(): Promise<any[]> {
|
||||
return new Promise((resolve,reject) => {
|
||||
this.oncelisteners.push((...args:any) => {resolve(args)})
|
||||
})
|
||||
}
|
||||
/**Emit this event to all listeners. You are required to provide all parameters of the event! */
|
||||
async emit(params:any[]): Promise<void> {
|
||||
for (const listener of this.#getCurrentListeners()){
|
||||
try{
|
||||
await listener(...params)
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODEventManager `class`
|
||||
* This is an open ticket event manager.
|
||||
*
|
||||
* This class is made to manage all events in the bot. You can compare it with the built-in node.js `EventEmitter`
|
||||
*
|
||||
* You will probably not create this class yourself, but use it globaly instead!
|
||||
* Check out the `openticket.events` class!
|
||||
*/
|
||||
export class ODEventManager extends ODManager<ODEvent> {
|
||||
/**Reference to the Open Ticket debugger */
|
||||
#debug: ODDebugger
|
||||
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"event")
|
||||
this.#debug = debug
|
||||
}
|
||||
|
||||
add(data:ODEvent, overwrite?:boolean): boolean {
|
||||
data.useDebug(this.#debug)
|
||||
return super.add(data,overwrite)
|
||||
}
|
||||
remove(id:ODValidId): ODEvent|null {
|
||||
const data = super.remove(id)
|
||||
if (data) data.useDebug(null)
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
///////////////////////////////////////
|
||||
//FLAG MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODValidId, ODManager, ODManagerData } from "./base"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
/**## ODFlag `class`
|
||||
* This is an open ticket flag.
|
||||
*
|
||||
* A flag is a boolean that can be specified by a parameter in the console.
|
||||
* It's useful for small settings that are only required once in a while.
|
||||
*
|
||||
* Flags can also be enabled manually by plugins!
|
||||
*/
|
||||
export class ODFlag extends ODManagerData {
|
||||
/**The method that has been used to set the value of this flag. (`null` when not set) */
|
||||
method: "param"|"manual"|null = null
|
||||
/**The name of this flag. Visible to the user. */
|
||||
name: string
|
||||
/**The description of this flag. Visible to the user. */
|
||||
description: string
|
||||
/**The name of the parameter in the console. (e.g. `--test`) */
|
||||
param: string
|
||||
/**A list of aliases for the parameter in the console. */
|
||||
aliases: string[]
|
||||
/**The value of this flag. */
|
||||
value: boolean = false
|
||||
|
||||
constructor(id:ODValidId, name:string, description:string, param:string, aliases?:string[], initialValue?:boolean){
|
||||
super(id)
|
||||
this.name = name
|
||||
this.description = description
|
||||
this.param = param
|
||||
this.aliases = aliases ?? []
|
||||
this.value = initialValue ?? false
|
||||
}
|
||||
|
||||
/**Set the value of this flag. */
|
||||
setValue(value:boolean,method?:"param"|"manual"){
|
||||
this.value = value
|
||||
this.method = method ?? "manual"
|
||||
}
|
||||
/**Detect if the process contains the param or aliases & set the value. Use `force` to overwrite a manually set value. */
|
||||
detectProcessParams(force?:boolean){
|
||||
if (force){
|
||||
const params = [this.param,...this.aliases]
|
||||
this.setValue(params.some((p) => process.argv.includes(p)),"param")
|
||||
|
||||
}else if (this.method != "manual"){
|
||||
const params = [this.param,...this.aliases]
|
||||
this.setValue(params.some((p) => process.argv.includes(p)),"param")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODFlagManager `class`
|
||||
* This is an open ticket flag manager.
|
||||
*
|
||||
* This class is responsible for managing & initiating all flags of the bot.
|
||||
* It also contains a shortcut for initiating all flags.
|
||||
*/
|
||||
export class ODFlagManager extends ODManager<ODFlag> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"flag")
|
||||
}
|
||||
|
||||
/**Set all flags to their `process.argv` value. */
|
||||
init(){
|
||||
this.getAll().forEach((flag) => {
|
||||
flag.detectProcessParams(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
///////////////////////////////////////
|
||||
//HELP MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
/**## ODHelpMenuComponentRenderer `type`
|
||||
* This is the callback of the help menu component renderer. It also contains information about how & where it is rendered.
|
||||
*/
|
||||
export type ODHelpMenuComponentRenderer = (page:number, category:number, location:number, mode:"slash"|"text") => string|Promise<string>
|
||||
|
||||
/**## ODHelpMenuComponent `class`
|
||||
* This is an open ticket help menu component.
|
||||
*
|
||||
* It can render something on the Open Ticket help menu.
|
||||
*/
|
||||
export class ODHelpMenuComponent extends ODManagerData {
|
||||
/**The priority of this component. The higher, the earlier it will appear in the help menu. */
|
||||
priority: number
|
||||
/**The render function for this component. */
|
||||
render: ODHelpMenuComponentRenderer
|
||||
|
||||
constructor(id:ODValidId, priority:number, render:ODHelpMenuComponentRenderer){
|
||||
super(id)
|
||||
this.priority = priority
|
||||
this.render = render
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODHelpMenuTextComponent `class`
|
||||
* This is an open ticket help menu text component.
|
||||
*
|
||||
* It can render a static piece of text on the Open Ticket help menu.
|
||||
*/
|
||||
export class ODHelpMenuTextComponent extends ODHelpMenuComponent {
|
||||
constructor(id:ODValidId, priority:number, text:string){
|
||||
super(id,priority,() => {
|
||||
return text
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODHelpMenuCommandComponentOption `interface`
|
||||
* This interface contains a command option for the `ODHelpMenuCommandComponent`.
|
||||
*/
|
||||
export interface ODHelpMenuCommandComponentOption {
|
||||
/**The name of this option. */
|
||||
name:string,
|
||||
/**Is this option optional? */
|
||||
optional:boolean
|
||||
}
|
||||
|
||||
/**## ODHelpMenuCommandComponentSettings `interface`
|
||||
* This interface contains the settings for the `ODHelpMenuCommandComponent`.
|
||||
*/
|
||||
export interface ODHelpMenuCommandComponentSettings {
|
||||
/**The name of this text command. */
|
||||
textName?:string,
|
||||
/**The name of this slash command. */
|
||||
slashName?:string,
|
||||
/**Options available in the text command. */
|
||||
textOptions?:ODHelpMenuCommandComponentOption[],
|
||||
/**Options available in the slash command. */
|
||||
slashOptions?:ODHelpMenuCommandComponentOption[],
|
||||
/**The description for the text command. */
|
||||
textDescription?:string,
|
||||
/**The description for the slash command. */
|
||||
slashDescription?:string
|
||||
}
|
||||
|
||||
/**## ODHelpMenuCommandComponent `class`
|
||||
* This is an open ticket help menu command component.
|
||||
*
|
||||
* It contains a useful helper to render a command in the Open Ticket help menu.
|
||||
*/
|
||||
export class ODHelpMenuCommandComponent extends ODHelpMenuComponent {
|
||||
constructor(id:ODValidId, priority:number, settings:ODHelpMenuCommandComponentSettings){
|
||||
super(id,priority,(page,category,location,mode) => {
|
||||
if (mode == "slash" && settings.slashName){
|
||||
return `\`${settings.slashName}${(settings.slashOptions) ? this.#renderOptions(settings.slashOptions) : ""}\` ➜ ${settings.slashDescription ?? ""}`
|
||||
|
||||
}else if (mode == "text" && settings.textName){
|
||||
return `\`${settings.textName}${(settings.textOptions) ? this.#renderOptions(settings.textOptions) : ""}\` ➜ ${settings.textDescription ?? ""}`
|
||||
|
||||
}else return ""
|
||||
})
|
||||
}
|
||||
|
||||
/**Utility function to render all command options. */
|
||||
#renderOptions(options:ODHelpMenuCommandComponentOption[]){
|
||||
return " "+options.map((opt) => (opt.optional) ? `[${opt.name}]` : `<${opt.name}>`).join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODHelpMenuCategory `class`
|
||||
* This is an open ticket help menu category.
|
||||
*
|
||||
* Every category in the help menu is an embed field by default.
|
||||
* Try to limit the amount of components per category.
|
||||
*/
|
||||
export class ODHelpMenuCategory extends ODManager<ODHelpMenuComponent> {
|
||||
/**The id of this category. */
|
||||
id: ODId
|
||||
/**The priority of this category. The higher, the earlier it will appear in the menu. */
|
||||
priority: number
|
||||
/**The name of this category. (can include emoji's) */
|
||||
name: string
|
||||
/**When enabled, it automatically starts this category on a new page. */
|
||||
newPage: boolean
|
||||
|
||||
constructor(id:ODValidId, priority:number, name:string, newPage?:boolean){
|
||||
super()
|
||||
this.id = new ODId(id)
|
||||
this.priority = priority
|
||||
this.name = name
|
||||
this.newPage = newPage ?? false
|
||||
}
|
||||
|
||||
/**Render this category and it's components. */
|
||||
async render(page:number, category:number, mode:"slash"|"text"){
|
||||
//sort from high priority to low
|
||||
const derefArray = [...this.getAll()]
|
||||
derefArray.sort((a,b) => {
|
||||
return b.priority-a.priority
|
||||
})
|
||||
const result: string[] = []
|
||||
|
||||
let i = 0
|
||||
for (const component of derefArray){
|
||||
try {
|
||||
result.push(await component.render(page,category,i,mode))
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
//only return the non-empty components
|
||||
return result.filter((component) => component !== "").join("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODHelpMenuRenderResult `type`
|
||||
* This is the array returned when the help menu has been rendered succesfully.
|
||||
*
|
||||
* It contains a list of pages, which contain categories by name & value (content).
|
||||
*/
|
||||
export type ODHelpMenuRenderResult = {name:string, value:string}[][]
|
||||
|
||||
/**## ODHelpMenuManager `class`
|
||||
* This is an open ticket help menu manager.
|
||||
*
|
||||
* It is responsible for rendering the entire help menu content.
|
||||
* You are also able to configure the amount of categories per page here.
|
||||
*
|
||||
* Fewer Categories == More Clean Menu
|
||||
*/
|
||||
export class ODHelpMenuManager extends ODManager<ODHelpMenuCategory> {
|
||||
/**Alias to open ticket debugger. */
|
||||
#debug: ODDebugger
|
||||
/**The amount of categories per-page. */
|
||||
categoriesPerPage: number = 3
|
||||
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"help menu category")
|
||||
this.#debug = debug
|
||||
}
|
||||
|
||||
add(data:ODHelpMenuCategory, overwrite?:boolean): boolean {
|
||||
data.useDebug(this.#debug,"help menu component")
|
||||
return super.add(data,overwrite)
|
||||
}
|
||||
|
||||
/**Render this entire help menu & return a `ODHelpMenuRenderResult`. */
|
||||
async render(mode:"slash"|"text"): Promise<ODHelpMenuRenderResult> {
|
||||
//sort from high priority to low
|
||||
const derefArray = [...this.getAll()]
|
||||
derefArray.sort((a,b) => {
|
||||
return b.priority-a.priority
|
||||
})
|
||||
const result: {name:string, value:string}[][] = []
|
||||
let currentPage: {name:string, value:string}[] = []
|
||||
|
||||
for (const category of derefArray){
|
||||
try {
|
||||
const renderedCategory = await category.render(result.length,currentPage.length,mode)
|
||||
|
||||
if (renderedCategory !== ""){
|
||||
//create new page when category wants to
|
||||
if (currentPage.length > 0 && category.newPage){
|
||||
result.push(currentPage)
|
||||
currentPage = []
|
||||
}
|
||||
|
||||
currentPage.push({
|
||||
name:category.name,
|
||||
value:renderedCategory
|
||||
})
|
||||
|
||||
//create new page when page is full
|
||||
if (currentPage.length >= this.categoriesPerPage){
|
||||
result.push(currentPage)
|
||||
currentPage = []
|
||||
}
|
||||
}
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
|
||||
//push current page when not-empty
|
||||
if (currentPage.length > 0) result.push(currentPage)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
///////////////////////////////////////
|
||||
//LANGUAGE MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base"
|
||||
import nodepath from "path"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
export class ODLanguage extends ODManagerData {
|
||||
file: string
|
||||
data: any
|
||||
metadata: {
|
||||
otversion:string,
|
||||
language:string,
|
||||
translator:string,
|
||||
lastedited:string
|
||||
}|null = null
|
||||
|
||||
constructor(id:ODValidId, file:string, customPath?:string){
|
||||
super(id)
|
||||
this.file = file
|
||||
try{
|
||||
const filename = (file.endsWith(".json")) ? file : file+".json"
|
||||
this.data = customPath ? require(nodepath.join("../../../../",customPath,filename)) : require(nodepath.join("../../../../languages",filename))
|
||||
}catch{
|
||||
throw new ODSystemError("[API ERROR] Language \""+file+"\" doesn't exist!")
|
||||
}
|
||||
if (this.data["_TRANSLATION"]) this.metadata = this.data["_TRANSLATION"]
|
||||
}
|
||||
}
|
||||
|
||||
export class ODLanguageManager extends ODManager<ODLanguage> {
|
||||
current: ODLanguage|null = null
|
||||
backup: ODLanguage|null = null
|
||||
|
||||
constructor(debug:ODDebugger, presets:boolean){
|
||||
super(debug,"language")
|
||||
if (presets) this.add(new ODLanguage("english","english.json"))
|
||||
this.current = presets ? new ODLanguage("english","english.json") : null
|
||||
this.backup = presets ? new ODLanguage("english","english.json") : null
|
||||
}
|
||||
|
||||
setCurrentLanguage(id:ODValidId){
|
||||
this.current = this.get(id)
|
||||
}
|
||||
getCurrentLanguage(){
|
||||
return (this.current) ? this.current : null
|
||||
}
|
||||
setBackupLanguage(id:ODValidId){
|
||||
this.backup = this.get(id)
|
||||
}
|
||||
getBackupLanguage(){
|
||||
return (this.backup) ? this.backup : null
|
||||
}
|
||||
getLanguageMetadata(frombackup?:boolean){
|
||||
if (frombackup) return (this.backup) ? this.backup.metadata : null
|
||||
return (this.current) ? this.current.metadata : null
|
||||
}
|
||||
getCurrentLanguageId(){
|
||||
return (this.current) ? this.current.id.value : ""
|
||||
}
|
||||
getTranslation(id:string): string|null {
|
||||
if (!this.current) return this.#getBackupTranslation(id)
|
||||
|
||||
const splitted = id.split(".")
|
||||
let currentObject = this.current.data
|
||||
let result: string|false = false
|
||||
splitted.forEach((id) => {
|
||||
if (typeof currentObject[id] == "object"){
|
||||
currentObject = currentObject[id]
|
||||
}else if (typeof currentObject[id] == "string"){
|
||||
result = currentObject[id]
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof result == "string") return result
|
||||
else return this.#getBackupTranslation(id)
|
||||
}
|
||||
|
||||
#getBackupTranslation(id:string): string|null {
|
||||
if (!this.backup) return null
|
||||
|
||||
const splitted = id.split(".")
|
||||
let currentObject = this.backup.data
|
||||
let result: string|false = false
|
||||
splitted.forEach((id) => {
|
||||
if (typeof currentObject[id] == "object"){
|
||||
currentObject = currentObject[id]
|
||||
}else if (typeof currentObject[id] == "string"){
|
||||
result = currentObject[id]
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof result == "string") return result
|
||||
else return null
|
||||
}
|
||||
|
||||
getTranslationWithParams(id:string, params:string[]): string|null {
|
||||
let translation = this.getTranslation(id)
|
||||
if (!translation) return translation
|
||||
|
||||
params.forEach((value,index) => {
|
||||
if (!translation) return
|
||||
translation = translation.replace(`{${index}}`,value)
|
||||
})
|
||||
return translation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
///////////////////////////////////////
|
||||
//PERMISSION MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODValidId, ODManager, ODSystemError, ODManagerData } from "./base"
|
||||
import * as discord from "discord.js"
|
||||
import { ODDebugger } from "./console"
|
||||
|
||||
export type ODPermissionType = "member"|"support"|"moderator"|"admin"|"owner"|"developer"
|
||||
export type ODPermissionScope = "global-user"|"channel-user"|"global-role"|"channel-role"
|
||||
export interface ODPermissionResult {
|
||||
type:ODPermissionType
|
||||
scope:ODPermissionScope|"default"
|
||||
level:ODPermissionLevel,
|
||||
source:ODPermission|null
|
||||
}
|
||||
|
||||
export enum ODPermissionLevel {
|
||||
member,
|
||||
support,
|
||||
moderator,
|
||||
admin,
|
||||
owner,
|
||||
developer
|
||||
}
|
||||
|
||||
export class ODPermission extends ODManagerData {
|
||||
readonly scope: ODPermissionScope
|
||||
readonly permission: ODPermissionType
|
||||
readonly value: discord.Role|discord.User
|
||||
readonly channel: discord.Channel|null
|
||||
|
||||
constructor(id:ODValidId, scope:"global-user", permission:ODPermissionType, value:discord.User)
|
||||
constructor(id:ODValidId, scope:"global-role", permission:ODPermissionType, value:discord.Role)
|
||||
constructor(id:ODValidId, scope:"channel-user", permission:ODPermissionType, value:discord.User, channel:discord.Channel)
|
||||
constructor(id:ODValidId, scope:"channel-role", permission:ODPermissionType, value:discord.Role, channel:discord.Channel)
|
||||
constructor(id:ODValidId, scope:ODPermissionScope, permission:ODPermissionType, value:discord.Role|discord.User, channel?:discord.Channel){
|
||||
super(id)
|
||||
this.scope = scope
|
||||
this.permission = permission
|
||||
this.value = value
|
||||
this.channel = channel ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export interface ODPermissionSettings {
|
||||
allowGlobalUserScope?:boolean,
|
||||
allowGlobalRoleScope?:boolean,
|
||||
allowChannelUserScope?:boolean,
|
||||
allowChannelRoleScope?:boolean,
|
||||
idRegex?:RegExp
|
||||
}
|
||||
|
||||
export type ODPermissionCalculationCallback = (user:discord.User, channel?:discord.Channel|null, guild?:discord.Guild|null, settings?:ODPermissionSettings|null) => Promise<ODPermissionResult>
|
||||
|
||||
export class ODPermissionManager extends ODManager<ODPermission> {
|
||||
#calculation: ODPermissionCalculationCallback|null
|
||||
defaultResult: ODPermissionResult = {
|
||||
level:ODPermissionLevel["member"],
|
||||
scope:"default",
|
||||
type:"member",
|
||||
source:null
|
||||
}
|
||||
|
||||
constructor(debug:ODDebugger, useDefaultCalculation?:boolean){
|
||||
super(debug,"permission")
|
||||
this.#calculation = useDefaultCalculation ? this.#defaultCalculation : null
|
||||
}
|
||||
|
||||
setCalculation(calculation:ODPermissionCalculationCallback){
|
||||
this.#calculation = calculation
|
||||
}
|
||||
setDefaultResult(result:ODPermissionResult){
|
||||
this.defaultResult = result
|
||||
}
|
||||
getPermissions(user:discord.User, channel?:discord.Channel|null, guild?:discord.Guild|null, settings?:ODPermissionSettings|null): Promise<ODPermissionResult> {
|
||||
try{
|
||||
if (!this.#calculation) throw new ODSystemError("ODPermissionManager:getPermissions() => missing perms calculation")
|
||||
return this.#calculation(user,channel,guild,settings)
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
throw new ODSystemError("ODPermissionManager:getPermissions() => failed perms calculation")
|
||||
}
|
||||
}
|
||||
hasPermissions(minimum:ODPermissionType, data:ODPermissionResult){
|
||||
if (minimum == "member") return true
|
||||
else if (minimum == "support") return (data.level >= ODPermissionLevel["support"])
|
||||
else if (minimum == "moderator") return (data.level >= ODPermissionLevel["moderator"])
|
||||
else if (minimum == "admin") return (data.level >= ODPermissionLevel["admin"])
|
||||
else if (minimum == "owner") return (data.level >= ODPermissionLevel["owner"])
|
||||
else if (minimum == "developer") return (data.level >= ODPermissionLevel["developer"])
|
||||
else throw new ODSystemError("Invalid minimum permission type at ODPermissionManager.hasPermissions()")
|
||||
}
|
||||
async #defaultCalculation(user:discord.User,channel?:discord.Channel|null,guild?:discord.Guild|null, settings?:ODPermissionSettings|null): Promise<ODPermissionResult> {
|
||||
const idRegex = (settings && typeof settings.idRegex != "undefined") ? settings.idRegex : null
|
||||
const allowChannelUserScope = (settings && typeof settings.allowChannelUserScope != "undefined") ? settings.allowChannelUserScope : true
|
||||
const allowChannelRoleScope = (settings && typeof settings.allowChannelRoleScope != "undefined") ? settings.allowChannelRoleScope : true
|
||||
const allowGlobalUserScope = (settings && typeof settings.allowGlobalUserScope != "undefined") ? settings.allowGlobalUserScope : true
|
||||
const allowGlobalRoleScope = (settings && typeof settings.allowGlobalRoleScope != "undefined") ? settings.allowGlobalRoleScope : true
|
||||
|
||||
if (guild && channel && !channel.isDMBased()){
|
||||
//check for channel user permissions
|
||||
if (allowChannelUserScope){
|
||||
const users = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "channel-user" && permission.channel && (permission.channel.id == channel.id) && (permission.value instanceof discord.User) && permission.value.id == user.id)
|
||||
|
||||
if (users.length > 0){
|
||||
//sort all permisions from highest to lowest
|
||||
users.sort((a,b) => {
|
||||
const levelA = ODPermissionLevel[a.permission]
|
||||
const levelB = ODPermissionLevel[b.permission]
|
||||
|
||||
if (levelB > levelA) return 1
|
||||
else if (levelA > levelB) return -1
|
||||
else return 0
|
||||
})
|
||||
|
||||
return {
|
||||
type:users[0].permission,
|
||||
scope:"channel-user",
|
||||
level:ODPermissionLevel[users[0].permission],
|
||||
source:users[0] ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check for channel role permissions
|
||||
if (allowChannelRoleScope){
|
||||
const member = await guild.members.fetch(user.id)
|
||||
if (member){
|
||||
const memberRoles = member.roles.cache.map((role) => role.id)
|
||||
const roles = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "channel-role" && permission.channel && (permission.channel.id == channel.id) && (permission.value instanceof discord.Role) && memberRoles.includes(permission.value.id) && permission.value.guild.id == guild.id)
|
||||
|
||||
if (roles.length > 0){
|
||||
//sort all permisions from highest to lowest
|
||||
roles.sort((a,b) => {
|
||||
const levelA = ODPermissionLevel[a.permission]
|
||||
const levelB = ODPermissionLevel[b.permission]
|
||||
|
||||
if (levelB > levelA) return 1
|
||||
else if (levelA > levelB) return -1
|
||||
else return 0
|
||||
})
|
||||
|
||||
return {
|
||||
type:roles[0].permission,
|
||||
scope:"channel-role",
|
||||
level:ODPermissionLevel[roles[0].permission],
|
||||
source:roles[0] ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check for global user permissions
|
||||
if (allowGlobalUserScope){
|
||||
const users = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "global-user" && (permission.value instanceof discord.User) && permission.value.id == user.id)
|
||||
|
||||
if (users.length > 0){
|
||||
//sort all permisions from highest to lowest
|
||||
users.sort((a,b) => {
|
||||
const levelA = ODPermissionLevel[a.permission]
|
||||
const levelB = ODPermissionLevel[b.permission]
|
||||
|
||||
if (levelB > levelA) return 1
|
||||
else if (levelA > levelB) return -1
|
||||
else return 0
|
||||
})
|
||||
|
||||
return {
|
||||
type:users[0].permission,
|
||||
scope:"global-user",
|
||||
level:ODPermissionLevel[users[0].permission],
|
||||
source:users[0] ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check for global role permissions
|
||||
if (allowGlobalRoleScope){
|
||||
if (guild){
|
||||
const member = await guild.members.fetch(user.id)
|
||||
if (member){
|
||||
const memberRoles = member.roles.cache.map((role) => role.id)
|
||||
const roles = this.getFiltered((permission) => (!idRegex || (idRegex && idRegex.test(permission.id.value))) && permission.scope == "global-role" && (permission.value instanceof discord.Role) && memberRoles.includes(permission.value.id) && permission.value.guild.id == guild.id)
|
||||
|
||||
if (roles.length > 0){
|
||||
//sort all permisions from highest to lowest
|
||||
roles.sort((a,b) => {
|
||||
const levelA = ODPermissionLevel[a.permission]
|
||||
const levelB = ODPermissionLevel[b.permission]
|
||||
|
||||
if (levelB > levelA) return 1
|
||||
else if (levelA > levelB) return -1
|
||||
else return 0
|
||||
})
|
||||
|
||||
return {
|
||||
type:roles[0].permission,
|
||||
scope:"global-role",
|
||||
level:ODPermissionLevel[roles[0].permission],
|
||||
source:roles[0] ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//spread result to prevent accidental referencing
|
||||
return {...this.defaultResult}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
///////////////////////////////////////
|
||||
//PLUGIN MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId, ODVersion } from "./base"
|
||||
import nodepath from "path"
|
||||
import { ODConsolePluginMessage, ODConsoleWarningMessage, ODDebugger } from "./console"
|
||||
|
||||
export interface ODUnknownCrashedPlugin {
|
||||
name:string,
|
||||
description:string
|
||||
}
|
||||
|
||||
export class ODPluginManager extends ODManager<ODPlugin> {
|
||||
events: ODPluginEventManager
|
||||
classes: ODPluginClassManager
|
||||
unknownCrashedPlugins: ODUnknownCrashedPlugin[] = []
|
||||
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"plugin")
|
||||
this.events = new ODPluginEventManager(debug)
|
||||
this.classes = new ODPluginClassManager(debug)
|
||||
}
|
||||
|
||||
/**Check if a plugin has loaded successfully.*/
|
||||
isPluginLoaded(id:ODValidId){
|
||||
const newId = new ODId(id)
|
||||
const plugin = this.get(newId)
|
||||
return (plugin && plugin.executed)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ODPluginData {
|
||||
name:string,
|
||||
id:string,
|
||||
version:string,
|
||||
startFile:string,
|
||||
|
||||
enabled:boolean,
|
||||
priority:number,
|
||||
events:string[]
|
||||
|
||||
npmDependencies:string[],
|
||||
requiredPlugins:string[],
|
||||
incompatiblePlugins:string[],
|
||||
|
||||
details:ODPluginDetails
|
||||
}
|
||||
|
||||
export interface ODPluginDetails {
|
||||
author:string,
|
||||
shortDescription:string,
|
||||
longDescription:string,
|
||||
imageUrl:string,
|
||||
projectUrl:string,
|
||||
tags:string[]
|
||||
}
|
||||
|
||||
export class ODPlugin extends ODManagerData {
|
||||
dir: string
|
||||
data: ODPluginData
|
||||
name: string
|
||||
priority: number
|
||||
version: ODVersion
|
||||
details: ODPluginDetails
|
||||
|
||||
enabled: boolean
|
||||
executed: boolean
|
||||
crashed: boolean
|
||||
crashReason: null|"incompatible.plugin"|"missing.plugin"|"missing.dependency"|"executed" = null
|
||||
|
||||
constructor(dir:string, jsondata:ODPluginData){
|
||||
super(jsondata.id)
|
||||
this.dir = dir
|
||||
this.data = jsondata
|
||||
this.name = jsondata.name
|
||||
this.priority = jsondata.priority
|
||||
this.version = ODVersion.fromString("plugin",jsondata.version)
|
||||
this.details = jsondata.details
|
||||
|
||||
this.enabled = jsondata.enabled
|
||||
this.executed = false
|
||||
this.crashed = false
|
||||
}
|
||||
|
||||
//Get the startfile location relative to the ./plugins/ directory
|
||||
getStartFile(){
|
||||
return nodepath.join(this.dir,this.data.startFile)
|
||||
}
|
||||
/**Execute this plugin. Returns `false` on crash. */
|
||||
async execute(debug:ODDebugger,force?:boolean): Promise<boolean> {
|
||||
if ((this.enabled && !this.crashed) || force){
|
||||
try{
|
||||
await require(nodepath.join("../../../../plugins/",this.getStartFile()))
|
||||
debug.console.log("Plugin \""+this.id.value+"\" loaded successfully!","plugin")
|
||||
this.executed = true
|
||||
return true
|
||||
}catch(error){
|
||||
this.crashed = true
|
||||
this.crashReason = "executed"
|
||||
|
||||
debug.console.log(error.message+", canceling plugin execution...","plugin",[
|
||||
{key:"path",value:"./plugins/"+this.dir}
|
||||
])
|
||||
debug.console.log("You can see more about this error in the ./otdebug.txt file!","info")
|
||||
debug.console.debugfile.writeText(error.stack)
|
||||
|
||||
return false
|
||||
}
|
||||
}else return true
|
||||
}
|
||||
|
||||
#checkDependency(id:string){
|
||||
try{
|
||||
require.resolve(id)
|
||||
return true
|
||||
}catch{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInstalled(){
|
||||
const missing: string[] = []
|
||||
this.data.npmDependencies.forEach((d) => {
|
||||
if (!this.#checkDependency(d)){
|
||||
missing.push(d)
|
||||
}
|
||||
})
|
||||
|
||||
return missing
|
||||
}
|
||||
pluginsInstalled(manager:ODPluginManager){
|
||||
const missing: string[] = []
|
||||
this.data.requiredPlugins.forEach((p) => {
|
||||
const plugin = manager.get(p)
|
||||
if (!plugin || !plugin.enabled){
|
||||
missing.push(p)
|
||||
}
|
||||
})
|
||||
|
||||
return missing
|
||||
}
|
||||
}
|
||||
|
||||
export class ODPluginEvent extends ODManagerData {
|
||||
listeners: Function[] = []
|
||||
oncelisteners: Function[] = []
|
||||
|
||||
getCurrentListeners(){
|
||||
const final: Function[] = []
|
||||
this.oncelisteners.forEach((l) => final.push(l))
|
||||
this.listeners.forEach((l) => final.push(l))
|
||||
|
||||
this.oncelisteners = []
|
||||
return final
|
||||
}
|
||||
}
|
||||
|
||||
export class ODPluginEventManager extends ODManager<ODPluginEvent> {
|
||||
#debug: ODDebugger
|
||||
listenerLimit: number = 25
|
||||
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"plugin event")
|
||||
this.#debug = debug
|
||||
}
|
||||
|
||||
setListenerLimit(limit:number){
|
||||
this.listenerLimit = limit
|
||||
}
|
||||
|
||||
on(event:string, callback:Function){
|
||||
const eventdata = this.get(event)
|
||||
if (eventdata){
|
||||
eventdata.listeners.push(callback)
|
||||
|
||||
if (eventdata.listeners.length > this.listenerLimit){
|
||||
this.#debug.console.log(new ODConsoleWarningMessage("Possible plugin event memory leak detected!",[
|
||||
{key:"event",value:event},
|
||||
{key:"listeners",value:eventdata.listeners.length.toString()}
|
||||
]))
|
||||
}
|
||||
}else{
|
||||
throw new ODSystemError("unknown plugin event \""+event+"\"")
|
||||
}
|
||||
}
|
||||
|
||||
once(event:string, callback:Function){
|
||||
const eventdata = this.get(event)
|
||||
if (eventdata){
|
||||
eventdata.oncelisteners.push(callback)
|
||||
|
||||
}else{
|
||||
throw new ODSystemError("unknown plugin event \""+event+"\"")
|
||||
}
|
||||
}
|
||||
|
||||
wait(event:string): Promise<any[]> {
|
||||
return new Promise((resolve,reject) => {
|
||||
const eventdata = this.get(event)
|
||||
if (eventdata){
|
||||
eventdata.oncelisteners.push((...args:any) => {resolve(args)})
|
||||
}else{
|
||||
reject("unknown plugin event \""+event+"\"")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
emit(event:string, params:any[]): Promise<void> {
|
||||
return new Promise(async (resolve,reject) => {
|
||||
const eventdata = this.get(event)
|
||||
if (eventdata){
|
||||
const listeners = eventdata.getCurrentListeners()
|
||||
|
||||
for (const listener of listeners){
|
||||
try {
|
||||
await listener(...params)
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
}else{
|
||||
reject("unknown plugin event \""+event+"\"")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ODPluginClassManager extends ODManager<ODManagerData> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"plugin class")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
///////////////////////////////////////
|
||||
//POST MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
import { ODMessageBuildResult, ODMessageBuildSentResult } from "./builder"
|
||||
import { ODDebugger } from "./console"
|
||||
import * as discord from "discord.js"
|
||||
|
||||
/**## ODPostManager `class`
|
||||
* This is an open ticket post manager.
|
||||
*
|
||||
* It manages `ODPosts`'s for you.
|
||||
*
|
||||
* You will probably use this to get the logs channel of the bot (or some other static channel/category).
|
||||
*/
|
||||
export class ODPostManager extends ODManager<ODPost<discord.GuildBasedChannel>> {
|
||||
/**A reference to the main server of the bot */
|
||||
#guild: discord.Guild|null = null
|
||||
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"post")
|
||||
}
|
||||
|
||||
add(data:ODPost<discord.GuildBasedChannel>, overwrite?:boolean): boolean {
|
||||
if (this.#guild) data.useGuild(this.#guild)
|
||||
return super.add(data,overwrite)
|
||||
}
|
||||
/**Initialize the post manager & all posts. */
|
||||
async init(guild:discord.Guild){
|
||||
this.#guild = guild
|
||||
for (const post of this.getAll()){
|
||||
post.useGuild(guild)
|
||||
await post.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODPost `class`
|
||||
* This is an open ticket post class.
|
||||
*
|
||||
* A post is just a shortcut to a static discord channel or category.
|
||||
* This can be used to get a specific channel over and over again!
|
||||
*
|
||||
* This class also contains utilities for sending messages via the Open Ticket builders.
|
||||
*/
|
||||
export class ODPost<ChannelType extends discord.GuildBasedChannel> extends ODManagerData {
|
||||
/**A reference to the main server of the bot */
|
||||
#guild: discord.Guild|null = null
|
||||
/**Is this post already initialized? */
|
||||
ready: boolean = false
|
||||
/**The discord.js channel */
|
||||
channel: ChannelType|null = null
|
||||
/**The discord channel id */
|
||||
channelId: string
|
||||
|
||||
constructor(id:ODValidId, channelId:string){
|
||||
super(id)
|
||||
this.channelId = channelId
|
||||
}
|
||||
|
||||
/**Use a specific guild in this class for fetching the channel*/
|
||||
useGuild(guild:discord.Guild|null){
|
||||
this.#guild = guild
|
||||
}
|
||||
/**Change the channel id to another channel! */
|
||||
setChannelId(id:string){
|
||||
this.channelId = id
|
||||
}
|
||||
/**Initialize the discord.js channel of this post. */
|
||||
async init(){
|
||||
if (this.ready) return
|
||||
if (!this.#guild) return this.channel = null
|
||||
try{
|
||||
this.channel = await this.#guild.channels.fetch(this.channelId) as ChannelType
|
||||
}catch{
|
||||
this.channel = null
|
||||
}
|
||||
this.ready = true
|
||||
}
|
||||
/**Send a message to this channel using the Open Ticket builder system */
|
||||
async send(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<true>> {
|
||||
if (!this.channel || !this.channel.isTextBased()) return {success:false,message:null}
|
||||
try{
|
||||
const sent = await this.channel.send(msg.message)
|
||||
return {success:true,message:sent}
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
///////////////////////////////////////
|
||||
//RESPONDER MODULE
|
||||
///////////////////////////////////////
|
||||
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 { ODDropdownData, ODMessageBuildResult, ODMessageBuildSentResult, ODModalBuildResult } from "./builder"
|
||||
|
||||
/**## ODResponderImplementation `class`
|
||||
* This is an open ticket responder implementation.
|
||||
*
|
||||
* It is a basic implementation of the `ODWorkerManager` used by all `ODResponder` classes.
|
||||
*
|
||||
* This class can't be used stand-alone & needs to be extended from!
|
||||
*/
|
||||
export class ODResponderImplementation<Instance,Source extends string,Params> extends ODManagerData {
|
||||
/**The manager that has all workers of this implementation */
|
||||
workers: ODWorkerManager<Instance,Source,Params>
|
||||
/**The `commandName` or `customId` needs to match this string or regex for this responder to be executed. */
|
||||
match: string|RegExp
|
||||
|
||||
constructor(id:ODValidId, match:string|RegExp, callback?:ODWorkerCallback<Instance,Source,Params>, priority?:number, callbackId?:ODValidId){
|
||||
super(id)
|
||||
this.match = match
|
||||
this.workers = new ODWorkerManager("descending")
|
||||
if (callback) this.workers.add(new ODWorker(callbackId ? callbackId : id,priority ?? 0,callback))
|
||||
}
|
||||
/**Execute all workers & return the result. */
|
||||
async respond(instance:Instance, source:Source, params:Params): Promise<void> {
|
||||
throw new ODSystemError("Tried to build an unimplemented ODResponderImplementation")
|
||||
}
|
||||
}
|
||||
|
||||
export type ODResponderTimeoutErrorCallback<Instance, Source extends "slash"|"text"|"button"|"dropdown"|"modal"|"other"> = (instance:Instance, source:Source) => void|Promise<void>
|
||||
|
||||
export class ODResponderManager {
|
||||
commands: ODCommandResponderManager
|
||||
buttons: ODButtonResponderManager
|
||||
dropdowns: ODDropdownResponderManager
|
||||
modals: ODModalResponderManager
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
export class ODCommandResponderManager extends ODManager<ODCommandResponder<"slash"|"text",any>> {
|
||||
#client: ODClientManager
|
||||
#timeoutErrorCallback: ODResponderTimeoutErrorCallback<ODCommandResponderInstance,"slash"|"text">|null = null
|
||||
#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<ODCommandResponderInstance,"slash"|"text">|null, ms:number|null){
|
||||
this.#timeoutErrorCallback = callback
|
||||
this.#timeoutMs = ms
|
||||
}
|
||||
|
||||
add(data:ODCommandResponder<"slash"|"text",any>, overwrite?:boolean){
|
||||
const res = super.add(data,overwrite)
|
||||
|
||||
//add the callback to the slash command manager
|
||||
this.#client.slashCommands.onInteraction(data.match,(interaction,cmd) => {
|
||||
const newData = this.get(data.id)
|
||||
if (!newData) return
|
||||
newData.respond(new ODCommandResponderInstance(interaction,cmd,this.#timeoutErrorCallback,this.#timeoutMs),"slash",{})
|
||||
})
|
||||
|
||||
//add the callback to the text command manager
|
||||
this.#client.textCommands.onInteraction(data.prefix,data.match,(interaction,cmd,options) => {
|
||||
const newData = this.get(data.id)
|
||||
if (!newData) return
|
||||
newData.respond(new ODCommandResponderInstance(interaction,cmd,this.#timeoutErrorCallback,this.#timeoutMs,options),"text",{})
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
export class ODCommandResponderInstanceOptions {
|
||||
#interaction:discord.ChatInputCommandInteraction|discord.Message
|
||||
#cmd:ODSlashCommand|ODTextCommand
|
||||
#options: ODTextCommandInteractionOption[]
|
||||
|
||||
constructor(interaction:discord.ChatInputCommandInteraction|discord.Message, cmd:ODSlashCommand|ODTextCommand, options?:ODTextCommandInteractionOption[]){
|
||||
this.#interaction = interaction
|
||||
this.#cmd = cmd
|
||||
this.#options = options ?? []
|
||||
}
|
||||
getString(name:string,required:true): string
|
||||
getString(name:string,required:false): string|null
|
||||
getString(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getString(name,required)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getString() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "string" && opt.name == name)
|
||||
if (opt && typeof opt.value == "string") return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getBoolean(name:string,required:true): boolean
|
||||
getBoolean(name:string,required:false): boolean|null
|
||||
getBoolean(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getBoolean(name,required)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getBoolean() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "boolean" && opt.name == name)
|
||||
if (opt && typeof opt.value == "boolean") return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getNumber(name:string,required:true): number
|
||||
getNumber(name:string,required:false): number|null
|
||||
getNumber(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getNumber(name,required)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getNumber() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "number" && opt.name == name)
|
||||
if (opt && typeof opt.value == "number") return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getChannel(name:string,required:true): discord.TextChannel|discord.VoiceChannel|discord.StageChannel|discord.NewsChannel|discord.MediaChannel|discord.ForumChannel|discord.CategoryChannel
|
||||
getChannel(name:string,required:false): discord.TextChannel|discord.VoiceChannel|discord.StageChannel|discord.NewsChannel|discord.MediaChannel|discord.ForumChannel|discord.CategoryChannel|null
|
||||
getChannel(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getChannel(name,required)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getChannel() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "channel" && opt.name == name)
|
||||
if (opt && (opt.value instanceof discord.TextChannel || opt.value instanceof discord.VoiceChannel || opt.value instanceof discord.StageChannel || opt.value instanceof discord.NewsChannel || opt.value instanceof discord.MediaChannel || opt.value instanceof discord.ForumChannel || opt.value instanceof discord.CategoryChannel)) return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getRole(name:string,required:true): discord.Role
|
||||
getRole(name:string,required:false): discord.Role|null
|
||||
getRole(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getRole(name,required)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getRole() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "role" && opt.name == name)
|
||||
if (opt && opt.value instanceof discord.Role) return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getUser(name:string,required:true): discord.User
|
||||
getUser(name:string,required:false): discord.User|null
|
||||
getUser(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getUser(name,required)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getUser() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "user" && opt.name == name)
|
||||
if (opt && opt.value instanceof discord.User) return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getGuildMember(name:string,required:true): discord.GuildMember
|
||||
getGuildMember(name:string,required:false): discord.GuildMember|null
|
||||
getGuildMember(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
const member = this.#interaction.options.getMember(name)
|
||||
if (!member && required) throw new ODSystemError("ODCommandResponderInstanceOptions:getGuildMember() slash command option not found!")
|
||||
return member
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getGuildMember() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "guildmember" && opt.name == name)
|
||||
if (opt && opt.value instanceof discord.GuildMember) return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getMentionable(name:string,required:true): discord.User|discord.Role
|
||||
getMentionable(name:string,required:false): discord.User|discord.Role|null
|
||||
getMentionable(name:string,required:boolean){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getMentionable(name,required)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getGuildMember() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message){
|
||||
const opt = this.#options.find((opt) => opt.type == "mentionable" && opt.name == name)
|
||||
if (opt && (opt.value instanceof discord.User || opt.value instanceof discord.Role)) return opt.value
|
||||
else return null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getSubGroup(): string|null
|
||||
getSubGroup(){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getSubcommandGroup(true)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getSubGroup() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message && this.#cmd instanceof ODTextCommand){
|
||||
//0: name, 1:sub/group, 2:sub
|
||||
const splittedName: string[] = this.#cmd.builder.name.split(" ")
|
||||
return splittedName[1] ?? null
|
||||
|
||||
}else return null
|
||||
}
|
||||
getSubCommand(): string|null
|
||||
getSubCommand(){
|
||||
if (this.#interaction instanceof discord.ChatInputCommandInteraction){
|
||||
try {
|
||||
return this.#interaction.options.getSubcommand(true)
|
||||
}catch{
|
||||
throw new ODSystemError("ODCommandResponderInstanceOptions:getSubCommand() slash command option not found!")
|
||||
}
|
||||
|
||||
}else if (this.#interaction instanceof discord.Message && this.#cmd instanceof ODTextCommand){
|
||||
//0: name, 1:sub/group, 2:sub
|
||||
const splittedName: string[] = this.#cmd.builder.name.split(" ")
|
||||
|
||||
//return the second subcommand when there is a subgroup
|
||||
if (splittedName.length > 2){
|
||||
return splittedName[2] ?? null
|
||||
}else return splittedName[1] ?? null
|
||||
|
||||
}else return null
|
||||
}
|
||||
}
|
||||
|
||||
export class ODCommandResponderInstance {
|
||||
interaction:discord.ChatInputCommandInteraction|discord.Message
|
||||
cmd:ODSlashCommand|ODTextCommand
|
||||
type: "message"|"interaction"
|
||||
didReply: boolean = false
|
||||
options: ODCommandResponderInstanceOptions
|
||||
user: discord.User
|
||||
member: discord.GuildMember|null
|
||||
guild: discord.Guild|null
|
||||
channel: discord.TextBasedChannel
|
||||
|
||||
constructor(interaction:discord.ChatInputCommandInteraction|discord.Message, cmd:ODSlashCommand|ODTextCommand, errorCallback:ODResponderTimeoutErrorCallback<ODCommandResponderInstance,"slash"|"text">|null, timeoutMs:number|null, options?:ODTextCommandInteractionOption[]){
|
||||
if (!interaction.channel) throw new ODSystemError("ODCommandResponderInstance: Unable to find interaction channel!")
|
||||
this.interaction = interaction
|
||||
this.cmd = cmd
|
||||
this.type = (interaction instanceof discord.Message) ? "message" : "interaction"
|
||||
this.options = new ODCommandResponderInstanceOptions(interaction,cmd,options)
|
||||
this.user = (interaction instanceof discord.Message) ? interaction.author : interaction.user
|
||||
this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null
|
||||
this.guild = interaction.guild
|
||||
this.channel = interaction.channel
|
||||
|
||||
|
||||
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 command!**"
|
||||
}})
|
||||
}else{
|
||||
await errorCallback(this,(this.type == "interaction") ? "slash" : "text")
|
||||
}
|
||||
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
},timeoutMs ?? 2500)
|
||||
}
|
||||
|
||||
async reply(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<boolean>> {
|
||||
try {
|
||||
if (this.type == "interaction" && this.interaction instanceof discord.ChatInputCommandInteraction){
|
||||
if (this.interaction.replied || this.interaction.deferred){
|
||||
const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:sent}
|
||||
}else{
|
||||
const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}
|
||||
}else if (this.type == "message" && this.interaction instanceof discord.Message){
|
||||
const sent = await this.interaction.channel.send(msg.message)
|
||||
this.didReply = true
|
||||
return {success:true,message:sent}
|
||||
}else return {success:false,message:null}
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
async defer(ephemeral:boolean){
|
||||
if (this.type != "interaction" || !(this.interaction instanceof discord.ChatInputCommandInteraction)) return false
|
||||
if (this.interaction.deferred) return false
|
||||
await this.interaction.deferReply({ephemeral})
|
||||
this.didReply = true
|
||||
return true
|
||||
}
|
||||
async modal(modal:ODModalBuildResult){
|
||||
if (this.type != "interaction" || !(this.interaction instanceof discord.ChatInputCommandInteraction)) return false
|
||||
this.interaction.showModal(modal.modal)
|
||||
this.didReply = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export class ODCommandResponder<Source extends "slash"|"text",Params> extends ODResponderImplementation<ODCommandResponderInstance,Source,Params> {
|
||||
/**The prefix of the text command needs to match this */
|
||||
prefix: string
|
||||
|
||||
constructor(id:ODValidId, prefix:string, match:string|RegExp, callback?:ODWorkerCallback<ODCommandResponderInstance,Source,Params>, priority?:number, callbackId?:ODValidId){
|
||||
super(id,match,callback,priority,callbackId)
|
||||
this.prefix = prefix
|
||||
}
|
||||
|
||||
/**Respond to this command */
|
||||
async respond(instance:ODCommandResponderInstance, source:Source, params:Params){
|
||||
//wait for workers to finish
|
||||
await this.workers.executeWorkers(instance,source,params)
|
||||
}
|
||||
}
|
||||
|
||||
export class ODButtonResponderManager extends ODManager<ODButtonResponder<"button",any>> {
|
||||
#client: ODClientManager
|
||||
#timeoutErrorCallback: ODResponderTimeoutErrorCallback<ODButtonResponderInstance,"button">|null = null
|
||||
#timeoutMs: number|null = null
|
||||
#listeners: ((interaction:discord.ButtonInteraction) => void)[] = []
|
||||
|
||||
constructor(debug:ODDebugger, debugname:string, client:ODClientManager){
|
||||
super(debug,debugname)
|
||||
this.#client = client
|
||||
|
||||
this.#client.client.on("interactionCreate",(interaction) => {
|
||||
if (!interaction.isButton()) return
|
||||
this.#listeners.forEach((cb) => cb(interaction))
|
||||
})
|
||||
}
|
||||
|
||||
/**Set the message to send when the response times out! */
|
||||
setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback<ODButtonResponderInstance,"button">|null, ms:number|null){
|
||||
this.#timeoutErrorCallback = callback
|
||||
this.#timeoutMs = ms
|
||||
}
|
||||
|
||||
add(data:ODButtonResponder<"button",any>, overwrite?:boolean){
|
||||
const res = super.add(data,overwrite)
|
||||
|
||||
this.#listeners.push((interaction) => {
|
||||
const newData = this.get(data.id)
|
||||
if (!newData) return
|
||||
if ((typeof newData.match == "string") ? interaction.customId == newData.match : newData.match.test(interaction.customId)) newData.respond(new ODButtonResponderInstance(interaction,this.#timeoutErrorCallback,this.#timeoutMs),"button",{})
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
export class ODButtonResponderInstance {
|
||||
interaction:discord.ButtonInteraction
|
||||
didReply: boolean = false
|
||||
user: discord.User
|
||||
member: discord.GuildMember|null
|
||||
guild: discord.Guild|null
|
||||
channel: discord.TextBasedChannel
|
||||
message: discord.Message
|
||||
|
||||
constructor(interaction:discord.ButtonInteraction, errorCallback:ODResponderTimeoutErrorCallback<ODButtonResponderInstance,"button">|null, timeoutMs:number|null){
|
||||
if (!interaction.channel) throw new ODSystemError("ODButtonResponderInstance: 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.message = interaction.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 button!**"
|
||||
}})
|
||||
}else{
|
||||
await errorCallback(this,"button")
|
||||
}
|
||||
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
},timeoutMs ?? 2500)
|
||||
}
|
||||
|
||||
async reply(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<boolean>> {
|
||||
try{
|
||||
if (this.interaction.replied || this.interaction.deferred){
|
||||
const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:sent}
|
||||
}else{
|
||||
const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
async update(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<boolean>> {
|
||||
try{
|
||||
if (this.interaction.replied || this.interaction.deferred){
|
||||
const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}else{
|
||||
const sent = await this.interaction.update(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
async defer(type:"reply"|"update", ephemeral:boolean){
|
||||
if (this.interaction.deferred) return false
|
||||
if (type == "reply"){
|
||||
await this.interaction.deferReply({ephemeral})
|
||||
}else{
|
||||
await this.interaction.deferUpdate()
|
||||
}
|
||||
this.didReply = true
|
||||
return true
|
||||
}
|
||||
async modal(modal:ODModalBuildResult){
|
||||
this.interaction.showModal(modal.modal)
|
||||
this.didReply = true
|
||||
return true
|
||||
}
|
||||
|
||||
getMessageComponent(type:"button",id:string|RegExp): discord.ButtonComponent|null
|
||||
getMessageComponent(type:"string-dropdown",id:string|RegExp): discord.StringSelectMenuComponent|null
|
||||
getMessageComponent(type:"user-dropdown",id:string|RegExp): discord.UserSelectMenuComponent|null
|
||||
getMessageComponent(type:"channel-dropdown",id:string|RegExp): discord.ChannelSelectMenuComponent|null
|
||||
getMessageComponent(type:"role-dropdown",id:string|RegExp): discord.RoleSelectMenuComponent|null
|
||||
getMessageComponent(type:"mentionable-dropdown",id:string|RegExp): discord.MentionableSelectMenuComponent|null
|
||||
|
||||
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) => {
|
||||
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
|
||||
else if (type == "user-dropdown" && component.type == discord.ComponentType.UserSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
else if (type == "channel-dropdown" && component.type == discord.ComponentType.ChannelSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
else if (type == "role-dropdown" && component.type == discord.ComponentType.RoleSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
else if (type == "mentionable-dropdown" && component.type == discord.ComponentType.MentionableSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
getMessageEmbed(): discord.Embed|null {
|
||||
return this.message.embeds[0] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export class ODButtonResponder<Source extends string,Params> extends ODResponderImplementation<ODButtonResponderInstance,Source,Params> {
|
||||
/**Respond to this button */
|
||||
async respond(instance:ODButtonResponderInstance, source:Source, params:Params){
|
||||
//wait for workers to finish
|
||||
await this.workers.executeWorkers(instance,source,params)
|
||||
}
|
||||
}
|
||||
|
||||
export class ODDropdownResponderManager extends ODManager<ODDropdownResponder<"dropdown",any>> {
|
||||
#client: ODClientManager
|
||||
#timeoutErrorCallback: ODResponderTimeoutErrorCallback<ODDropdownResponderInstance,"dropdown">|null = null
|
||||
#timeoutMs: number|null = null
|
||||
#listeners: ((interaction:discord.AnySelectMenuInteraction) => void)[] = []
|
||||
|
||||
constructor(debug:ODDebugger, debugname:string, client:ODClientManager){
|
||||
super(debug,debugname)
|
||||
this.#client = client
|
||||
|
||||
this.#client.client.on("interactionCreate",(interaction) => {
|
||||
if (!interaction.isAnySelectMenu()) return
|
||||
this.#listeners.forEach((cb) => cb(interaction))
|
||||
})
|
||||
}
|
||||
|
||||
/**Set the message to send when the response times out! */
|
||||
setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback<ODDropdownResponderInstance,"dropdown">|null, ms:number|null){
|
||||
this.#timeoutErrorCallback = callback
|
||||
this.#timeoutMs = ms
|
||||
}
|
||||
|
||||
add(data:ODDropdownResponder<"dropdown",any>, overwrite?:boolean){
|
||||
const res = super.add(data,overwrite)
|
||||
|
||||
this.#listeners.push((interaction) => {
|
||||
const newData = this.get(data.id)
|
||||
if (!newData) return
|
||||
if ((typeof newData.match == "string") ? interaction.customId == newData.match : newData.match.test(interaction.customId)) newData.respond(new ODDropdownResponderInstance(interaction,this.#timeoutErrorCallback,this.#timeoutMs),"dropdown",{})
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
export class ODDropdownResponderInstanceValues {
|
||||
#interaction:discord.AnySelectMenuInteraction
|
||||
#type: ODDropdownData["type"]
|
||||
|
||||
constructor(interaction:discord.AnySelectMenuInteraction, type:ODDropdownData["type"]){
|
||||
this.#interaction = interaction
|
||||
this.#type = type
|
||||
|
||||
if (interaction.isChannelSelectMenu()){
|
||||
interaction.values
|
||||
}
|
||||
}
|
||||
getStringValues(): string[] {
|
||||
try {
|
||||
return this.#interaction.values
|
||||
}catch{
|
||||
throw new ODSystemError("ODDropdownResponderInstanceValues:getStringValues() invalid values!")
|
||||
}
|
||||
}
|
||||
async getRoleValues(): Promise<discord.Role[]> {
|
||||
if (this.#type != "role") throw new ODSystemError("ODDropdownResponderInstanceValues:getRoleValues() dropdown type isn't role!")
|
||||
try {
|
||||
const result: discord.Role[] = []
|
||||
for (const id of this.#interaction.values){
|
||||
if (!this.#interaction.guild) break
|
||||
const role = await this.#interaction.guild.roles.fetch(id)
|
||||
if (role) result.push(role)
|
||||
}
|
||||
return result
|
||||
}catch{
|
||||
throw new ODSystemError("ODDropdownResponderInstanceValues:getRoleValues() invalid values!")
|
||||
}
|
||||
}
|
||||
async getUserValues(): Promise<discord.User[]> {
|
||||
if (this.#type != "role") throw new ODSystemError("ODDropdownResponderInstanceValues:getUserValues() dropdown type isn't user!")
|
||||
try {
|
||||
const result: discord.User[] = []
|
||||
for (const id of this.#interaction.values){
|
||||
const user = await this.#interaction.client.users.fetch(id)
|
||||
if (user) result.push(user)
|
||||
}
|
||||
return result
|
||||
}catch{
|
||||
throw new ODSystemError("ODDropdownResponderInstanceValues:getUserValues() invalid values!")
|
||||
}
|
||||
}
|
||||
async getChannelValues(): Promise<discord.GuildBasedChannel[]> {
|
||||
if (this.#type != "role") throw new ODSystemError("ODDropdownResponderInstanceValues:getChannelValues() dropdown type isn't channel!")
|
||||
try {
|
||||
const result: discord.GuildBasedChannel[] = []
|
||||
for (const id of this.#interaction.values){
|
||||
if (!this.#interaction.guild) break
|
||||
const guild = await this.#interaction.guild.channels.fetch(id)
|
||||
if (guild) result.push(guild)
|
||||
}
|
||||
return result
|
||||
}catch{
|
||||
throw new ODSystemError("ODDropdownResponderInstanceValues:getChannelValues() invalid values!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ODDropdownResponderInstance {
|
||||
interaction:discord.AnySelectMenuInteraction
|
||||
didReply: boolean = false
|
||||
type: ODDropdownData["type"]
|
||||
values: ODDropdownResponderInstanceValues
|
||||
user: discord.User
|
||||
member: discord.GuildMember|null
|
||||
guild: discord.Guild|null
|
||||
channel: discord.TextBasedChannel
|
||||
message: discord.Message
|
||||
|
||||
constructor(interaction:discord.AnySelectMenuInteraction, errorCallback:ODResponderTimeoutErrorCallback<ODDropdownResponderInstance,"dropdown">|null, timeoutMs:number|null){
|
||||
if (!interaction.channel) throw new ODSystemError("ODDropdownResponderInstance: Unable to find interaction channel!")
|
||||
this.interaction = interaction
|
||||
if (interaction.isStringSelectMenu()){
|
||||
this.type = "string"
|
||||
}else if (interaction.isRoleSelectMenu()){
|
||||
this.type = "role"
|
||||
}else if (interaction.isUserSelectMenu()){
|
||||
this.type = "user"
|
||||
}else if (interaction.isChannelSelectMenu()){
|
||||
this.type = "channel"
|
||||
}else if (interaction.isMentionableSelectMenu()){
|
||||
this.type = "mentionable"
|
||||
}else throw new ODSystemError("ODDropdownResponderInstance: invalid dropdown type!")
|
||||
|
||||
this.values = new ODDropdownResponderInstanceValues(interaction,this.type)
|
||||
this.user = interaction.user
|
||||
this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null
|
||||
this.guild = interaction.guild
|
||||
this.channel = interaction.channel
|
||||
this.message = interaction.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 dropdown!**"
|
||||
}})
|
||||
}else{
|
||||
await errorCallback(this,"dropdown")
|
||||
}
|
||||
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
},timeoutMs ?? 2500)
|
||||
}
|
||||
|
||||
async reply(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<boolean>> {
|
||||
try {
|
||||
if (this.interaction.replied || this.interaction.deferred){
|
||||
const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:sent}
|
||||
}else{
|
||||
const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
async update(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<boolean>> {
|
||||
try{
|
||||
if (this.interaction.replied || this.interaction.deferred){
|
||||
const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}else{
|
||||
const sent = await this.interaction.update(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
async defer(type:"reply"|"update", ephemeral:boolean){
|
||||
if (this.interaction.deferred) return false
|
||||
if (type == "reply"){
|
||||
await this.interaction.deferReply({ephemeral})
|
||||
}else{
|
||||
await this.interaction.deferUpdate()
|
||||
}
|
||||
this.didReply = true
|
||||
return true
|
||||
}
|
||||
async modal(modal:ODModalBuildResult){
|
||||
this.interaction.showModal(modal.modal)
|
||||
this.didReply = true
|
||||
return true
|
||||
}
|
||||
|
||||
getMessageComponent(type:"button",id:string|RegExp): discord.ButtonComponent|null
|
||||
getMessageComponent(type:"string-dropdown",id:string|RegExp): discord.StringSelectMenuComponent|null
|
||||
getMessageComponent(type:"user-dropdown",id:string|RegExp): discord.UserSelectMenuComponent|null
|
||||
getMessageComponent(type:"channel-dropdown",id:string|RegExp): discord.ChannelSelectMenuComponent|null
|
||||
getMessageComponent(type:"role-dropdown",id:string|RegExp): discord.RoleSelectMenuComponent|null
|
||||
getMessageComponent(type:"mentionable-dropdown",id:string|RegExp): discord.MentionableSelectMenuComponent|null
|
||||
|
||||
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) => {
|
||||
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
|
||||
else if (type == "user-dropdown" && component.type == discord.ComponentType.UserSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
else if (type == "channel-dropdown" && component.type == discord.ComponentType.ChannelSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
else if (type == "role-dropdown" && component.type == discord.ComponentType.RoleSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
else if (type == "mentionable-dropdown" && component.type == discord.ComponentType.MentionableSelect && component.customId && ((typeof id == "string") ? component.customId == id : id.test(component.customId))) result = component
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
getMessageEmbed(): discord.Embed|null {
|
||||
return this.message.embeds[0] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export class ODDropdownResponder<Source extends string,Params> extends ODResponderImplementation<ODDropdownResponderInstance,Source,Params> {
|
||||
/**Respond to this dropdown */
|
||||
async respond(instance:ODDropdownResponderInstance, source:Source, params:Params){
|
||||
//wait for workers to finish
|
||||
await this.workers.executeWorkers(instance,source,params)
|
||||
}
|
||||
}
|
||||
|
||||
export class ODModalResponderManager extends ODManager<ODModalResponder<"modal",any>> {
|
||||
#client: ODClientManager
|
||||
#timeoutErrorCallback: ODResponderTimeoutErrorCallback<ODModalResponderInstance,"modal">|null = null
|
||||
#timeoutMs: number|null = null
|
||||
#listeners: ((interaction:discord.ModalSubmitInteraction) => void)[] = []
|
||||
|
||||
constructor(debug:ODDebugger, debugname:string, client:ODClientManager){
|
||||
super(debug,debugname)
|
||||
this.#client = client
|
||||
|
||||
this.#client.client.on("interactionCreate",(interaction) => {
|
||||
if (!interaction.isModalSubmit()) return
|
||||
this.#listeners.forEach((cb) => cb(interaction))
|
||||
})
|
||||
}
|
||||
|
||||
/**Set the message to send when the response times out! */
|
||||
setTimeoutErrorCallback(callback:ODResponderTimeoutErrorCallback<ODModalResponderInstance,"modal">|null, ms:number|null){
|
||||
this.#timeoutErrorCallback = callback
|
||||
this.#timeoutMs = ms
|
||||
}
|
||||
|
||||
add(data:ODModalResponder<"modal",any>, overwrite?:boolean){
|
||||
const res = super.add(data,overwrite)
|
||||
|
||||
this.#listeners.push((interaction) => {
|
||||
const newData = this.get(data.id)
|
||||
if (!newData) return
|
||||
if ((typeof newData.match == "string") ? interaction.customId == newData.match : newData.match.test(interaction.customId)) newData.respond(new ODModalResponderInstance(interaction,this.#timeoutErrorCallback,this.#timeoutMs),"modal",{})
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
export class ODModalResponderInstanceValues {
|
||||
#interaction:discord.ModalSubmitInteraction
|
||||
|
||||
constructor(interaction:discord.ModalSubmitInteraction){
|
||||
this.#interaction = interaction
|
||||
}
|
||||
getTextField(name:string,required:true): string
|
||||
getTextField(name:string,required:false): string|null
|
||||
getTextField(name:string,required:boolean){
|
||||
try {
|
||||
const data = this.#interaction.fields.getField(name,discord.ComponentType.TextInput)
|
||||
if (!data && required) throw new ODSystemError("ODModalResponderInstanceValues:getTextField() field not found!")
|
||||
return (data) ? data.value : null
|
||||
}catch{
|
||||
throw new ODSystemError("ODModalResponderInstanceValues:getTextField() field not found!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ODModalResponderInstance {
|
||||
interaction:discord.ModalSubmitInteraction
|
||||
didReply: boolean = false
|
||||
values: ODModalResponderInstanceValues
|
||||
user: discord.User
|
||||
member: discord.GuildMember|null
|
||||
guild: discord.Guild|null
|
||||
channel: discord.TextBasedChannel|null
|
||||
|
||||
constructor(interaction:discord.ModalSubmitInteraction, errorCallback:ODResponderTimeoutErrorCallback<ODModalResponderInstance,"modal">|null, timeoutMs:number|null){
|
||||
this.interaction = interaction
|
||||
this.values = new ODModalResponderInstanceValues(interaction)
|
||||
this.user = interaction.user
|
||||
this.member = (interaction.member instanceof discord.GuildMember) ? interaction.member : null
|
||||
this.guild = interaction.guild
|
||||
this.channel = interaction.channel
|
||||
|
||||
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 modal!**"
|
||||
}})
|
||||
}else{
|
||||
await errorCallback(this,"modal")
|
||||
}
|
||||
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
},timeoutMs ?? 2500)
|
||||
}
|
||||
|
||||
async reply(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<boolean>> {
|
||||
try{
|
||||
if (this.interaction.replied || this.interaction.deferred){
|
||||
const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:sent}
|
||||
}else{
|
||||
const sent = await this.interaction.reply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
async update(msg:ODMessageBuildResult): Promise<ODMessageBuildSentResult<boolean>> {
|
||||
try{
|
||||
if (this.interaction.replied || this.interaction.deferred){
|
||||
const sent = await this.interaction.editReply(Object.assign(msg.message,{ephemeral:msg.ephemeral}))
|
||||
this.didReply = true
|
||||
return {success:true,message:await sent.fetch()}
|
||||
}else throw new ODSystemError()
|
||||
}catch{
|
||||
return {success:false,message:null}
|
||||
}
|
||||
}
|
||||
async defer(type:"reply"|"update", ephemeral:boolean){
|
||||
if (this.interaction.deferred) return false
|
||||
if (type == "reply"){
|
||||
await this.interaction.deferReply({ephemeral})
|
||||
}else{
|
||||
await this.interaction.deferUpdate()
|
||||
}
|
||||
this.didReply = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export class ODModalResponder<Source extends string,Params> extends ODResponderImplementation<ODModalResponderInstance,Source,Params> {
|
||||
/**Respond to this modal */
|
||||
async respond(instance:ODModalResponderInstance, source:Source, params:Params){
|
||||
//wait for workers to finish
|
||||
await this.workers.executeWorkers(instance,source,params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
///////////////////////////////////////
|
||||
//SESSION MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
import { ODDebugger } from "./console"
|
||||
import * as crypto from "crypto"
|
||||
|
||||
/**## ODSessionManager `class`
|
||||
* This is an open ticket session manager.
|
||||
*
|
||||
* It contains all sessions in open ticket. Sessions are a sort of temporary storage which will be cleared when the bot stops.
|
||||
* Data in sessions have a randomly generated key which will always be unique.
|
||||
*
|
||||
* Visit the `ODSession` class for more info
|
||||
*/
|
||||
export class ODSessionManager extends ODManager<ODSession> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"session")
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODSessionInstance `interface`
|
||||
* This interface represents a single session instance. It contains an id, data & some dates.
|
||||
*/
|
||||
export interface ODSessionInstance {
|
||||
/**The id of this session instance. */
|
||||
id:string,
|
||||
/**The creation date of this session instance. */
|
||||
creation:number,
|
||||
/**The custom amount of minutes before this session expires. */
|
||||
timeout:number|null,
|
||||
/**This is the data from this session instance */
|
||||
data:any
|
||||
}
|
||||
|
||||
/**## ODSessionTimeoutCallback `type`
|
||||
* This is the callback used for session timeout listeners.
|
||||
*/
|
||||
export type ODSessionTimeoutCallback = (id:string, timeout:"default"|"custom", data:any, creation:Date) => void
|
||||
|
||||
/**## ODSession `class`
|
||||
* This is an open ticket session.
|
||||
*
|
||||
* It can be used to create 100% unique id's for usage in the bot. An id can also store additional data which isn't saved to the filesystem.
|
||||
* You can almost compare it to the PHP session system.
|
||||
*/
|
||||
export class ODSession extends ODManagerData {
|
||||
/**The history of previously generated instance ids. Used to reduce the risk of generating the same id twice. */
|
||||
#idHistory: string[] = []
|
||||
/**The max length of the instance id history. */
|
||||
#maxIdHistoryLength: number = 500
|
||||
/**An array of all the currently active session instances. */
|
||||
sessions: ODSessionInstance[] = []
|
||||
/**The default amount of minutes before a session automatically stops. */
|
||||
timeoutMinutes: number = 30
|
||||
/**The id of the auto-timeout session checker interval */
|
||||
#intervalId: NodeJS.Timeout
|
||||
/**Listeners for when a session times-out. */
|
||||
#timeoutListeners: ODSessionTimeoutCallback[] = []
|
||||
|
||||
constructor(id:ODValidId, intervalSeconds?:number){
|
||||
super(id)
|
||||
|
||||
//create the auto-timeout session checker
|
||||
this.#intervalId = setInterval(() => {
|
||||
const deletableSessions: {instance:ODSessionInstance,reason:"default"|"custom"}[] = []
|
||||
|
||||
//collect all deletable sessions
|
||||
this.sessions.forEach((session) => {
|
||||
if (session.timeout && (new Date().getTime() - session.creation) > session.timeout*60000){
|
||||
//stop session => custom timeout
|
||||
deletableSessions.push({instance:session,reason:"custom"})
|
||||
}else if (!session.timeout && (new Date().getTime() - session.creation) > this.timeoutMinutes*60000){
|
||||
//stop session => default timeout
|
||||
deletableSessions.push({instance:session,reason:"default"})
|
||||
}
|
||||
})
|
||||
|
||||
//permanently delete sessions
|
||||
deletableSessions.forEach((session) => {
|
||||
const index = this.sessions.findIndex((s) => s.id === session.instance.id)
|
||||
this.sessions.splice(index,1)
|
||||
|
||||
//emit timeout listeners
|
||||
this.#timeoutListeners.forEach((cb) => cb(session.instance.id,session.reason,session.instance.data,new Date(session.instance.creation)))
|
||||
})
|
||||
|
||||
},((intervalSeconds) ? (intervalSeconds * 1000) : 60000))
|
||||
}
|
||||
|
||||
/**Create a unique hex id of 8 characters and add it to the instance id history */
|
||||
#createUniqueId(): string {
|
||||
const hex = crypto.randomBytes(4).toString("hex")
|
||||
if (this.#idHistory.includes(hex)){
|
||||
return this.#createUniqueId()
|
||||
}else{
|
||||
this.#idHistory.push(hex)
|
||||
if (this.#idHistory.length > this.#maxIdHistoryLength) this.#idHistory.shift()
|
||||
return hex
|
||||
}
|
||||
}
|
||||
/**Stop the global interval that automatically deletes timed-out sessions. (This action can't be reverted!) */
|
||||
stopAutoTimeout(){
|
||||
clearInterval(this.#intervalId)
|
||||
}
|
||||
|
||||
/**Start a session instance with data. Returns the unique id required to access the session. */
|
||||
start(data?:any): string {
|
||||
const id = this.#createUniqueId()
|
||||
this.sessions.push({
|
||||
id,data,
|
||||
creation:new Date().getTime(),
|
||||
timeout:null
|
||||
})
|
||||
return id
|
||||
}
|
||||
/**Get the data of a session instance. Returns `null` when not found. */
|
||||
data(id:string): any|null {
|
||||
const session = this.sessions.find((session) => session.id === id)
|
||||
if (!session) return null
|
||||
return session.data
|
||||
}
|
||||
/**Stop & delete a session instance. Returns `true` when sucessful. */
|
||||
stop(id:string): boolean {
|
||||
const index = this.sessions.findIndex((session) => session.id === id)
|
||||
if (index < 0) return false
|
||||
this.sessions.splice(index,1)
|
||||
return true
|
||||
}
|
||||
/**Update the data of a session instance. Returns `true` when sucessful. */
|
||||
update(id:string, data:any): boolean {
|
||||
const session = this.sessions.find((session) => session.id === id)
|
||||
if (!session) return false
|
||||
session.data = data
|
||||
return true
|
||||
}
|
||||
/**Change the global or session timeout minutes. Returns `true` when sucessful. */
|
||||
setTimeout(min:number, id?:string): boolean {
|
||||
if (!id){
|
||||
//change global timeout minutes
|
||||
this.timeoutMinutes = min
|
||||
return true
|
||||
}else{
|
||||
//change session instance timeout minutes
|
||||
const session = this.sessions.find((session) => session.id === id)
|
||||
if (!session) return false
|
||||
session.timeout = min
|
||||
return true
|
||||
}
|
||||
}
|
||||
/**Listen for a session timeout (default or custom) */
|
||||
onTimeout(callback:ODSessionTimeoutCallback){
|
||||
this.#timeoutListeners.push(callback)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
///////////////////////////////////////
|
||||
//STARTSCREEN MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
import { ODDebugger, ODError, ODLiveStatusManager } from "./console"
|
||||
import { ODFlag } from "./flag"
|
||||
import { ODPlugin, ODUnknownCrashedPlugin } from "./plugin"
|
||||
import ansis from "ansis"
|
||||
|
||||
export type ODStartScreenComponentRenderCallback = (location:number) => string|Promise<string>
|
||||
|
||||
export class ODStartScreenManager extends ODManager<ODStartScreenComponent> {
|
||||
/**Alias to the Open Ticket debugger. */
|
||||
#debug: ODDebugger
|
||||
/**Alias to the livestatus manager. */
|
||||
livestatus: ODLiveStatusManager
|
||||
|
||||
constructor(debug:ODDebugger,livestatus:ODLiveStatusManager){
|
||||
super(debug,"startscreen component")
|
||||
this.#debug = debug
|
||||
this.livestatus = livestatus
|
||||
}
|
||||
|
||||
/**Get all components in sorted order. */
|
||||
getSortedComponents(priority:"ascending"|"descending"){
|
||||
return this.getAll().sort((a,b) => {
|
||||
if (priority == "ascending") return a.priority-b.priority
|
||||
else return b.priority-a.priority
|
||||
})
|
||||
}
|
||||
/**Render all startscreen components in priority order. */
|
||||
async renderAllComponents(){
|
||||
const components = this.getSortedComponents("descending")
|
||||
|
||||
let location = 0
|
||||
for (const component of components){
|
||||
try {
|
||||
const renderedText = await component.renderAll(location)
|
||||
console.log(renderedText)
|
||||
this.#debug.console.debugfile.writeText("[STARTSCREEN] Component: \""+component.id+"\"\n"+ansis.strip(renderedText))
|
||||
}catch(e){
|
||||
this.#debug.console.log("Unable to render \""+component.id+"\" startscreen component!","error")
|
||||
this.#debug.console.debugfile.writeErrorMessage(new ODError(e,"uncaughtException"))
|
||||
}
|
||||
location++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStartScreenComponent extends ODManagerData {
|
||||
priority: number
|
||||
renderBefore: ODStartScreenComponentRenderCallback|null = null
|
||||
render: ODStartScreenComponentRenderCallback
|
||||
renderAfter: ODStartScreenComponentRenderCallback|null = null
|
||||
|
||||
constructor(id:ODValidId, priority:number, render:ODStartScreenComponentRenderCallback){
|
||||
super(id)
|
||||
this.priority = priority
|
||||
this.render = render
|
||||
}
|
||||
|
||||
async renderAll(location:number){
|
||||
const textBefore = (this.renderBefore) ? await this.renderBefore(location) : ""
|
||||
const text = await this.render(location)
|
||||
const textAfter = (this.renderAfter) ? await this.renderAfter(location) : ""
|
||||
return (textBefore ? textBefore+"\n" : "")+text+(textAfter ? "\n"+textAfter : "")
|
||||
}
|
||||
}
|
||||
|
||||
export interface ODStartScreenProperty {
|
||||
key:string,
|
||||
value:string
|
||||
}
|
||||
|
||||
export class ODStartScreenLogoComponent extends ODStartScreenComponent {
|
||||
logo: string[]
|
||||
topPadding: boolean
|
||||
bottomPadding: boolean
|
||||
logoHexColor: string
|
||||
|
||||
constructor(id:ODValidId, priority:number, logo:string[], topPadding?:boolean, bottomPadding?:boolean, logoHexColor?:string){
|
||||
super(id,priority,() => {
|
||||
const renderedTop = (this.topPadding ? "\n" : "")
|
||||
const renderedLogo = this.logo.join("\n")
|
||||
const renderedBottom = (this.bottomPadding ? "\n" : "")
|
||||
return ansis.hex(this.logoHexColor)(renderedTop+renderedLogo+renderedBottom)
|
||||
})
|
||||
this.logo = logo
|
||||
this.topPadding = topPadding ?? false
|
||||
this.bottomPadding = bottomPadding ?? false
|
||||
this.logoHexColor = logoHexColor ?? "#f8ba00"
|
||||
}
|
||||
}
|
||||
|
||||
export interface ODStartScreenHeaderAlignmentSettings {
|
||||
align:"center"|"left"|"right",
|
||||
width:number|ODStartScreenComponent
|
||||
}
|
||||
|
||||
export class ODStartScreenHeaderComponent extends ODStartScreenComponent {
|
||||
properties: ODStartScreenProperty[]
|
||||
spacer: string
|
||||
align: ODStartScreenHeaderAlignmentSettings|null
|
||||
|
||||
constructor(id:ODValidId, priority:number, properties:ODStartScreenProperty[], spacer?:string, align?:ODStartScreenHeaderAlignmentSettings){
|
||||
super(id,priority,async () => {
|
||||
const renderedProperties = ansis.bold(this.properties.map((prop) => prop.key+": "+prop.value).join(this.spacer))
|
||||
if (!this.align || this.align.align == "left"){
|
||||
return renderedProperties
|
||||
}else if (this.align.align == "right"){
|
||||
const width = (typeof this.align.width == "number") ? this.align.width : (
|
||||
ansis.strip(await this.align.width.renderAll(0)).split("\n").map((row) => row.length).reduce((prev,curr) => {
|
||||
if (prev < curr) return curr
|
||||
else return prev
|
||||
},0)
|
||||
)
|
||||
const offset = width - ansis.strip(renderedProperties).length
|
||||
if (offset < 0) return renderedProperties
|
||||
else{
|
||||
return (" ".repeat(offset) + renderedProperties)
|
||||
}
|
||||
}else if (this.align.align == "center"){
|
||||
const width = (typeof this.align.width == "number") ? this.align.width : (
|
||||
ansis.strip(await this.align.width.renderAll(0)).split("\n").map((row) => row.length).reduce((prev,curr) => {
|
||||
if (prev < curr) return curr
|
||||
else return prev
|
||||
})
|
||||
)
|
||||
const offset = Math.round((width - ansis.strip(renderedProperties).length)/2)
|
||||
if (offset < 0) return renderedProperties
|
||||
else{
|
||||
return (" ".repeat(offset) + renderedProperties)
|
||||
}
|
||||
}
|
||||
return renderedProperties
|
||||
})
|
||||
this.properties = properties
|
||||
this.spacer = spacer ?? " - "
|
||||
this.align = align ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStartScreenCategoryComponent extends ODStartScreenComponent {
|
||||
name: string
|
||||
renderIfEmpty: boolean
|
||||
|
||||
constructor(id:ODValidId, priority:number, name:string, render:ODStartScreenComponentRenderCallback, renderIfEmpty?:boolean){
|
||||
super(id,priority,async (location) => {
|
||||
const contents = await render(location)
|
||||
if (contents != "" || this.renderIfEmpty){
|
||||
return ansis.bold.underline("\n"+name.toUpperCase()+(contents != "" ? ":\n" : ":")) + contents
|
||||
}else return ""
|
||||
})
|
||||
this.name = name
|
||||
this.renderIfEmpty = renderIfEmpty ?? true
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStartScreenPropertiesCategoryComponent extends ODStartScreenCategoryComponent {
|
||||
properties: ODStartScreenProperty[]
|
||||
propertyHexColor: string
|
||||
|
||||
constructor(id:ODValidId, priority:number, name:string, properties:ODStartScreenProperty[], propertyHexColor?:string, renderIfEmpty?:boolean){
|
||||
super(id,priority,name,() => {
|
||||
return this.properties.map((prop) => ansis.hex(this.propertyHexColor)(prop.key+": ")+prop.value).join("\n")
|
||||
},renderIfEmpty)
|
||||
|
||||
this.properties = properties
|
||||
this.propertyHexColor = propertyHexColor ?? "#f8ba00"
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStartScreenFlagsCategoryComponent extends ODStartScreenCategoryComponent {
|
||||
flags: ODFlag[]
|
||||
|
||||
constructor(id:ODValidId, priority:number, flags:ODFlag[]){
|
||||
super(id,priority,"flags",() => {
|
||||
return this.flags.filter((flag) => (flag.value == true)).map((flag) => ansis.blue("["+flag.name+"] "+flag.description)).join("\n")
|
||||
},false)
|
||||
this.flags = flags
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStartScreenPluginsCategoryComponent extends ODStartScreenCategoryComponent {
|
||||
plugins: ODPlugin[]
|
||||
unknownCrashedPlugins: ODUnknownCrashedPlugin[]
|
||||
|
||||
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 renderedUnknownPlugins = unknownCrashedPlugins.map((plugin) => ansis.red("❌ ["+plugin.name+"] "+plugin.description))
|
||||
return [...renderedPlugins,...renderedUnknownPlugins].join("\n")
|
||||
},false)
|
||||
this.plugins = plugins
|
||||
this.unknownCrashedPlugins = unknownCrashedPlugins
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStartScreenLiveStatusCategoryComponent extends ODStartScreenCategoryComponent {
|
||||
livestatus: ODLiveStatusManager
|
||||
|
||||
constructor(id:ODValidId, priority:number, livestatus:ODLiveStatusManager){
|
||||
super(id,priority,"livestatus",async () => {
|
||||
const messages = await this.livestatus.getAllMessages()
|
||||
return this.livestatus.renderer.render(messages)
|
||||
},false)
|
||||
this.livestatus = livestatus
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStartScreenLogCategoryComponent extends ODStartScreenCategoryComponent {
|
||||
constructor(id:ODValidId, priority:number){
|
||||
super(id,priority,"logs",() => "",true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
///////////////////////////////////////
|
||||
//STAT MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODSystemError, ODValidId } from "./base"
|
||||
import { ODDebugger } from "./console"
|
||||
import { ODDatabase, ODJsonDatabaseStructure } from "./database"
|
||||
import * as discord from "discord.js"
|
||||
|
||||
export type ODValidStatValue = string|number|boolean
|
||||
export type ODStatsManagerInitCallback = (database:ODJsonDatabaseStructure, deletables:ODJsonDatabaseStructure) => void|Promise<void>
|
||||
export type ODStatScopeSetMode = "set"|"increase"|"decrease"
|
||||
|
||||
export class ODStatsManager extends ODManager<ODStatScope> {
|
||||
/**Alias to open ticket debugger. */
|
||||
#debug: ODDebugger
|
||||
/**Alias to open ticket stats database. */
|
||||
database: ODDatabase|null = null
|
||||
/**All the listeners for the init event. */
|
||||
#initListeners: ODStatsManagerInitCallback[] = []
|
||||
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"stat scope")
|
||||
this.#debug = debug
|
||||
}
|
||||
|
||||
useDatabase(database:ODDatabase){
|
||||
this.database = database
|
||||
}
|
||||
add(data:ODStatScope, overwrite?:boolean): boolean {
|
||||
data.useDebug(this.#debug,"stat")
|
||||
if (this.database) data.useDatabase(this.database)
|
||||
return super.add(data,overwrite)
|
||||
}
|
||||
async init(){
|
||||
if (!this.database) throw new ODSystemError("Unable to initialize stats scopes due to missing database!")
|
||||
|
||||
//get all valid categories
|
||||
const validCategories: string[] = []
|
||||
for (const scope of this.getAll()){
|
||||
validCategories.push(...scope.init())
|
||||
}
|
||||
|
||||
//filter out the deletable stats
|
||||
const deletableStats: ODJsonDatabaseStructure = []
|
||||
this.database.getAll().forEach((data) => {
|
||||
if (!validCategories.includes(data.category)) deletableStats.push(data)
|
||||
})
|
||||
|
||||
//do additional deletion
|
||||
for (const cb of this.#initListeners){
|
||||
await cb(this.database.getAll(),deletableStats)
|
||||
}
|
||||
|
||||
//delete all deletable stats
|
||||
deletableStats.forEach((data) => {
|
||||
if (!this.database) return
|
||||
this.database.delete(data.category,data.key)
|
||||
})
|
||||
}
|
||||
reset(){
|
||||
if (!this.database) return
|
||||
this.database.getAll().forEach((data) => {
|
||||
if (!this.database) return
|
||||
this.database.delete(data.category,data.key)
|
||||
})
|
||||
}
|
||||
onInit(callback:ODStatsManagerInitCallback){
|
||||
this.#initListeners.push(callback)
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStatScope extends ODManager<ODStat> {
|
||||
/**The id of this statistics scope. */
|
||||
id: ODId
|
||||
/**Is this stat scope already initialized? */
|
||||
ready: boolean = false
|
||||
/**Alias to open ticket stats database. */
|
||||
database: ODDatabase|null = null
|
||||
/**The name of this scope (used in embed title) */
|
||||
name:string
|
||||
|
||||
constructor(id:ODValidId, name:string){
|
||||
super()
|
||||
this.id = new ODId(id)
|
||||
this.name = name
|
||||
}
|
||||
|
||||
useDatabase(database:ODDatabase){
|
||||
this.database = database
|
||||
}
|
||||
getStat(id:ODValidId, scopeId:string): ODValidStatValue|null {
|
||||
if (!this.database) return null
|
||||
const newId = new ODId(id)
|
||||
const data = this.database.get(this.id.value+"_"+newId.value,scopeId)
|
||||
|
||||
if (typeof data == "undefined"){
|
||||
//set stats to default value & return
|
||||
return this.resetStat(id,scopeId)
|
||||
}else if (typeof data == "string" || typeof data == "boolean" || typeof data == "number"){
|
||||
//return value received from database
|
||||
return data
|
||||
}
|
||||
//return null on error
|
||||
return null
|
||||
}
|
||||
setStat(id:ODValidId, scopeId:string, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean {
|
||||
if (!this.database) return false
|
||||
const stat = this.get(id)
|
||||
if (!stat) return false
|
||||
if (mode == "set" || typeof value != "number"){
|
||||
this.database.set(this.id.value+"_"+stat.id.value,scopeId,value)
|
||||
}else if (mode == "increase"){
|
||||
const currentValue = this.getStat(id,scopeId)
|
||||
if (typeof currentValue != "number") this.database.set(this.id.value+"_"+stat.id.value,scopeId,0)
|
||||
else this.database.set(this.id.value+"_"+stat.id.value,scopeId,currentValue+value)
|
||||
}else if (mode == "decrease"){
|
||||
const currentValue = this.getStat(id,scopeId)
|
||||
if (typeof currentValue != "number") this.database.set(this.id.value+"_"+stat.id.value,scopeId,0)
|
||||
else this.database.set(this.id.value+"_"+stat.id.value,scopeId,currentValue-value)
|
||||
}
|
||||
return true
|
||||
}
|
||||
resetStat(id:ODValidId, scopeId:string): ODValidStatValue|null {
|
||||
if (!this.database) return null
|
||||
const stat = this.get(id)
|
||||
if (!stat) return null
|
||||
if (stat.value != null) this.database.set(this.id.value+"_"+stat.id.value,scopeId,stat.value)
|
||||
return stat.value
|
||||
}
|
||||
init(): string[] {
|
||||
//get all valid stats categories
|
||||
this.ready = true
|
||||
return this.getAll().map((stat) => this.id.value+"_"+stat.id.value)
|
||||
}
|
||||
async render(scopeId:string, guild:discord.Guild, channel:discord.TextBasedChannel, user:discord.User): Promise<string> {
|
||||
//sort from high priority to low
|
||||
const derefArray = [...this.getAll()]
|
||||
derefArray.sort((a,b) => {
|
||||
return b.priority-a.priority
|
||||
})
|
||||
const result: string[] = []
|
||||
|
||||
for (const stat of derefArray){
|
||||
try {
|
||||
if (stat instanceof ODDynamicStat){
|
||||
//dynamic render (without value)
|
||||
result.push(await stat.render("",scopeId,guild,channel,user))
|
||||
}else{
|
||||
//normal render (with value)
|
||||
const value = this.getStat(stat.id,scopeId)
|
||||
if (value != null) result.push(await stat.render(value,scopeId,guild,channel,user))
|
||||
}
|
||||
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
|
||||
return result.filter((stat) => stat !== "").join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
export class ODStatGlobalScope extends ODStatScope {
|
||||
getStat(id:ODValidId): ODValidStatValue|null {
|
||||
return super.getStat(id,"GLOBAL")
|
||||
}
|
||||
setStat(id:ODValidId, value:ODValidStatValue, mode:ODStatScopeSetMode): boolean {
|
||||
return super.setStat(id,"GLOBAL",value,mode)
|
||||
}
|
||||
resetStat(id:ODValidId): ODValidStatValue|null {
|
||||
return super.resetStat(id,"GLOBAL")
|
||||
}
|
||||
render(scopeId:"GLOBAL", guild:discord.Guild, channel:discord.TextBasedChannel, user: discord.User): Promise<string> {
|
||||
return super.render("GLOBAL",guild,channel,user)
|
||||
}
|
||||
}
|
||||
|
||||
export type ODStatRenderer = (value:ODValidStatValue, scopeId:string, guild:discord.Guild, channel:discord.TextBasedChannel, user:discord.User) => string|Promise<string>
|
||||
|
||||
export class ODStat extends ODManagerData {
|
||||
priority: number
|
||||
render: ODStatRenderer
|
||||
value: ODValidStatValue|null
|
||||
|
||||
constructor(id:ODValidId, priority:number, render:ODStatRenderer, value?:ODValidStatValue){
|
||||
super(id)
|
||||
this.priority = priority
|
||||
this.render = render
|
||||
this.value = value ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export class ODBasicStat extends ODStat {
|
||||
name: string
|
||||
|
||||
constructor(id:ODValidId, priority:number, name:string, value:ODValidStatValue){
|
||||
super(id,priority,(value) => {
|
||||
return ""+name+": `"+value.toString()+"`"
|
||||
},value)
|
||||
this.name = name
|
||||
}
|
||||
}
|
||||
|
||||
export type ODDynamicStatRenderer = (scopeId:string, guild:discord.Guild, channel:discord.TextBasedChannel, user:discord.User) => string|Promise<string>
|
||||
|
||||
export class ODDynamicStat extends ODStat {
|
||||
constructor(id:ODValidId, priority:number, render:ODDynamicStatRenderer){
|
||||
super(id,priority,(value,scopeId,guild,channel,user) => {
|
||||
return render(scopeId,guild,channel,user)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
///////////////////////////////////////
|
||||
//VERIFYBAR MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
import { ODMessage } from "./builder"
|
||||
import { ODDebugger } from "./console"
|
||||
import { ODButtonResponderInstance } from "./responder"
|
||||
import * as discord from "discord.js"
|
||||
import { ODWorkerManager } from "./worker"
|
||||
|
||||
export type ODVerifyBarCallback = (responder:ODButtonResponderInstance,customData?:string) => void|Promise<void>
|
||||
|
||||
export class ODVerifyBar extends ODManagerData {
|
||||
success: ODWorkerManager<ODButtonResponderInstance,"verifybar",{data:string|null,verifybarMessage:discord.Message<boolean>|null}>
|
||||
failure: ODWorkerManager<ODButtonResponderInstance,"verifybar",{data:string|null,verifybarMessage:discord.Message<boolean>|null}>
|
||||
message: ODMessage<"verifybar",{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,verifybar:ODVerifyBar,originalMessage:discord.Message<boolean>}>
|
||||
enabled: boolean
|
||||
|
||||
constructor(id:ODValidId, message:ODMessage<"verifybar",{guild:discord.Guild|null,channel:discord.TextBasedChannel,user:discord.User,originalMessage:discord.Message<boolean>}>, enabled?:boolean){
|
||||
super(id)
|
||||
this.success = new ODWorkerManager("descending")
|
||||
this.failure = new ODWorkerManager("descending")
|
||||
this.message = message
|
||||
this.enabled = enabled ?? true
|
||||
}
|
||||
|
||||
async activate(responder:ODButtonResponderInstance){
|
||||
if (this.enabled){
|
||||
//show verifybar
|
||||
const {guild,channel,user,message} = responder
|
||||
await responder.update(await this.message.build("verifybar",{guild,channel,user,verifybar:this,originalMessage:message}))
|
||||
}else{
|
||||
//instant success
|
||||
if (this.success) await this.success.executeWorkers(responder,"verifybar",{data:null,verifybarMessage:null})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ODVerifyBarManager extends ODManager<ODVerifyBar> {
|
||||
constructor(debug:ODDebugger){
|
||||
super(debug,"verifybar")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
///////////////////////////////////////
|
||||
//WORKER MODULE
|
||||
///////////////////////////////////////
|
||||
import { ODId, ODManager, ODManagerData, ODValidId } from "./base"
|
||||
|
||||
/**## ODWorkerCallback `type`
|
||||
* This is the callback used in `ODWorker`!
|
||||
*/
|
||||
export type ODWorkerCallback<Instance, Source extends string, Params> = (instance:Instance, params:Params, source:Source, cancel:() => void) => void|Promise<void>
|
||||
|
||||
/**## ODWorker `class`
|
||||
* This is an open ticket worker.
|
||||
*
|
||||
* You can compare it with a normal javascript callback, but slightly more advanced!
|
||||
*
|
||||
* - It has an `id` for identification of the function
|
||||
* - A `priority` to know when to execute this callback (related to others)
|
||||
* - It knows who called this callback (`source`)
|
||||
* - And much more!
|
||||
*/
|
||||
export class ODWorker<Instance, Source extends string, Params> extends ODManagerData {
|
||||
/**The priority of this worker */
|
||||
priority: number
|
||||
/**The main callback of this worker */
|
||||
callback: ODWorkerCallback<Instance,Source,Params>
|
||||
|
||||
constructor(id:ODValidId, priority:number, callback:ODWorkerCallback<Instance,Source,Params>){
|
||||
super(id)
|
||||
this.priority = priority
|
||||
this.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
/**## ODWorker `class`
|
||||
* This is an open ticket worker manager.
|
||||
*
|
||||
* It manages & executes `ODWorker`'s in the correct order.
|
||||
*
|
||||
* You will probably register a custom worker in this class to create a message or button.
|
||||
*/
|
||||
export class ODWorkerManager<Instance, Source extends string, Params> extends ODManager<ODWorker<Instance,Source,Params>> {
|
||||
/**The order of execution for workers inside this manager. */
|
||||
#priorityOrder: "ascending"|"descending"
|
||||
/**The backup worker will be executed when one of the workers fails or cancels execution. */
|
||||
backupWorker: ODWorker<{reason:"error"|"cancel"},Source,Params>|null = null
|
||||
|
||||
constructor(priorityOrder:"ascending"|"descending"){
|
||||
super()
|
||||
this.#priorityOrder = priorityOrder
|
||||
}
|
||||
|
||||
/**Get all workers in sorted order. */
|
||||
getSortedWorkers(priority:"ascending"|"descending"){
|
||||
const derefArray = [...this.getAll()]
|
||||
|
||||
return derefArray.sort((a,b) => {
|
||||
if (priority == "ascending") return a.priority-b.priority
|
||||
else return b.priority-a.priority
|
||||
})
|
||||
}
|
||||
/**Execute all workers on an instance using the given source & parameters. */
|
||||
async executeWorkers(instance:Instance, source:Source, params:Params){
|
||||
const derefParams = {...params}
|
||||
const workers = this.getSortedWorkers(this.#priorityOrder)
|
||||
let didCancel = false
|
||||
let didCrash = false
|
||||
|
||||
for (const worker of workers){
|
||||
if (didCancel) break
|
||||
try {
|
||||
await worker.callback(instance,derefParams,source,() => {
|
||||
didCancel = true
|
||||
})
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
didCrash = true
|
||||
}
|
||||
}
|
||||
if (didCancel && this.backupWorker){
|
||||
try{
|
||||
await this.backupWorker.callback({reason:"cancel"},derefParams,source,() => {})
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}else if (didCrash && this.backupWorker){
|
||||
try{
|
||||
await this.backupWorker.callback({reason:"error"},derefParams,source,() => {})
|
||||
}catch(err){
|
||||
process.emit("uncaughtException",err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user