Open Ticket v4 Beta
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import * as fs from "fs"
|
||||
|
||||
let tempErrors: string[] = []
|
||||
const tempError = () => {
|
||||
if (tempErrors.length > 0){
|
||||
console.log("\n\n==============================\n[OPEN TICKET ERROR]: "+tempErrors.join("\n[OPEN TICKET ERROR]: ")+"\n==============================\n\n")
|
||||
process.exit(0)
|
||||
}
|
||||
tempErrors = []
|
||||
}
|
||||
|
||||
const nodev = process.versions.node.split(".")
|
||||
if (Number(nodev[0]) < 18){
|
||||
tempErrors.push("Invalid node.js version. Open Ticket requires node.js v18 or above!")
|
||||
}
|
||||
tempError()
|
||||
|
||||
const moduleInstalled = (id:string, throwError:boolean) => {
|
||||
try{
|
||||
require.resolve(id)
|
||||
return true
|
||||
|
||||
}catch{
|
||||
if (throwError) tempErrors.push("npm module \""+id+"\" is not installed! Install it via 'npm install "+id+"'")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
moduleInstalled("discord.js",true)
|
||||
moduleInstalled("ansis",true)
|
||||
moduleInstalled("formatted-json-stringify",true)
|
||||
moduleInstalled("tsx",true)
|
||||
tempError()
|
||||
|
||||
//init API
|
||||
import * as api from "../api/api" //import for local use
|
||||
export * as api from "../api/api" //export to other parts of bot
|
||||
|
||||
|
||||
export const openticket = new api.ODMain()
|
||||
console.log("\n--------------------------- OPEN TICKET STARTUP ---------------------------")
|
||||
openticket.log("Logging system activated!","system")
|
||||
openticket.debug.debug("Using Node.js "+process.version+"!")
|
||||
|
||||
try{
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json").toString())
|
||||
openticket.debug.debug("Using discord.js "+packageJson.dependencies["discord.js"]+"!")
|
||||
openticket.debug.debug("Using ansis "+packageJson.dependencies["ansis"]+"!")
|
||||
openticket.debug.debug("Using formatted-json-stringify "+packageJson.dependencies["formatted-json-stringify"]+"!")
|
||||
openticket.debug.debug("Using tsx "+packageJson.dependencies["tsx"]+"!")
|
||||
}catch{
|
||||
openticket.debug.debug("Failed to fetch module versions!")
|
||||
}
|
||||
|
||||
const timer = (ms:number): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
},ms)
|
||||
})
|
||||
}
|
||||
|
||||
interface ODUtilities {
|
||||
/**## project `utility variable`
|
||||
* This is the name of the project you are currently in.
|
||||
*
|
||||
* Developers can use this to create a multi-plugin compatible with all bots supporting the `open-discord` framework!
|
||||
*/
|
||||
project:"openticket"
|
||||
/**## isBeta `utility variable`
|
||||
* Check if you're running a beta version of Open Ticket.
|
||||
*/
|
||||
isBeta:boolean
|
||||
/**## moduleInstalled `utility function`
|
||||
* Use this function to check if an npm package is installed or not!
|
||||
* @example utilities.moduleInstalled("discord.js") //check if discord.js is installed
|
||||
*/
|
||||
moduleInstalled(id:string): boolean
|
||||
/**## timer `utility function`
|
||||
* Use this to wait for a certain amount of milliseconds. This only works when using `await`
|
||||
* @example await utilities.timer(1000) //wait 1sec
|
||||
*/
|
||||
timer(ms:number): Promise<void>
|
||||
/**## emojiTitle `utility function`
|
||||
* Use this function to create a title with an emoji before/after the text. The style & divider are set in `openticket.defaults`
|
||||
* @example utilities.emojiTitle("📎","Links") //create a title with an emoji based on the bot emoji style
|
||||
*/
|
||||
emojiTitle(emoji:string, text:string): string
|
||||
/**## runAsync `utility function`
|
||||
* Use this function to run a piece of code asyncronous without creating a separate function for it!
|
||||
*/
|
||||
runAsync(func:() => Promise<void>): void
|
||||
/**## timedAwait `utility function`
|
||||
* Use this function to await a promise but reject after the certain timeout has been reached.
|
||||
*/
|
||||
timedAwait<ReturnValue extends Promise<any>>(promise:ReturnValue, timeout:number, onError:(err:Error) => void): ReturnValue
|
||||
/**## dateString `utility function`
|
||||
* Use this function to create a short date string in the following format: `DD/MM/YYYY HH:MM:SS`
|
||||
*/
|
||||
dateString(date:Date): string
|
||||
/**## asyncReplace `utility function`
|
||||
* Same as `string.replace(search, value)` but with async compatibility
|
||||
*/
|
||||
asyncReplace(text:string, regex:RegExp, func:(value:string,...args:any[]) => Promise<string>): Promise<string>
|
||||
/**## easterEggs `utility object`
|
||||
* Object containing data for Open Ticket easter eggs.
|
||||
*/
|
||||
easterEggs: api.ODEasterEggs,
|
||||
/**## ODVersionMigration `utility class`
|
||||
* This class is used to manage data migration between Open Ticket versions.
|
||||
*
|
||||
* It shouldn't be used by plugins because this is an internal API feature!
|
||||
*/
|
||||
ODVersionMigration:new (version:api.ODVersion,func:() => void|Promise<void>) => ODVersionMigration
|
||||
}
|
||||
|
||||
/**## ODVersionMigration `utility class`
|
||||
* This class is used to manage data migration between Open Ticket versions.
|
||||
*
|
||||
* It shouldn't be used by plugins because this is an internal API feature!
|
||||
*/
|
||||
class ODVersionMigration {
|
||||
/**The version to migrate data to */
|
||||
version: api.ODVersion
|
||||
/**The migration function */
|
||||
#func: () => void|Promise<void>
|
||||
|
||||
constructor(version:api.ODVersion,func:() => void|Promise<void>){
|
||||
this.version = version
|
||||
this.#func = func
|
||||
}
|
||||
/**Run this version migration as a plugin. Returns `false` when someting goes wrong. */
|
||||
async migrate(): Promise<boolean> {
|
||||
try{
|
||||
await this.#func()
|
||||
return true
|
||||
}catch{
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const utilities: ODUtilities = {
|
||||
project:"openticket",
|
||||
isBeta:true,
|
||||
moduleInstalled:(id:string) => {
|
||||
return moduleInstalled(id,false)
|
||||
},
|
||||
timer,
|
||||
emojiTitle(emoji:string, text:string){
|
||||
const style = openticket.defaults.getDefault("emojiTitleStyle")
|
||||
const divider = openticket.defaults.getDefault("emojiTitleDivider")
|
||||
|
||||
if (style == "disabled") return text
|
||||
else if (style == "before") return emoji+divider+text
|
||||
else if (style == "after") return text+divider+emoji
|
||||
else if (style == "double") return emoji+divider+text+divider+emoji
|
||||
else return text
|
||||
},
|
||||
runAsync(func){
|
||||
func()
|
||||
},
|
||||
timedAwait<ReturnValue>(promise:ReturnValue,timeout:number,onError:(err:Error) => void): ReturnValue {
|
||||
let allowResolve = true
|
||||
return new Promise(async (resolve,reject) => {
|
||||
//set timeout & stop if it is before the promise resolved
|
||||
setTimeout(() => {
|
||||
allowResolve = false
|
||||
reject("utilities.timedAwait() => Promise Timeout")
|
||||
},timeout)
|
||||
|
||||
//get promise result & return if not already rejected
|
||||
try{
|
||||
const res = await promise
|
||||
if (allowResolve) resolve(res)
|
||||
}catch(err){
|
||||
onError(err)
|
||||
}
|
||||
return promise
|
||||
}) as ReturnValue
|
||||
},
|
||||
dateString(date): string {
|
||||
return `${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
|
||||
},
|
||||
async asyncReplace(text,regex,func): Promise<string> {
|
||||
const promises: Promise<string>[] = []
|
||||
text.replace(regex,(match,...args) => {
|
||||
promises.push(func(match,...args))
|
||||
return match
|
||||
})
|
||||
const data = await Promise.all(promises)
|
||||
const result = text.replace(regex,(match) => {
|
||||
const replaceResult = data.shift()
|
||||
return replaceResult ?? match
|
||||
})
|
||||
return result
|
||||
},
|
||||
easterEggs:{
|
||||
creator:"779742674932072469",
|
||||
translators:[
|
||||
"779742674932072469"
|
||||
]
|
||||
},
|
||||
ODVersionMigration
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import {openticket, api, utilities} from "../../index"
|
||||
|
||||
export const loadVersionMigrationSystem = async () => {
|
||||
//ENTER MIGRATION CONTEXT
|
||||
await preloadMigrationContext()
|
||||
|
||||
const lastVersion = isMigrationRequired()
|
||||
openticket.versions.add(lastVersion ? lastVersion : api.ODVersion.fromString("openticket:last-version",openticket.versions.get("openticket:version").toString()))
|
||||
if (lastVersion && !openticket.flags.get("openticket:no-migration").value){
|
||||
//MIGRATION IS REQUIRED
|
||||
openticket.log("Detected old data!","info")
|
||||
openticket.log("Starting closed API context...","debug")
|
||||
await utilities.timer(600)
|
||||
openticket.log("Migrating data to new version...","debug")
|
||||
await loadAllVersionMigrations(lastVersion)
|
||||
openticket.log("Stopping closed API context...","debug")
|
||||
await utilities.timer(400)
|
||||
openticket.log("All data is now up to date!","info")
|
||||
await utilities.timer(200)
|
||||
console.log("---------------------------------------------------------------------")
|
||||
}
|
||||
saveAllVersionsToDatabase()
|
||||
|
||||
//DEFAULT FLAGS
|
||||
if (openticket.flags.exists("openticket:no-plugins") && openticket.flags.get("openticket:no-plugins").value) openticket.defaults.setDefault("pluginLoading",false)
|
||||
if (openticket.flags.exists("openticket:soft-plugins") && openticket.flags.get("openticket:soft-plugins").value) openticket.defaults.setDefault("softPluginLoading",true)
|
||||
if (openticket.flags.exists("openticket:crash") && openticket.flags.get("openticket:crash").value) openticket.defaults.setDefault("crashOnError",true)
|
||||
if (openticket.flags.exists("openticket:force-slash-update") && openticket.flags.get("openticket:force-slash-update").value) openticket.defaults.setDefault("forceSlashCommandRegistration",true)
|
||||
|
||||
//LEAVE MIGRATION CONTEXT
|
||||
await unloadMigrationContext()
|
||||
}
|
||||
|
||||
const preloadMigrationContext = async () => {
|
||||
openticket.debug.debug("-- MIGRATION CONTEXT START --")
|
||||
await (await import("../../data/framework/flagLoader.ts")).loadAllFlags()
|
||||
openticket.flags.init()
|
||||
await (await import("../../data/framework/configLoader.ts")).loadAllConfigs()
|
||||
await (await import("../../data/framework/databaseLoader.ts")).loadAllDatabases()
|
||||
openticket.debug.visible = true
|
||||
}
|
||||
|
||||
const unloadMigrationContext = async () => {
|
||||
openticket.debug.visible = false
|
||||
openticket.databases.getAll().forEach((database) => openticket.databases.remove(database.id))
|
||||
openticket.configs.getAll().forEach((config) => openticket.configs.remove(config.id))
|
||||
openticket.flags.getAll().forEach((flag) => openticket.flags.remove(flag.id))
|
||||
openticket.debug.debug("-- MIGRATION CONTEXT END --")
|
||||
}
|
||||
|
||||
const isMigrationRequired = (): false|api.ODVersion => {
|
||||
const rawVersion = openticket.databases.get("openticket:global").get("openticket:last-version","openticket:version")
|
||||
if (!rawVersion) return false
|
||||
const version = api.ODVersion.fromString("openticket:last-version",rawVersion)
|
||||
if (openticket.versions.get("openticket:version").compare(version) == "higher"){
|
||||
return version
|
||||
}else return false
|
||||
}
|
||||
|
||||
const loadAllVersionMigrations = async (lastVersion:api.ODVersion) => {
|
||||
const migrations = (await import("./migration.ts")).migrations
|
||||
migrations.sort((a,b) => {
|
||||
const comparison = a.version.compare(b.version)
|
||||
if (comparison == "equal") return 0
|
||||
else if (comparison == "higher") return 1
|
||||
else return -1
|
||||
})
|
||||
for (const migration of migrations){
|
||||
if (migration.version.compare(lastVersion) == "higher"){
|
||||
const success = await migration.migrate()
|
||||
if (success) openticket.log("Migrated data to "+migration.version.toString()+"!","debug",[
|
||||
{key:"success",value:success ? "true" : "false"}
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveAllVersionsToDatabase = async () => {
|
||||
const globalDatabase = openticket.databases.get("openticket:global")
|
||||
|
||||
openticket.versions.getAll().forEach((version) => {
|
||||
globalDatabase.set("openticket:last-version",version.id.value,version.toString())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import {openticket, api, utilities} from "../../index"
|
||||
|
||||
export const migrations = [
|
||||
//MIGRATE TO v4.0.0
|
||||
new utilities.ODVersionMigration(api.ODVersion.fromString("openticket:version","v4.0.0"),async () => {
|
||||
//nothing needs to be transferred :)
|
||||
})
|
||||
]
|
||||
@@ -0,0 +1,197 @@
|
||||
import {openticket, api, utilities} from "../../index"
|
||||
import fs from "fs"
|
||||
import { ODPluginError } from "../api/api"
|
||||
|
||||
export const loadAllPlugins = async () => {
|
||||
//start launching plugins
|
||||
openticket.log("Loading plugins...","system")
|
||||
let initPluginError: boolean = false
|
||||
|
||||
if (!fs.existsSync("./plugins")){
|
||||
openticket.log("Couldn't find ./plugins directory, canceling all plugin execution!","error")
|
||||
return
|
||||
}
|
||||
const plugins = fs.readdirSync("./plugins")
|
||||
|
||||
//check & validate
|
||||
plugins.forEach(async (p) => {
|
||||
//prechecks
|
||||
if (!fs.statSync("./plugins/"+p).isDirectory()) return openticket.log("Plugin is not a directory, canceling plugin execution...","plugin",[
|
||||
{key:"plugin",value:"./plugins/"+p}
|
||||
])
|
||||
if (!fs.existsSync("./plugins/"+p+"/plugin.json")){
|
||||
initPluginError = true
|
||||
openticket.log("Plugin doesn't have a plugin.json, canceling plugin execution...","plugin",[
|
||||
{key:"plugin",value:"./plugins/"+p}
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
//plugin loading
|
||||
try {
|
||||
const rawplugindata: api.ODPluginData = JSON.parse(fs.readFileSync("./plugins/"+p+"/plugin.json").toString())
|
||||
|
||||
if (typeof rawplugindata != "object") throw new ODPluginError("Failed to load plugin.json")
|
||||
if (typeof rawplugindata.id != "string") throw new ODPluginError("Failed to load plugin.json/id")
|
||||
if (typeof rawplugindata.name != "string") throw new ODPluginError("Failed to load plugin.json/name")
|
||||
if (typeof rawplugindata.version != "string") throw new ODPluginError("Failed to load plugin.json/version")
|
||||
if (typeof rawplugindata.startFile != "string") throw new ODPluginError("Failed to load plugin.json/startFile")
|
||||
|
||||
if (typeof rawplugindata.enabled != "boolean") throw new ODPluginError("Failed to load plugin.json/enabled")
|
||||
if (typeof rawplugindata.priority != "number") throw new ODPluginError("Failed to load plugin.json/priority")
|
||||
if (!Array.isArray(rawplugindata.events)) throw new ODPluginError("Failed to load plugin.json/events")
|
||||
|
||||
if (!Array.isArray(rawplugindata.npmDependencies)) throw new ODPluginError("Failed to load plugin.json/npmDependencies")
|
||||
if (!Array.isArray(rawplugindata.requiredPlugins)) throw new ODPluginError("Failed to load plugin.json/requiredPlugins")
|
||||
if (!Array.isArray(rawplugindata.incompatiblePlugins)) throw new ODPluginError("Failed to load plugin.json/incompatiblePlugins")
|
||||
|
||||
if (typeof rawplugindata.details != "object") throw new ODPluginError("Failed to load plugin.json/details")
|
||||
if (typeof rawplugindata.details.author != "string") throw new ODPluginError("Failed to load plugin.json/details/author")
|
||||
if (typeof rawplugindata.details.shortDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/shortDescription")
|
||||
if (typeof rawplugindata.details.longDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/longDescription")
|
||||
if (typeof rawplugindata.details.imageUrl != "string") throw new ODPluginError("Failed to load plugin.json/details/imageUrl")
|
||||
if (typeof rawplugindata.details.projectUrl != "string") throw new ODPluginError("Failed to load plugin.json/details/projectUrl")
|
||||
if (!Array.isArray(rawplugindata.details.tags)) throw new ODPluginError("Failed to load plugin.json/details/tags")
|
||||
|
||||
if (rawplugindata.id != p) throw new ODPluginError("Failed to load plugin, directory name is required to match the id")
|
||||
|
||||
if (openticket.plugins.exists(rawplugindata.id)) throw new ODPluginError("Failed to load plugin, this id already exists in another plugin")
|
||||
|
||||
//plugin.json is valid => load plugin
|
||||
const plugin = new api.ODPlugin(p,rawplugindata)
|
||||
openticket.plugins.add(plugin)
|
||||
|
||||
}catch(e){
|
||||
//when any of the above errors happen, crash the bot when soft mode isn't enabled
|
||||
initPluginError = true
|
||||
openticket.log(e.message+", canceling plugin execution...","plugin",[
|
||||
{key:"path",value:"./plugins/"+p}
|
||||
])
|
||||
openticket.log("You can see more about this error in the ./otdebug.txt file!","info")
|
||||
openticket.debugfile.writeText(e.stack)
|
||||
|
||||
//try to get some crashed plugin data
|
||||
try{
|
||||
const rawplugindata: api.ODPluginData = JSON.parse(fs.readFileSync("./plugins/"+p+"/plugin.json").toString())
|
||||
openticket.plugins.unknownCrashedPlugins.push({
|
||||
name:rawplugindata.name ?? "./plugins/"+p,
|
||||
description:(rawplugindata.details && rawplugindata.details.shortDescription) ? rawplugindata.details.shortDescription : "This plugin crashed :(",
|
||||
})
|
||||
}catch{}
|
||||
}
|
||||
})
|
||||
|
||||
//sorted plugins (based on priority)
|
||||
const sortedPlugins = openticket.plugins.getAll().sort((a,b) => {
|
||||
return (b.priority - a.priority)
|
||||
})
|
||||
|
||||
//check for incompatible & missing plugins/dependencies
|
||||
const incompatibilities: {from:string,to:string}[] = []
|
||||
const missingDependencies: {id:string,missing:string}[] = []
|
||||
const missingPlugins: {id:string,missing:string}[] = []
|
||||
|
||||
//go trough all plugins for errors
|
||||
sortedPlugins.forEach((plugin) => {
|
||||
const from = plugin.id.value
|
||||
plugin.data.incompatiblePlugins.forEach((to) => {
|
||||
//deny incompatibility if it already exists
|
||||
if (incompatibilities.find((p) => (p.from == from && p.to == to) || (p.to == from && p.from == to))) return
|
||||
|
||||
//check for existence of both plugins => add to list
|
||||
if (openticket.plugins.exists(to)) incompatibilities.push({from,to})
|
||||
})
|
||||
plugin.dependenciesInstalled().forEach((missing) => missingDependencies.push({id:from,missing}))
|
||||
plugin.pluginsInstalled(openticket.plugins).forEach((missing) => missingPlugins.push({id:from,missing}))
|
||||
})
|
||||
|
||||
//handle all incompatibilities
|
||||
incompatibilities.forEach((match) => {
|
||||
const fromPlugin = openticket.plugins.get(match.from)
|
||||
if (fromPlugin && !fromPlugin.crashed){
|
||||
fromPlugin.crashed = true
|
||||
fromPlugin.crashReason = "incompatible.plugin"
|
||||
}
|
||||
const toPlugin = openticket.plugins.get(match.to)
|
||||
if (toPlugin && !toPlugin.crashed){
|
||||
toPlugin.crashed = true
|
||||
toPlugin.crashReason = "incompatible.plugin"
|
||||
}
|
||||
|
||||
openticket.log(`Incompatible plugins => "${match.from}" & "${match.to}", canceling plugin execution...`,"plugin",[
|
||||
{key:"path1",value:"./plugins/"+match.from},
|
||||
{key:"path2",value:"./plugins/"+match.to}
|
||||
])
|
||||
initPluginError = true
|
||||
})
|
||||
|
||||
//handle all missing dependencies
|
||||
missingDependencies.forEach((match) => {
|
||||
const plugin = openticket.plugins.get(match.id)
|
||||
if (plugin && !plugin.crashed){
|
||||
plugin.crashed = true
|
||||
plugin.crashReason = "missing.dependency"
|
||||
}
|
||||
|
||||
openticket.log(`Missing npm dependency "${match.missing}", canceling plugin execution...`,"plugin",[
|
||||
{key:"path",value:"./plugins/"+match.id}
|
||||
])
|
||||
initPluginError = true
|
||||
})
|
||||
|
||||
//handle all missing plugins
|
||||
missingPlugins.forEach((match) => {
|
||||
const plugin = openticket.plugins.get(match.id)
|
||||
if (plugin && !plugin.crashed){
|
||||
plugin.crashed = true
|
||||
plugin.crashReason = "missing.plugin"
|
||||
}
|
||||
|
||||
openticket.log(`Missing required plugin "${match.missing}", canceling plugin execution...`,"plugin",[
|
||||
{key:"path",value:"./plugins/"+match.id}
|
||||
])
|
||||
initPluginError = true
|
||||
})
|
||||
|
||||
//exit on error (when soft mode disabled)
|
||||
if (!openticket.defaults.getDefault("softPluginLoading") && initPluginError){
|
||||
console.log("")
|
||||
openticket.log("Please fix all plugin errors above & try again!","error")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
//preload all events required for every plugin
|
||||
for (const plugin of sortedPlugins){
|
||||
if (plugin.enabled) plugin.data.events.forEach((event) => openticket.events.add(new api.ODEvent(event)))
|
||||
}
|
||||
|
||||
//execute all working plugins
|
||||
for (const plugin of sortedPlugins){
|
||||
const status = await plugin.execute(openticket.debug,false)
|
||||
|
||||
//exit on error (when soft mode disabled)
|
||||
if (!status && !openticket.defaults.getDefault("softPluginLoading")){
|
||||
console.log("")
|
||||
openticket.log("Please fix all plugin errors above & try again!","error")
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
for (const plugin of sortedPlugins){
|
||||
if (plugin.enabled){
|
||||
openticket.debug.debug("Plugin \""+plugin.id.value+"\" loaded",[
|
||||
{key:"status",value:(plugin.crashed ? "crashed" : "success")},
|
||||
{key:"crashReason",value:(plugin.crashed ? (plugin.crashReason ?? "/") : "/")},
|
||||
{key:"author",value:plugin.details.author},
|
||||
{key:"version",value:plugin.version.toString()},
|
||||
{key:"priority",value:plugin.priority.toString()}
|
||||
])
|
||||
}else{
|
||||
openticket.debug.debug("Plugin \""+plugin.id.value+"\" disabled",[
|
||||
{key:"author",value:plugin.details.author},
|
||||
{key:"version",value:plugin.version.toString()},
|
||||
{key:"priority",value:plugin.priority.toString()}
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user