Files
open-ticket/src/core/api/modules/base.ts
T

762 lines
29 KiB
TypeScript

///////////////////////////////////////
//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>
/**## ODOptionalPromise `type`
* This is a simple type to represent a type as normal value or a promise value.
*/
export type ODOptionalPromise<T> = T|Promise<T>
/**## 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 can 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 (text before `:`). (e.g. `openticket` for all built-in ids) */
source: string
/**The identifier of the id (text 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 can 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 classes based on their `ODId`.
* It is somewhat the same as the default JS `Map()`.
* You can extend this class when creating your own classes & managers.
*
* This class has many useful functions based on `ODId` (add, get, remove, getAll, getFiltered, exists, loopAll, ...)
*/
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)
}
/**Run an iterator over all data in this manager. This method also supports async-await behaviour!*/
async loopAll(cb:(data:DataType,id:ODId) => ODPromiseVoid): Promise<void> {
for (const data of this.getAll()){
await cb(data,data.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[]
}