mirror of
https://github.com/ragestudio/linebridge.git
synced 2025-06-09 10:34:17 +00:00
423 lines
13 KiB
JavaScript
Executable File
423 lines
13 KiB
JavaScript
Executable File
import("./patches")
|
|
|
|
import fs from "node:fs"
|
|
import path from "node:path"
|
|
import { EventEmitter } from "@foxify/events"
|
|
|
|
import Endpoint from "./classes/endpoint"
|
|
|
|
import defaults from "./defaults"
|
|
|
|
import IPCClient from "./classes/IPCClient"
|
|
|
|
async function loadEngine(engine) {
|
|
const enginesPath = path.resolve(__dirname, "engines")
|
|
|
|
const selectedEnginePath = path.resolve(enginesPath, engine)
|
|
|
|
if (!fs.existsSync(selectedEnginePath)) {
|
|
throw new Error(`Engine ${engine} not found!`)
|
|
}
|
|
|
|
return require(selectedEnginePath).default
|
|
}
|
|
|
|
class Server {
|
|
constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) {
|
|
this.isExperimental = defaults.isExperimental ?? false
|
|
|
|
if (this.isExperimental) {
|
|
console.warn("🚧 This version of Linebridge is experimental! 🚧")
|
|
}
|
|
|
|
this.params = {
|
|
...defaults.params,
|
|
...params.default ?? params,
|
|
}
|
|
|
|
this.controllers = {
|
|
...controllers.default ?? controllers,
|
|
}
|
|
|
|
this.middlewares = {
|
|
...middlewares.default ?? middlewares,
|
|
}
|
|
|
|
this.headers = {
|
|
...defaults.headers,
|
|
...headers.default ?? headers,
|
|
}
|
|
|
|
this.valid_http_methods = defaults.valid_http_methods
|
|
|
|
// fix and fulfill params
|
|
this.params.useMiddlewares = this.params.useMiddlewares ?? []
|
|
this.params.name = this.constructor.refName ?? this.params.refName
|
|
this.params.useEngine = this.constructor.useEngine ?? this.params.useEngine ?? "express"
|
|
this.params.listen_ip = this.constructor.listenIp ?? this.constructor.listen_ip ?? this.params.listen_ip ?? "0.0.0.0"
|
|
this.params.listen_port = this.constructor.listenPort ?? this.constructor.listen_port ?? this.params.listen_port ?? 3000
|
|
this.params.http_protocol = this.params.http_protocol ?? "http"
|
|
this.params.http_address = `${this.params.http_protocol}://${defaults.localhost_address}:${this.params.listen_port}`
|
|
this.params.disableWebSockets = this.constructor.disableWebSockets ?? this.params.disableWebSockets ?? false
|
|
|
|
this.params.routesPath = this.constructor.routesPath ?? this.params.routesPath
|
|
this.params.wsRoutesPath = this.constructor.wsRoutesPath ?? this.params.wsRoutesPath
|
|
|
|
return this
|
|
}
|
|
|
|
engine = null
|
|
|
|
events = null
|
|
|
|
ipc = null
|
|
|
|
ipcEvents = null
|
|
|
|
eventBus = new EventEmitter()
|
|
|
|
initialize = async () => {
|
|
const startHrTime = process.hrtime()
|
|
|
|
// register events
|
|
if (this.events) {
|
|
if (this.events.default) {
|
|
this.events = this.events.default
|
|
}
|
|
|
|
for (const [eventName, eventHandler] of Object.entries(this.events)) {
|
|
this.eventBus.on(eventName, eventHandler)
|
|
}
|
|
}
|
|
|
|
const engineParams = {
|
|
...this.params,
|
|
handleWsAuth: this.handleWsAuth,
|
|
handleAuth: this.handleHttpAuth,
|
|
requireAuth: this.constructor.requireHttpAuth,
|
|
refName: this.constructor.refName ?? this.params.refName,
|
|
}
|
|
|
|
// initialize engine
|
|
this.engine = await loadEngine(this.params.useEngine)
|
|
|
|
this.engine = new this.engine(engineParams)
|
|
|
|
if (typeof this.engine.init === "function") {
|
|
await this.engine.init(engineParams)
|
|
}
|
|
|
|
// create a router map
|
|
if (typeof this.engine.router.map !== "object") {
|
|
this.engine.router.map = {}
|
|
}
|
|
|
|
// try to execute onInitialize hook
|
|
if (typeof this.onInitialize === "function") {
|
|
await this.onInitialize()
|
|
}
|
|
|
|
// set server defined headers
|
|
this.useDefaultHeaders()
|
|
|
|
// set server defined middlewares
|
|
this.useDefaultMiddlewares()
|
|
|
|
// register controllers
|
|
await this.initializeControllers()
|
|
|
|
// register routes
|
|
await this.initializeRoutes()
|
|
|
|
// register main index endpoint `/`
|
|
await this.registerBaseEndpoints()
|
|
|
|
// use main router
|
|
await this.engine.app.use(this.engine.router)
|
|
|
|
// initialize websocket init hook if needed
|
|
if (this.engine.ws) {
|
|
if (typeof this.engine.ws?.initialize == "function") {
|
|
await this.engine.ws.initialize({
|
|
redisInstance: this.redis
|
|
})
|
|
}
|
|
}
|
|
|
|
// if is a linebridge service then initialize IPC Channels
|
|
if (process.env.lb_service) {
|
|
await this.initializeIpc()
|
|
}
|
|
|
|
// try to execute beforeInitialize hook.
|
|
if (typeof this.afterInitialize === "function") {
|
|
await this.afterInitialize()
|
|
}
|
|
|
|
// listen
|
|
await this.engine.listen(engineParams)
|
|
|
|
// calculate elapsed time on ms, to fixed 2
|
|
const elapsedHrTime = process.hrtime(startHrTime)
|
|
const elapsedTimeInMs = elapsedHrTime[0] * 1e3 + elapsedHrTime[1] / 1e6
|
|
|
|
console.info(`🛰 Server ready!\n\t - ${this.params.http_protocol}://${this.params.listen_ip}:${this.params.listen_port} \n\t - Tooks ${elapsedTimeInMs.toFixed(2)}ms`)
|
|
}
|
|
|
|
initializeIpc = async () => {
|
|
console.info("🚄 Starting IPC client")
|
|
|
|
this.ipc = global.ipc = new IPCClient(this, process)
|
|
}
|
|
|
|
useDefaultHeaders = () => {
|
|
this.engine.app.use((req, res, next) => {
|
|
Object.keys(this.headers).forEach((key) => {
|
|
res.setHeader(key, this.headers[key])
|
|
})
|
|
|
|
next()
|
|
})
|
|
}
|
|
|
|
useDefaultMiddlewares = async () => {
|
|
const middlewares = await this.resolveMiddlewares([
|
|
...this.params.useMiddlewares,
|
|
...defaults.useMiddlewares,
|
|
])
|
|
|
|
middlewares.forEach((middleware) => {
|
|
this.engine.app.use(middleware)
|
|
})
|
|
}
|
|
|
|
initializeControllers = async () => {
|
|
const controllers = Object.entries(this.controllers)
|
|
|
|
for await (let [key, controller] of controllers) {
|
|
if (typeof controller !== "function") {
|
|
throw new Error(`Controller must use the controller class!`)
|
|
}
|
|
|
|
if (controller.disabled) {
|
|
console.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`)
|
|
continue
|
|
}
|
|
|
|
try {
|
|
const ControllerInstance = new controller()
|
|
|
|
// get endpoints from controller (ComplexController)
|
|
const HTTPEndpoints = ControllerInstance.__get_http_endpoints()
|
|
const WSEndpoints = ControllerInstance.__get_ws_endpoints()
|
|
|
|
HTTPEndpoints.forEach((endpoint) => {
|
|
this.register.http(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares))
|
|
})
|
|
|
|
// WSEndpoints.forEach((endpoint) => {
|
|
// this.registerWSEndpoint(endpoint)
|
|
// })
|
|
} catch (error) {
|
|
console.error(`\n\x1b[41m\x1b[37m🆘 [${controller.refName ?? controller.name}] Controller initialization failed:\x1b[0m ${error.stack} \n`)
|
|
}
|
|
}
|
|
}
|
|
|
|
initializeRoutes = async (filePath) => {
|
|
if (!this.params.routesPath) {
|
|
return false
|
|
}
|
|
|
|
const scanPath = filePath ?? this.params.routesPath
|
|
|
|
const files = fs.readdirSync(scanPath)
|
|
|
|
for await (const file of files) {
|
|
const filePath = `${scanPath}/${file}`
|
|
|
|
const stat = fs.statSync(filePath)
|
|
|
|
if (stat.isDirectory()) {
|
|
await this.initializeRoutes(filePath)
|
|
|
|
continue
|
|
} else if (file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
let splitedFilePath = filePath.split("/")
|
|
|
|
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf("routes") + 1)
|
|
|
|
const method = splitedFilePath[splitedFilePath.length - 1].split(".")[0].toLocaleLowerCase()
|
|
|
|
splitedFilePath = splitedFilePath.slice(0, splitedFilePath.length - 1)
|
|
|
|
// parse parametrized routes
|
|
const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g
|
|
|
|
splitedFilePath = splitedFilePath.map((route) => {
|
|
if (route.match(parametersRegex)) {
|
|
route = route.replace(parametersRegex, ":$1")
|
|
}
|
|
|
|
route = route.replace("[$]", "*")
|
|
|
|
return route
|
|
})
|
|
|
|
let route = splitedFilePath.join("/")
|
|
|
|
route = route.replace(".jsx", "")
|
|
route = route.replace(".js", "")
|
|
route = route.replace(".ts", "")
|
|
route = route.replace(".tsx", "")
|
|
|
|
if (route.endsWith("/index")) {
|
|
route = route.replace("/index", "")
|
|
}
|
|
|
|
route = `/${route}`
|
|
|
|
// import route
|
|
let routeFile = require(filePath)
|
|
|
|
routeFile = routeFile.default ?? routeFile
|
|
|
|
if (typeof routeFile !== "function") {
|
|
if (!routeFile.fn) {
|
|
console.warn(`Missing fn handler in [${method}][${route}]`)
|
|
continue
|
|
}
|
|
|
|
if (Array.isArray(routeFile.useContext)) {
|
|
let contexts = {}
|
|
|
|
for (const context of routeFile.useContext) {
|
|
contexts[context] = this.contexts[context]
|
|
}
|
|
|
|
routeFile.contexts = contexts
|
|
|
|
routeFile.fn.bind({ contexts })
|
|
}
|
|
}
|
|
|
|
new Endpoint(
|
|
this,
|
|
{
|
|
route: route,
|
|
enabled: true,
|
|
middlewares: routeFile.middlewares,
|
|
handlers: {
|
|
[method]: routeFile.fn ?? routeFile,
|
|
}
|
|
}
|
|
)
|
|
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
register = {
|
|
http: (endpoint, ..._middlewares) => {
|
|
// check and fix method
|
|
endpoint.method = endpoint.method?.toLowerCase() ?? "get"
|
|
|
|
if (defaults.fixed_http_methods[endpoint.method]) {
|
|
endpoint.method = defaults.fixed_http_methods[endpoint.method]
|
|
}
|
|
|
|
// check if method is supported
|
|
if (typeof this.engine.router[endpoint.method] !== "function") {
|
|
throw new Error(`Method [${endpoint.method}] is not supported!`)
|
|
}
|
|
|
|
// grab the middlewares
|
|
let middlewares = [..._middlewares]
|
|
|
|
if (endpoint.middlewares) {
|
|
middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)]
|
|
}
|
|
|
|
this.engine.router.map[endpoint.route] = {
|
|
method: endpoint.method,
|
|
path: endpoint.route,
|
|
}
|
|
|
|
// register endpoint to http interface router
|
|
this.engine.router[endpoint.method](endpoint.route, ...middlewares, endpoint.fn)
|
|
},
|
|
ws: (endpoint, ...execs) => {
|
|
endpoint.nsp = endpoint.nsp ?? "/main"
|
|
|
|
this.websocket_instance.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch])
|
|
|
|
this.websocket_instance.map[endpoint.on] = {
|
|
nsp: endpoint.nsp,
|
|
channel: endpoint.on,
|
|
}
|
|
},
|
|
}
|
|
|
|
async registerBaseEndpoints() {
|
|
if (this.params.disableBaseEndpoint) {
|
|
console.warn("‼️ [disableBaseEndpoint] Base endpoint is disabled! Endpoints mapping will not be available, so linebridge client bridges will not work! ‼️")
|
|
return false
|
|
}
|
|
|
|
const scanPath = path.join(__dirname, "baseEndpoints")
|
|
const files = fs.readdirSync(scanPath)
|
|
|
|
for await (const file of files) {
|
|
if (file === "index.js") {
|
|
continue
|
|
}
|
|
|
|
let endpoint = require(path.join(scanPath, file)).default
|
|
|
|
new endpoint(this)
|
|
}
|
|
}
|
|
|
|
resolveMiddlewares = (requestedMiddlewares) => {
|
|
const middlewares = {
|
|
...this.middlewares,
|
|
...defaults.middlewares,
|
|
}
|
|
|
|
requestedMiddlewares = Array.isArray(requestedMiddlewares) ? requestedMiddlewares : [requestedMiddlewares]
|
|
|
|
const execs = []
|
|
|
|
requestedMiddlewares.forEach((middlewareKey) => {
|
|
if (typeof middlewareKey === "string") {
|
|
if (typeof middlewares[middlewareKey] !== "function") {
|
|
throw new Error(`Middleware ${middlewareKey} not found!`)
|
|
}
|
|
|
|
execs.push(middlewares[middlewareKey])
|
|
}
|
|
|
|
if (typeof middlewareKey === "function") {
|
|
execs.push(middlewareKey)
|
|
}
|
|
})
|
|
|
|
return execs
|
|
}
|
|
|
|
// Utilities
|
|
toogleEndpointReachability = (method, route, enabled) => {
|
|
if (typeof this.endpoints_map[method] !== "object") {
|
|
throw new Error(`Cannot toogle endpoint, method [${method}] not set!`)
|
|
}
|
|
|
|
if (typeof this.endpoints_map[method][route] !== "object") {
|
|
throw new Error(`Cannot toogle endpoint [${route}], is not registered!`)
|
|
}
|
|
|
|
this.endpoints_map[method][route].enabled = enabled ?? !this.endpoints_map[method][route].enabled
|
|
}
|
|
}
|
|
|
|
module.exports = Server |