diff --git a/project/package.json b/project/package.json index 5f834739..a7ee60d5 100644 --- a/project/package.json +++ b/project/package.json @@ -70,12 +70,12 @@ "@yao-pkg/pkg-fetch": "3.5.9", "cross-env": "~7.0", "eslint": "~8.57", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "~9.1", "eslint-import-resolver-typescript": "~3.6", "eslint-plugin-import": "~2.29", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-switch-allman": "^1.0.2", - "eslint-plugin-unused-imports": "^3.2.0", + "eslint-plugin-prettier": "~5.1", + "eslint-plugin-switch-allman": "~1.0", + "eslint-plugin-unused-imports": "~3.2", "fs-extra": "~11.2", "gulp": "~4.0", "gulp-decompress": "~3.0", @@ -84,11 +84,11 @@ "gulp-rename": "~2.0", "madge": "~6.1", "minimist": "~1.2", - "prettier": "^3.2.5", + "prettier": "~3.2", "resedit": "~2.0", "ts-node-dev": "~2.0", "tsconfig-paths": "~4.2", - "tslint-config-prettier": "^1.18.0", + "tslint-config-prettier": "~1.18", "typedoc": "~0.25", "typemoq": "~2.1", "typescript-eslint": "~7.8", diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 9b062a33..d5ab4f97 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -192,6 +192,10 @@ import { HttpServer } from "@spt-aki/servers/HttpServer"; import { RagfairServer } from "@spt-aki/servers/RagfairServer"; import { SaveServer } from "@spt-aki/servers/SaveServer"; import { WebSocketServer } from "@spt-aki/servers/WebSocketServer"; +import { AkiWebSocketConnectionHandler } from "@spt-aki/servers/ws/AkiWebSocketConnectionHandler"; +import { IWebSocketConnectionHandler } from "@spt-aki/servers/ws/IWebSocketConnectionHandler"; +import { DefaultAkiWebSocketMessageHandler } from "@spt-aki/servers/ws/message/DefaultAkiWebSocketMessageHandler"; +import { IAkiWebSocketMessageHandler } from "@spt-aki/servers/ws/message/IAkiWebSocketMessageHandler"; import { BotEquipmentFilterService } from "@spt-aki/services/BotEquipmentFilterService"; import { BotEquipmentModPoolService } from "@spt-aki/services/BotEquipmentModPoolService"; import { BotGenerationCacheService } from "@spt-aki/services/BotGenerationCacheService"; @@ -383,6 +387,12 @@ export class Container depContainer.registerType("SptCommand", "GiveSptCommand"); depContainer.registerType("SptCommand", "TraderSptCommand"); depContainer.registerType("SptCommand", "ProfileSptCommand"); + + // WebSocketHandlers + depContainer.registerType("WebSocketConnectionHandler", "AkiWebSocketConnectionHandler"); + + // WebSocketMessageHandlers + depContainer.registerType("AkiWebSocketMessageHandler", "DefaultAkiWebSocketMessageHandler"); } private static registerUtils(depContainer: DependencyContainer): void @@ -778,6 +788,8 @@ export class Container depContainer.register("DatabaseServer", DatabaseServer, { lifecycle: Lifecycle.Singleton }); depContainer.register("HttpServer", HttpServer, { lifecycle: Lifecycle.Singleton }); depContainer.register("WebSocketServer", WebSocketServer, { lifecycle: Lifecycle.Singleton }); + depContainer.register("AkiWebSocketConnectionHandler", AkiWebSocketConnectionHandler, { lifecycle: Lifecycle.Singleton }); + depContainer.register("DefaultAkiWebSocketMessageHandler", DefaultAkiWebSocketMessageHandler, { lifecycle: Lifecycle.Singleton }); depContainer.register("RagfairServer", RagfairServer); depContainer.register("SaveServer", SaveServer, { lifecycle: Lifecycle.Singleton }); depContainer.register("ConfigServer", ConfigServer, { lifecycle: Lifecycle.Singleton }); diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts index 4cd2b13d..4fe2dbba 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts @@ -191,8 +191,8 @@ export class GiveSptCommand implements ISptCommand } } - const localizedGlobal - = this.databaseServer.getTables().locales.global[locale] ?? this.databaseServer.getTables().locales.global.en; + const localizedGlobal = this.databaseServer.getTables().locales.global[locale] + ?? this.databaseServer.getTables().locales.global.en; // If item is an item name, we need to search using that item name and the locale which one we want otherwise // item is just the tplId. const tplId = isItemName diff --git a/project/src/helpers/NotificationSendHelper.ts b/project/src/helpers/NotificationSendHelper.ts index cc8158d2..9c798d67 100644 --- a/project/src/helpers/NotificationSendHelper.ts +++ b/project/src/helpers/NotificationSendHelper.ts @@ -6,7 +6,7 @@ import { MemberCategory } from "@spt-aki/models/enums/MemberCategory"; import { MessageType } from "@spt-aki/models/enums/MessageType"; import { NotificationEventType } from "@spt-aki/models/enums/NotificationEventType"; import { SaveServer } from "@spt-aki/servers/SaveServer"; -import { WebSocketServer } from "@spt-aki/servers/WebSocketServer"; +import { AkiWebSocketConnectionHandler } from "@spt-aki/servers/ws/AkiWebSocketConnectionHandler"; import { NotificationService } from "@spt-aki/services/NotificationService"; import { HashUtil } from "@spt-aki/utils/HashUtil"; @@ -14,7 +14,7 @@ import { HashUtil } from "@spt-aki/utils/HashUtil"; export class NotificationSendHelper { constructor( - @inject("WebSocketServer") protected webSocketServer: WebSocketServer, + @inject("AkiWebSocketConnectionHandler") protected akiWebSocketConnection: AkiWebSocketConnectionHandler, @inject("HashUtil") protected hashUtil: HashUtil, @inject("SaveServer") protected saveServer: SaveServer, @inject("NotificationService") protected notificationService: NotificationService, @@ -28,9 +28,9 @@ export class NotificationSendHelper */ public sendMessage(sessionID: string, notificationMessage: IWsNotificationEvent): void { - if (this.webSocketServer.isConnectionWebSocket(sessionID)) + if (this.akiWebSocketConnection.isConnectionWebSocket(sessionID)) { - this.webSocketServer.sendMessage(sessionID, notificationMessage); + this.akiWebSocketConnection.sendMessage(sessionID, notificationMessage); } else { diff --git a/project/src/servers/WebSocketServer.ts b/project/src/servers/WebSocketServer.ts index 73dc55a1..004ae6ca 100644 --- a/project/src/servers/WebSocketServer.ts +++ b/project/src/servers/WebSocketServer.ts @@ -1,54 +1,37 @@ import http, { IncomingMessage } from "node:http"; -import { inject, injectable } from "tsyringe"; -import WebSocket from "ws"; +import { inject, injectAll, injectable } from "tsyringe"; +import { WebSocket, Server } from "ws"; import { HttpServerHelper } from "@spt-aki/helpers/HttpServerHelper"; -import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; -import { IWsNotificationEvent } from "@spt-aki/models/eft/ws/IWsNotificationEvent"; -import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; -import { NotificationEventType } from "@spt-aki/models/enums/NotificationEventType"; -import { IHttpConfig } from "@spt-aki/models/spt/config/IHttpConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; -import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { RandomUtil } from "@spt-aki/utils/RandomUtil"; +import { IWebSocketConnectionHandler } from "./ws/IWebSocketConnectionHandler"; @injectable() export class WebSocketServer { + protected webSocketServer: Server; + constructor( @inject("WinstonLogger") protected logger: ILogger, @inject("RandomUtil") protected randomUtil: RandomUtil, - @inject("ConfigServer") protected configServer: ConfigServer, - @inject("LocalisationService") protected localisationService: LocalisationService, @inject("JsonUtil") protected jsonUtil: JsonUtil, + @inject("LocalisationService") protected localisationService: LocalisationService, @inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper, - @inject("ProfileHelper") protected profileHelper: ProfileHelper, + @injectAll("WebSocketConnectionHandler") protected webSocketConnectionHandlers: IWebSocketConnectionHandler[], ) { - this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP); } - protected httpConfig: IHttpConfig; - protected defaultNotification: IWsNotificationEvent = { type: NotificationEventType.PING, eventId: "ping" }; - - protected webSocketServer: WebSocket.Server; - protected webSockets: Record = {}; - protected websocketPingHandler = null; - - public getWebSocketServer(): WebSocket.Server + public getWebSocketServer(): Server { return this.webSocketServer; } - public getSessionWebSocket(sessionID: string): WebSocket.WebSocket - { - return this.webSockets[sessionID]; - } - public setupWebSocket(httpServer: http.Server): void { - this.webSocketServer = new WebSocket.Server({ server: httpServer }); + this.webSocketServer = new Server({ server: httpServer }); this.webSocketServer.addListener("listening", () => { @@ -63,26 +46,6 @@ export class WebSocketServer this.webSocketServer.addListener("connection", this.wsOnConnection.bind(this)); } - public sendMessage(sessionID: string, output: IWsNotificationEvent): void - { - try - { - if (this.isConnectionWebSocket(sessionID)) - { - this.webSockets[sessionID].send(this.jsonUtil.serialize(output)); - this.logger.debug(this.localisationService.getText("websocket-message_sent")); - } - else - { - this.logger.debug(this.localisationService.getText("websocket-not_ready_message_not_sent", sessionID)); - } - } - catch (err) - { - this.logger.error(this.localisationService.getText("websocket-message_send_failed_with_error", err)); - } - } - protected getRandomisedMessage(): string { if (this.randomUtil.getInt(1, 1000) > 999) @@ -95,49 +58,21 @@ export class WebSocketServer : this.localisationService.getText("server_start_success"); } - public isConnectionWebSocket(sessionID: string): boolean + protected wsOnConnection(ws: WebSocket, req: IncomingMessage): void { - return this.webSockets[sessionID] !== undefined && this.webSockets[sessionID].readyState === WebSocket.OPEN; - } - - protected wsOnConnection(ws: WebSocket.WebSocket, req: IncomingMessage): void - { - // Strip request and break it into sections - const splitUrl = req.url.substring(0, req.url.indexOf("?")).split("/"); - const sessionID = splitUrl.pop(); - const playerProfile = this.profileHelper.getFullProfile(sessionID); - const playerInfoText = `${playerProfile.info.username} (${sessionID})`; - - this.logger.info(this.localisationService.getText("websocket-player_connected", playerInfoText)); - - const logger = this.logger; - const msgToLog = this.localisationService.getText("websocket-received_message", playerInfoText); - ws.on("message", (msg) => + const socketHandlers = this.webSocketConnectionHandlers.filter((wsh) => req.url.includes(wsh.getHookUrl())); + if ((socketHandlers?.length ?? 0) === 0) { - logger.info(`${msgToLog} ${msg}`); - }); - - this.webSockets[sessionID] = ws; - - if (this.websocketPingHandler) - { - clearInterval(this.websocketPingHandler); + const message = `Socket connection received for url ${req.url}, but there is not websocket handler configured for it`; + this.logger.warning(message); + ws.send(this.jsonUtil.serialize({ error: message })); + ws.close(); + return; } - - this.websocketPingHandler = setInterval(() => + socketHandlers.forEach((wsh) => { - this.logger.debug(this.localisationService.getText("websocket-pinging_player", sessionID)); - - if (ws.readyState === WebSocket.OPEN) - { - ws.send(this.jsonUtil.serialize(this.defaultNotification)); - } - else - { - this.logger.debug(this.localisationService.getText("websocket-socket_lost_deleting_handle")); - clearInterval(this.websocketPingHandler); - delete this.webSockets[sessionID]; - } - }, this.httpConfig.webSocketPingDelayMs); + wsh.onConnection(ws, req); + this.logger.info(`WebSocketHandler "${wsh.getSocketId()}" connected`); + }); } } diff --git a/project/src/servers/ws/AkiWebSocketConnectionHandler.ts b/project/src/servers/ws/AkiWebSocketConnectionHandler.ts new file mode 100644 index 00000000..ca2caa61 --- /dev/null +++ b/project/src/servers/ws/AkiWebSocketConnectionHandler.ts @@ -0,0 +1,112 @@ +import { IncomingMessage } from "http"; +import { inject, injectAll, injectable } from "tsyringe"; +import { WebSocket } from "ws"; +import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; +import { IWsNotificationEvent } from "@spt-aki/models/eft/ws/IWsNotificationEvent"; +import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; +import { NotificationEventType } from "@spt-aki/models/enums/NotificationEventType"; +import { IHttpConfig } from "@spt-aki/models/spt/config/IHttpConfig"; +import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { ConfigServer } from "@spt-aki/servers/ConfigServer"; +import { IWebSocketConnectionHandler } from "@spt-aki/servers/ws/IWebSocketConnectionHandler"; +import { LocalisationService } from "@spt-aki/services/LocalisationService"; +import { JsonUtil } from "@spt-aki/utils/JsonUtil"; +import { IAkiWebSocketMessageHandler } from "./message/IAkiWebSocketMessageHandler"; + +@injectable() +export class AkiWebSocketConnectionHandler implements IWebSocketConnectionHandler +{ + protected httpConfig: IHttpConfig; + protected webSockets: Map = new Map(); + protected defaultNotification: IWsNotificationEvent = { type: NotificationEventType.PING, eventId: "ping" }; + + protected websocketPingHandler = null; + constructor( + @inject("WinstonLogger") protected logger: ILogger, + @inject("ProfileHelper") protected profileHelper: ProfileHelper, + @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("ConfigServer") protected configServer: ConfigServer, + @inject("JsonUtil") protected jsonUtil: JsonUtil, + @injectAll("AkiWebSocketMessageHandler") protected akiWebSocketMessageHandlers: IAkiWebSocketMessageHandler[], + ) + { + this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP); + } + + public getSocketId(): string + { + return "AKI WebSocket Handler"; + } + + public getHookUrl(): string + { + return "/notifierServer/getwebsocket/"; + } + + public onConnection(ws: WebSocket, req: IncomingMessage): void + { + // Strip request and break it into sections + const splitUrl = req.url.substring(0, req.url.indexOf("?")).split("/"); + const sessionID = splitUrl.pop(); + const playerProfile = this.profileHelper.getFullProfile(sessionID); + const playerInfoText = `${playerProfile.info.username} (${sessionID})`; + + this.logger.info(this.localisationService.getText("websocket-player_connected", playerInfoText)); + + // throw new Error("Method not implemented."); + this.webSockets.set(sessionID, ws); + + if (this.websocketPingHandler) + { + clearInterval(this.websocketPingHandler); + } + + ws.on("message", (msg) => this.akiWebSocketMessageHandlers.forEach((wsmh) => wsmh.onAkiMessage(sessionID, this.webSockets.get(sessionID), msg))); + + this.websocketPingHandler = setInterval(() => + { + this.logger.debug(this.localisationService.getText("websocket-pinging_player", sessionID)); + + if (ws.readyState === WebSocket.OPEN) + { + ws.send(this.jsonUtil.serialize(this.defaultNotification)); + } + else + { + this.logger.debug(this.localisationService.getText("websocket-socket_lost_deleting_handle")); + clearInterval(this.websocketPingHandler); + this.webSockets.delete(sessionID); + } + }, this.httpConfig.webSocketPingDelayMs); + } + + public sendMessage(sessionID: string, output: IWsNotificationEvent): void + { + try + { + if (this.isConnectionWebSocket(sessionID)) + { + this.webSockets.get(sessionID).send(this.jsonUtil.serialize(output)); + this.logger.debug(this.localisationService.getText("websocket-message_sent")); + } + else + { + this.logger.debug(this.localisationService.getText("websocket-not_ready_message_not_sent", sessionID)); + } + } + catch (err) + { + this.logger.error(this.localisationService.getText("websocket-message_send_failed_with_error", err)); + } + } + + public isConnectionWebSocket(sessionID: string): boolean + { + return this.webSockets.has(sessionID) && this.webSockets.get(sessionID).readyState === WebSocket.OPEN; + } + + public getSessionWebSocket(sessionID: string): WebSocket + { + return this.webSockets[sessionID]; + } +} diff --git a/project/src/servers/ws/IWebSocketConnectionHandler.ts b/project/src/servers/ws/IWebSocketConnectionHandler.ts new file mode 100644 index 00000000..82afa91f --- /dev/null +++ b/project/src/servers/ws/IWebSocketConnectionHandler.ts @@ -0,0 +1,9 @@ +import { IncomingMessage } from "node:http"; +import { WebSocket } from "ws"; + +export interface IWebSocketConnectionHandler +{ + getSocketId(): string + getHookUrl(): string + onConnection(ws: WebSocket, req: IncomingMessage): void +} diff --git a/project/src/servers/ws/message/DefaultAkiWebSocketMessageHandler.ts b/project/src/servers/ws/message/DefaultAkiWebSocketMessageHandler.ts new file mode 100644 index 00000000..0b8b4f2b --- /dev/null +++ b/project/src/servers/ws/message/DefaultAkiWebSocketMessageHandler.ts @@ -0,0 +1,16 @@ +import { inject, injectable } from "tsyringe"; +import { RawData, WebSocket } from "ws"; +import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; +import { IAkiWebSocketMessageHandler } from "@spt-aki/servers/ws/message/IAkiWebSocketMessageHandler"; + +@injectable() +export class DefaultAkiWebSocketMessageHandler implements IAkiWebSocketMessageHandler +{ + constructor(@inject("WinstonLogger") protected logger: ILogger) + {} + + public onAkiMessage(sessionId: string, client: WebSocket, message: RawData): void + { + this.logger.debug(`[${sessionId}] AKI message received: ${message}`); + } +} diff --git a/project/src/servers/ws/message/IAkiWebSocketMessageHandler.ts b/project/src/servers/ws/message/IAkiWebSocketMessageHandler.ts new file mode 100644 index 00000000..b766c117 --- /dev/null +++ b/project/src/servers/ws/message/IAkiWebSocketMessageHandler.ts @@ -0,0 +1,6 @@ +import { RawData, WebSocket } from "ws"; + +export interface IAkiWebSocketMessageHandler +{ + onAkiMessage(sessionID: string, client: WebSocket, message: RawData): void +} diff --git a/project/src/services/LocaleService.ts b/project/src/services/LocaleService.ts index 922bf746..1b6c0bca 100644 --- a/project/src/services/LocaleService.ts +++ b/project/src/services/LocaleService.ts @@ -1,7 +1,6 @@ import { inject, injectable } from "tsyringe"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ILocaleConfig } from "@spt-aki/models/spt/config/ILocaleConfig"; -import { ILocaleBase } from "@spt-aki/models/spt/server/ILocaleBase"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; @@ -13,7 +12,6 @@ import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; export class LocaleService { protected localeConfig: ILocaleConfig; - protected localesTable: ILocaleBase; constructor( @inject("WinstonLogger") protected logger: ILogger, @@ -22,7 +20,6 @@ export class LocaleService ) { this.localeConfig = this.configServer.getConfig(ConfigTypes.LOCALE); - this.localesTable = this.databaseServer.getTables().locales; } /** @@ -31,7 +28,7 @@ export class LocaleService */ public getLocaleDb(): Record { - const desiredLocale = this.localesTable.global[this.getDesiredGameLocale()]; + const desiredLocale = this.databaseServer.getTables().locales.global[this.getDesiredGameLocale()]; if (desiredLocale) { return desiredLocale; @@ -41,7 +38,7 @@ export class LocaleService `Unable to find desired locale file using locale: ${this.getDesiredGameLocale()} from config/locale.json, falling back to 'en'`, ); - return this.localesTable.global.en; + return this.databaseServer.getTables().locales.global.en; } /** @@ -138,19 +135,19 @@ export class LocaleService } const baseNameCode = platformLocale.baseName?.toLocaleLowerCase(); - if (baseNameCode && this.localesTable.global[baseNameCode]) + if (baseNameCode && this.databaseServer.getTables().locales.global[baseNameCode]) { return baseNameCode; } const languageCode = platformLocale.language?.toLowerCase(); - if (languageCode && this.localesTable.global[languageCode]) + if (languageCode && this.databaseServer.getTables().locales.global[languageCode]) { return languageCode; } const regionCode = platformLocale.region?.toLocaleLowerCase(); - if (regionCode && this.localesTable.global[regionCode]) + if (regionCode && this.databaseServer.getTables().locales.global[regionCode]) { return regionCode; }