import UsersManager from "../Users/Users"; import CompaniesManager from "../Companies/Companies"; import { NoPermissionMessage } from "../../helpers/NoPermissionMessage"; import EmployeesList from "../Employees/Employee"; import { EmployeeRoles } from "../Employees/Employees.Interface"; import { WapServerAdapterMongoose } from "./WapServer.Adapter.Mongoose"; import { AssingAvailableServerParams, BotCheckSession, BotCheckStatus, BotEventParams, BotEventError, BotView, IWapServer, IWapServerManager, SysAdminDeleteWapServerParams, SysAdminDeleteWapServerResult, SysAdminWapContainerDto, SysAdminWapServerAuditParams, SysAdminWapServerAuditResult, SysAdminWapServerRecalculateCountParams, SysAdminWapServerRecalculateCountResult, SysAdminWapServerDetachResult, SysAdminWapServerDeleteBotResult, SysAdminWapServerOrganizationActionParams, SysAdminWapServerQrResult, SendBotMessageParams, ValidateBotEventStatus, BOT_STATE, SERVER_STATE, BOT_SESSION_STATUS, CreateWapServerParams, VALIDATE_BOT_ENVENT_ERRORS, PaginateWapServerResults, PaginateWapServerParams, UpdateWapServerParams, } from "./WapServer.Interface"; import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons"; import { PlanFeatures } from "../Plans/Plans.interface"; import axios, { AxiosError } from "axios"; import { isNull } from "../../helpers/IsNull"; const BOT_ADMIN_ACTION_TIMEOUT_MS = 10000; const BOT_ADMIN_RESTART_STATUS_RETRIES = 5; const BOT_ADMIN_RESTART_STATUS_DELAY_MS = 1000; const MONGO_OBJECT_ID_REGEX = /^[a-fA-F0-9]{24}$/; const WAP_SERVER_METADATA_HOSTS = new Set(["169.254.169.254", "metadata.google.internal"]); export interface CreateBotResult { organizationId: string; port: number; error: string; message: string; } class WapServerManager implements IWapServerManager { servers: WapServerAdapterMongoose; constructor() { this.servers = new WapServerAdapterMongoose(); } public async botView(data: AssingAvailableServerParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error("El usuario no es valido."); } const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) { throw new Error(NoPermissionMessage()); } const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { switch (validationStatus.code) { case VALIDATE_BOT_ENVENT_ERRORS.NO_SERVER_ASSIGNED: return { runningState: "dead", serverId: undefined, serverName: undefined, serverDescription: undefined, serverState: SERVER_STATE.NOT_ASSIGNED, exists: false, automaticNotifications: isNull(companyCheck.automaticNotifications, false), }; default: throw new Error(validationStatus.error); } } if (!companyCheck.wapServerId) { return { runningState: "dead", serverId: undefined, serverName: undefined, serverDescription: undefined, serverState: SERVER_STATE.NOT_WORKING, exists: false, automaticNotifications: isNull(companyCheck.automaticNotifications, false), }; } const server = await this.servers.serverList.findOne({ _id: companyCheck.wapServerId }); if (server) { try { const chkStatus = await this.checkBot({ sessionUser: data.sessionUser, companyId: data.companyId, }); let botSession = BOT_SESSION_STATUS.UNLOGED; if (chkStatus.status.indexOf("Up") >= 0) { botSession = (await this.checkSession(data)).session; } return { serverId: server.id, serverName: server.name, serverDescription: server.description, serverState: chkStatus.serverState, exists: chkStatus.state == BOT_STATE.EXISTS ? true : false, runningState: chkStatus.status, botSession: botSession, automaticNotifications: isNull(companyCheck.automaticNotifications, false), } as BotView; } catch (error) { return { serverId: server.id, serverName: server.name, serverDescription: server.description, serverState: SERVER_STATE.NOT_WORKING, exists: false, runningState: "dead", automaticNotifications: isNull(companyCheck.automaticNotifications, false), }; } } return { serverId: undefined, serverName: undefined, serverDescription: undefined, exists: false, runningState: "dead", automaticNotifications: isNull(companyCheck.automaticNotifications, false), }; } public async findServers(): Promise { const servidores = await this.servers.find({}); return servidores; } private normalizeBotAdminApiUrl(url: string): string { const rawUrl = String(url).trim(); if (!rawUrl) { throw new Error("URL is required"); } if (/\s|[\x00-\x1F\x7F]/.test(rawUrl)) { throw new Error("URL must not contain whitespace or control characters"); } const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(rawUrl); const normalizedInput = hasProtocol ? rawUrl : `http://${rawUrl}`; let parsedUrl: URL; try { parsedUrl = new URL(normalizedInput); } catch (error) { throw new Error("URL must be a valid HTTP(S) origin"); } if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { throw new Error("URL protocol must be http or https"); } if (!parsedUrl.hostname) { throw new Error("URL host is required"); } if (parsedUrl.username || parsedUrl.password) { throw new Error("URL must not include credentials"); } const path = decodeURIComponent(parsedUrl.pathname); if ((parsedUrl.pathname && parsedUrl.pathname !== "/") || path.includes("..")) { throw new Error("URL path is not allowed; use only the origin"); } if (parsedUrl.search || parsedUrl.hash) { throw new Error("URL must not include query string or hash"); } if (WAP_SERVER_METADATA_HOSTS.has(parsedUrl.hostname.toLowerCase())) { throw new Error("URL host is not allowed"); } return `${parsedUrl.origin}/`; } private getBotAdminApiUrl(url: string): string { //return `https://${url}:${process.env.DEFAULT_BOT_ADMIN_PORT}/`; //return `https://${url}/`; return this.normalizeBotAdminApiUrl(url); } private formatResponseBody(data: unknown): string { if (data === undefined || data === null) { return "empty body"; } const body = typeof data === "string" ? data : JSON.stringify(data); return body.length > 1000 ? `${body.slice(0, 1000)}...` : body; } private createBotAdminContainersError(error: unknown, targetUrl: string): Error { if (axios.isAxiosError(error)) { const status = error.response?.status; const body = this.formatResponseBody(error.response?.data); const statusText = status ? `status ${status}` : error.code || "no response"; return new Error(`Bot-admin containers request failed for ${targetUrl}: ${statusText}; body: ${body}`); } if (error instanceof Error && error.message) { return new Error(`Bot-admin containers request failed for ${targetUrl}: ${error.message}`); } return new Error(`Bot-admin containers request failed for ${targetUrl}: unknown error`); } private createBotAdminActionError(error: unknown, targetUrl: string): Error { if (axios.isAxiosError(error)) { const status = error.response?.status; const body = this.formatResponseBody(error.response?.data); const statusText = status ? `status ${status}` : error.code || "no response"; return new Error(`Bot-admin request failed for ${targetUrl}: ${statusText}; body: ${body}`); } if (error instanceof Error && error.message) { return new Error(`Bot-admin request failed for ${targetUrl}: ${error.message}`); } return new Error(`Bot-admin request failed for ${targetUrl}: unknown error`); } private isBotAdminMissingContainerError(error: unknown): boolean { if (!axios.isAxiosError(error)) { return false; } const status = error.response?.status; const body = this.formatResponseBody(error.response?.data).toLowerCase(); // Be conservative: only allow detach to continue for explicit missing-container signals. return status === 404 || body.includes("not found") || body.includes("no such container"); } private async getSysAdminServerActionTarget(data: SysAdminWapServerOrganizationActionParams): Promise<{ server: any; baseUrl: string; encodedOrganizationId: string; }> { if (!data.serverId) { throw new Error("Server id is required"); } if (!data.organizationId) { throw new Error("Organization id is required"); } if (!MONGO_OBJECT_ID_REGEX.test(data.organizationId)) { throw new Error("Organization id must be a valid Mongo ObjectId"); } const server = await this.servers.serverList.findOne({ _id: data.serverId }); if (!server) { throw new Error("WAP server not found"); } return { server, baseUrl: this.getBotAdminApiUrl(server.ipv4), encodedOrganizationId: encodeURIComponent(data.organizationId) }; } private async postSysAdminBotAction( data: SysAdminWapServerOrganizationActionParams, action: "start" | "stop", ): Promise { const { baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); const targetUrl = `${baseUrl}containers/${encodedOrganizationId}/${action}`; try { const response = await axios.post(targetUrl, {}, { headers: { "Content-Type": "application/json" }, timeout: BOT_ADMIN_ACTION_TIMEOUT_MS, }); const result: CreateBotResult = response.data; if (result.error) { throw new Error(result.error); } } catch (error) { throw this.createBotAdminActionError(error, targetUrl); } } private async deleteSysAdminRuntimeBot( baseUrl: string, encodedOrganizationId: string, allowMissingContainer = false, ): Promise<{ botDeleted: boolean; botAlreadyMissing: boolean }> { const targetUrl = `${baseUrl}containers/${encodedOrganizationId}`; try { const response = await axios.delete(targetUrl, { timeout: BOT_ADMIN_ACTION_TIMEOUT_MS }); const result: CreateBotResult = response.data; if (result.error) { throw new Error(result.error); } return { botDeleted: true, botAlreadyMissing: false }; } catch (error) { if (allowMissingContainer && this.isBotAdminMissingContainerError(error)) { return { botDeleted: false, botAlreadyMissing: true }; } throw this.createBotAdminActionError(error, targetUrl); } } private async waitForSysAdminBotStopped(data: SysAdminWapServerOrganizationActionParams): Promise { const { baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); const targetUrl = `${baseUrl}containers/${encodedOrganizationId}/status`; for (let attempt = 0; attempt < BOT_ADMIN_RESTART_STATUS_RETRIES; attempt += 1) { try { const response = await axios.post(targetUrl, {}, { timeout: BOT_ADMIN_ACTION_TIMEOUT_MS }); const status = String(response.data?.status || "").toLowerCase(); if (!status.includes("up")) { return; } } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 500) { return; } throw this.createBotAdminActionError(error, targetUrl); } await new Promise((resolve) => setTimeout(resolve, BOT_ADMIN_RESTART_STATUS_DELAY_MS)); } throw new Error("Bot did not stop before restart timeout; start was not attempted."); } private extractOrganizationIdFromContainer(container: any): string | undefined { const names = Array.isArray(container?.Names) ? container.Names : []; for (const rawName of names) { const name = String(rawName || "").replace(/^\//, ""); const match = name.match(/^(.+)_bot_\d+$/); if (match && match[1]) { return match[1]; } } return undefined; } private sanitizeContainer(container: any): SysAdminWapContainerDto { const names = Array.isArray(container?.Names) ? container.Names : []; const firstName = names.length > 0 ? String(names[0]).replace(/^\//, "") : undefined; return { id: container?.Id, name: firstName, organizationId: this.extractOrganizationIdFromContainer(container), image: container?.Image, state: container?.State, status: container?.Status, ports: Array.isArray(container?.Ports) ? container.Ports : [], created: container?.Created, }; } private async getSysAdminRuntimeContainers(server: any): Promise { const containersUrl = this.getBotAdminApiUrl(server.ipv4) + "containers"; try { const response = await axios.get(containersUrl, { headers: { "Content-Type": "application/json" }, timeout: BOT_ADMIN_ACTION_TIMEOUT_MS, }); return Array.isArray(response.data) ? response.data.map((container: any) => this.sanitizeContainer(container)) : []; } catch (error) { throw this.createBotAdminContainersError(error, containersUrl); } } public async getAvailableServer(): Promise { const servidores = await this.servers.find({ active: true }); if (servidores.length == 0) { throw new Error("No se encontraron servidores disponibles"); } const servidoresConCapacidad = servidores.filter((s) => { return s.countBots < s.maxBots; }); if (servidoresConCapacidad.length == 0) { throw new Error("No se encontraron servidores disponibles"); } const returnServer = servidoresConCapacidad.sort((a, b) => a.countBots - b.countBots).shift(); if (!returnServer) { throw new Error("No se encontraron servidores disponibles"); } return returnServer; } public async quitServer(data: AssingAvailableServerParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción"); } //Validar que la organizacion exista. const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } //Validar que el usuario sea el administrador de la organizacion. if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) { throw new Error(NoPermissionMessage()); } //Descontar 1 al contador de bots del servidor. if (companyCheck && companyCheck.wapServerId) { const server = await this.servers.serverList.findOne({ _id: companyCheck.wapServerId, }); if (server) { server.countBots = server.countBots - 1; if (server.countBots < 0) { server.countBots = 0; } await server.save(); } } companyCheck.wapServerId = undefined; await companyCheck.save(); } public async assignAvailableServer(data: AssingAvailableServerParams): Promise { const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser }); if (!sessionUser) { throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción"); } //Validar que la organizacion exista. const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId }); if (!companyCheck) { throw new Error("La compañia no existe"); } //Validar que el usuario sea el administrador de la organizacion. if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) { throw new Error(NoPermissionMessage()); } //Obtener el id del usuario owner de la organizacion. const organizationOwner = companyCheck.ownerId; //Validar que el owner de la organizacion tenga un plan que permita bots. if ( !PlanSubscriptionsList.checkFeature({ userId: organizationOwner, feature: PlanFeatures.HAS_BOT, }) ) { throw new Error("El plan de la organizacion no permite bots."); } //Validar que el plan este activo. if (!PlanSubscriptionsList.isSubscriptionActive({ sessionUser: organizationOwner })) { throw new Error("La organizacion no tiene una suscripcion activa."); } //Validar que la organizacion no tenga ningun servidor asignado. let organizationServerId = companyCheck.wapServerId; if (organizationServerId) { throw new Error("La organizacion ya tiene un servidor asignado."); } //Obtener el servidor con menos bots activos. const availableServer = await this.getAvailableServer(); if (!availableServer) { throw new Error("No se encontraron servidores disponibles."); } const server = await this.servers.serverList.findOne({ _id: availableServer.id }); if (!server) { throw new Error("No se encontro el servidor"); } //Guardar en la coleccion Companies los datos del servidor asignado. await CompaniesManager.updateCompany({ wapServerId: availableServer.id, id: data.companyId, sessionUser: data.sessionUser, }); //Incrementear la cantidad de bots en el servidor elegido. server.countBots = server.countBots + 1; await server.save(); return server; } private async validateBotEvent( sessionUserId: string, companyId?: string, ): Promise { if (!sessionUserId) { return { status: false, error: "No se ha encontrado el usuario", code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_USER, }; } const sessionUser = await UsersManager.users.findOne({ _id: sessionUserId }); if (!sessionUser) { return { status: false, error: "No se ha encontrado el usuario o no tiene permisos para realizar esta acción", code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_USER, }; } if (!companyId) { return { status: false, error: "No se ha encontrado la compañia", code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_ORG, }; } //Validar que la organizacion exista. const companyCheck = await CompaniesManager.companies.findOne({ _id: companyId }); if (!companyCheck) { return { status: false, error: "La compañia no existe", code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_ORG, }; } //Validar que el usuario sea el administrador de la organizacion. if (!(await EmployeesList.checkPermission(companyCheck, sessionUserId, EmployeeRoles.ADMIN))) { return { status: false, error: NoPermissionMessage(), code: VALIDATE_BOT_ENVENT_ERRORS.NO_PERMISSION, }; } //Obtener el id del usuario owner de la organizacion. const organizationOwner = companyCheck.ownerId; const checkFeature = await PlanSubscriptionsList.checkFeature({ userId: organizationOwner, feature: PlanFeatures.WAP_NOTIFICATIONS, }); //Validar que el owner de la organizacion tenga un plan que permita bots. if (!checkFeature) { const serverAssigned = await this.servers.serverList.findOne({ _id: companyCheck.wapServerId, }); if (!serverAssigned) { return { status: false, error: "No se ha asignado un servidor a la compañia", code: VALIDATE_BOT_ENVENT_ERRORS.NO_SERVER_ASSIGNED, }; } return { status: false, error: "El plan de la organizacion no permite bots.", code: VALIDATE_BOT_ENVENT_ERRORS.NO_BOT_ALLOWED, serverUrl: serverAssigned.ipv4, }; } //Validar que el plan este activo. const checkSubscriptionActive = await PlanSubscriptionsList.isSubscriptionActive({ sessionUser: organizationOwner, }); if (!checkSubscriptionActive) { return { status: false, error: "La organizacion no tiene una suscripcion activa.", code: VALIDATE_BOT_ENVENT_ERRORS.NO_SUBSCRIPTION_ACTIVE, }; } const serverAssigned = await this.servers.serverList.findOne({ _id: companyCheck.wapServerId, }); if (!serverAssigned) { return { status: false, error: "No se ha asignado un servidor a la compañia", code: VALIDATE_BOT_ENVENT_ERRORS.NO_SERVER_ASSIGNED, }; } return { status: true, error: "", serverUrl: serverAssigned.ipv4, }; } public async createBot(data: BotEventParams): Promise { const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { throw new Error(validationStatus.error); } const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const dataApi = { organizationId: data.companyId, }; const response = await axios.post(this.getBotAdminApiUrl(url) + "containers/create", dataApi, { headers: { "Content-Type": "application/json", }, }); const result: CreateBotResult = response.data; if (result.error) { throw new Error(result.error); } //Podria guardar el puerto asignado al bot. //Pero en realidad no hace falta, ya que conociendo el servidor //Donde se ejecuta el bot puedo averiguar el puerto si el que se encarga //de enviar el mensaje de wap es el mismo bot admin. //La idea seria que el server de tx le envia un post al bot admin para enviar un mensaje de wap y luego //el bot admin averigua cual es el puerto donde se esta ejecutando el bot y envia la solicitud. //De esta manera nunca hay una conexion directa entre la api principal (expuesta) y el bot. //Por lo tanto por el momento solo retorno sin novedades... return; } catch (error) { console.log(error); throw new Error("Ha ocurrido un error al crear el bot."); } } public async startBot(data: BotEventParams): Promise { const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { throw new Error(validationStatus.error); } const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const dataApi = {}; const response = await axios.post( this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/start", dataApi, { headers: { "Content-Type": "application/json", }, }, ); const result: CreateBotResult = response.data; if (result.error) { throw new Error(result.error); } return; } catch (error) { if (error instanceof AxiosError) { const axiosError = error as AxiosError; if (axiosError.response) { const responseError = axiosError.response; console.log(responseError.data); } } throw new Error("Ha ocurrido un error al iniciar el bot."); } } public async stopBot(data: BotEventParams): Promise { const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { throw new Error(validationStatus.error); } const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const dataApi = {}; const response = await axios.post( this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/stop", dataApi, { headers: { "Content-Type": "application/json", }, }, ); const result: CreateBotResult = response.data; if (result.error) { throw new Error(result.error); } return; } catch (error) { console.log(error); throw new Error("Ha ocurrido un error al detener el bot."); } } public async deleteBot(data: BotEventParams): Promise { //Como quiere borrarlo no voy a validar si tiene una suscripcion activa. const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); /*if (!validationStatus.status) { throw new Error(validationStatus.error); }*/ const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const response = await axios.delete(this.getBotAdminApiUrl(url) + "containers/" + data.companyId); const result: CreateBotResult = response.data; if (result.error) { throw new Error(result.error); } return; } catch (error) { throw new Error("Ha ocurrido un error al detener el bot."); } } public async checkBot(data: BotEventParams): Promise { const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { throw new Error(validationStatus.error); } const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const dataApi = {}; const response = await axios.post( this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/status", dataApi, { headers: { "Content-Type": "application/json", }, }, ); return response.data as BotCheckStatus; } catch (error) { if (error instanceof AxiosError) { const errorAxios: AxiosError = error; if (errorAxios.response) { const errorData = errorAxios.response.data as BotEventError; if (errorData.code == 404) { return { state: BOT_STATE.NOT_EXISTS, status: "dead", serverState: SERVER_STATE.RUNNING, }; } else { throw new Error("Ha ocurrido un error al obtener el estado del bot."); } } else { //no hay comunicacion con el bot-api-admin. return { state: BOT_STATE.NOT_EXISTS, status: "dead", serverState: SERVER_STATE.NOT_WORKING, }; throw new Error("Ha ocurrido un error al obtener el estado del bot."); } } else { throw new Error("Ha ocurrido un error al obtener el estado del bot."); } } } public async checkSession(data: BotEventParams): Promise { const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { throw new Error(validationStatus.error); } const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const dataApi = {}; const response = await axios.post( this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/session", dataApi, { headers: { "Content-Type": "application/json", }, }, ); if (response.data.message == "ok") { return { session: BOT_SESSION_STATUS.LOGED, }; } return { session: BOT_SESSION_STATUS.UNLOGED, }; } catch (error) { console.log(error); throw new Error("Ha ocurrido un error al obtener el estado del bot."); } } public async sendMessage(data: SendBotMessageParams): Promise { const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { throw new Error(validationStatus.error); } const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const dataApi = { number: data.number, message: data.message, }; const response = await axios.post( this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/send", dataApi, { headers: { "Content-Type": "application/json", }, }, ); const result: CreateBotResult = response.data; if (result.error) { throw new Error(result.error); } return; } catch (error) { throw new Error("Ha ocurrido un error al enviar el mensaje."); } } public async getQr(data: BotEventParams): Promise { const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId); if (!validationStatus.status) { throw new Error(validationStatus.error); } const url = validationStatus.serverUrl; if (!url) { throw new Error("No se ha asignado un servidor a la organizacion"); } try { const response = await axios.get( this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/qr", { responseType: "arraybuffer" }, ); const base64Image = Buffer.from(response.data, "binary").toString("base64"); return base64Image; } catch (error) { throw new Error("Ha ocurrido un error al obtener el estado del bot."); } } public async sysAdminGetWapServers(data: PaginateWapServerParams): Promise { const servers = await this.servers.paginate(data); return servers; } public async sysAdminCreateWapServer(data: CreateWapServerParams): Promise { if (!data.name || !String(data.name).trim()) { throw new Error("Name is required"); } if (!data.ipv4 || !String(data.ipv4).trim()) { throw new Error("IPv4 is required"); } if (!Number.isFinite(data.port) || data.port < 0) { throw new Error("Port must be a finite non-negative number"); } if (!Number.isFinite(data.maxBots) || data.maxBots < 0) { throw new Error("Max bots must be a finite non-negative number"); } return await this.servers.create({ name: String(data.name).trim(), description: data.description ? String(data.description).trim() : "", ipv4: this.normalizeBotAdminApiUrl(data.ipv4), ipv6: data.ipv6 ? String(data.ipv6).trim() : "", countBots: 0, port: data.port, maxBots: data.maxBots, active: data.active === undefined ? true : Boolean(data.active), } as CreateWapServerParams & { countBots: number }); } public async sysAdminUpdateWapServer(data: UpdateWapServerParams): Promise { if (!data.serverId) { throw new Error("Server id is required"); } if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { throw new Error("Server id must be a valid Mongo ObjectId"); } const server = await this.servers.serverList.findOne({ _id: data.serverId }); if (!server) { throw new Error("WAP server not found"); } if (data.name !== undefined) { server.name = String(data.name).trim(); } if (data.ipv4 !== undefined) { server.ipv4 = this.normalizeBotAdminApiUrl(data.ipv4); } if (data.port !== undefined) { if (!Number.isFinite(data.port) || data.port < 0) { throw new Error("Port must be a finite non-negative number"); } server.port = data.port; } if (data.maxBots !== undefined) { if (!Number.isFinite(data.maxBots) || data.maxBots < 0) { throw new Error("Max bots must be a finite non-negative number"); } server.maxBots = data.maxBots; } if (data.active !== undefined) { server.active = Boolean(data.active); } await server.save(); return server; } public async sysAdminAuditWapServer(data: SysAdminWapServerAuditParams): Promise { if (!data.serverId) { throw new Error("Server id is required"); } if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { throw new Error("Server id must be a valid Mongo ObjectId"); } const server = await this.servers.serverList.findOne({ _id: data.serverId }); if (!server) { throw new Error("WAP server not found"); } const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); const assignedOrganizations = assignedCompanies.map((company: any) => ({ id: String(company.id || company._id), name: company.name, })); const assignedIds = new Set(assignedOrganizations.map((organization) => organization.id)); const detectedBots = await this.getSysAdminRuntimeContainers(server); const runtimeOrganizationIds = new Set( detectedBots .map((container) => container.organizationId) .filter((organizationId): organizationId is string => Boolean(organizationId)), ); const validBots = detectedBots.filter( (container) => container.organizationId && assignedIds.has(container.organizationId), ); const ghostBots = detectedBots.filter( (container) => !container.organizationId || !assignedIds.has(container.organizationId), ); const missingBots = assignedOrganizations.filter( (organization) => !runtimeOrganizationIds.has(organization.id), ); const expectedCountBots = assignedOrganizations.length; const storedCountBots = server.countBots ?? 0; const runtimeCountBots = detectedBots.length; return { serverId: String(server.id || server._id), assignedOrganizations, detectedBots, validBots, ghostBots, missingBots, expectedCountBots, storedCountBots, runtimeCountBots, countMismatch: storedCountBots !== expectedCountBots || runtimeCountBots !== expectedCountBots, }; } public async sysAdminRecalculateWapServerCount( data: SysAdminWapServerRecalculateCountParams, ): Promise { if (!data.serverId) { throw new Error("Server id is required"); } if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { throw new Error("Server id must be a valid Mongo ObjectId"); } const server = await this.servers.serverList.findOne({ _id: data.serverId }); if (!server) { throw new Error("WAP server not found"); } const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); const before = server.countBots; const after = assignedCompanies.length; server.countBots = after; await server.save(); return { serverId: String(server.id || server._id), before, after }; } public async sysAdminDeleteWapServer(data: SysAdminDeleteWapServerParams): Promise { if (!data.serverId) { throw new Error("Server id is required"); } if (!MONGO_OBJECT_ID_REGEX.test(data.serverId)) { throw new Error("Server id must be a valid Mongo ObjectId"); } const server = await this.servers.serverList.findOne({ _id: data.serverId }); if (!server) { throw new Error("WAP server not found"); } const force = data.force === true; if (!force && server.active) { throw new Error("WAP server must be inactive before deletion"); } const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); if (!force && assignedCompanies.length > 0) { throw new Error(`WAP server cannot be deleted because it has ${assignedCompanies.length} associated bot(s) in DB`); } let detectedBots: SysAdminWapContainerDto[] = []; let auditFailed = false; let auditMessage: string | undefined; try { detectedBots = await this.getSysAdminRuntimeContainers(server); } catch (error) { const message = error instanceof Error && error.message ? error.message : "unknown error"; if (!force) { throw new Error(`WAP server cannot be deleted because bot-admin could not be audited. Verify runtime bots manually before deleting. ${message}`); } auditFailed = true; auditMessage = message; } if (!force && detectedBots.length > 0) { throw new Error(`WAP server cannot be deleted because bot-admin detected ${detectedBots.length} runtime bot(s)`); } let deletedRuntimeBots = 0; let skippedRuntimeBots = 0; if (force) { const baseUrl = this.getBotAdminApiUrl(server.ipv4); for (const bot of detectedBots) { if (!bot.organizationId) { skippedRuntimeBots += 1; continue; } try { const result = await this.deleteSysAdminRuntimeBot(baseUrl, encodeURIComponent(bot.organizationId), true); if (result.botDeleted) { deletedRuntimeBots += 1; } } catch (_error) { skippedRuntimeBots += 1; } } for (const company of assignedCompanies) { const companyDocument = company as typeof company & { save: () => Promise }; companyDocument.wapServerId = undefined; await companyDocument.save(); } } await this.servers.delete(data.serverId); return { serverId: data.serverId, deleted: true, ...(force ? { force: true, detachedOrganizations: assignedCompanies.length, deletedRuntimeBots, skippedRuntimeBots, ...(auditFailed ? { auditFailed: true, auditMessage } : {}), } : {}), }; } public async sysAdminStartBot(data: SysAdminWapServerOrganizationActionParams): Promise { await this.postSysAdminBotAction(data, "start"); } public async sysAdminStopBot(data: SysAdminWapServerOrganizationActionParams): Promise { await this.postSysAdminBotAction(data, "stop"); } public async sysAdminRestartBot(data: SysAdminWapServerOrganizationActionParams): Promise { await this.postSysAdminBotAction(data, "stop"); await this.waitForSysAdminBotStopped(data); try { await this.postSysAdminBotAction(data, "start"); } catch (error) { const message = error instanceof Error && error.message ? error.message : "unknown error"; throw new Error(`Bot restart stopped the bot, but start failed. The bot may be stopped. ${message}`); } } public async sysAdminGetQr(data: SysAdminWapServerOrganizationActionParams): Promise { const { baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); const targetUrl = `${baseUrl}containers/${encodedOrganizationId}/qr`; try { const response = await axios.get(targetUrl, { responseType: "arraybuffer", timeout: BOT_ADMIN_ACTION_TIMEOUT_MS, }); const qr = Buffer.from(response.data, "binary").toString("base64"); return { serverId: data.serverId, organizationId: data.organizationId, qr }; } catch (error) { throw this.createBotAdminActionError(error, targetUrl); } } public async sysAdminDeleteBot( data: SysAdminWapServerOrganizationActionParams, ): Promise { const { server, baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); const before = server.countBots ?? 0; await this.deleteSysAdminRuntimeBot(baseUrl, encodedOrganizationId); // Deleting the runtime bot intentionally keeps the DB assignment; detach is a separate sysadmin action. const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); const after = assignedCompanies.length; server.countBots = after; await server.save(); return { serverId: data.serverId, organizationId: data.organizationId, assignmentRemains: true, before, after }; } public async sysAdminDetachOrganizationServer( data: SysAdminWapServerOrganizationActionParams, ): Promise { const { server, baseUrl, encodedOrganizationId } = await this.getSysAdminServerActionTarget(data); const company = await CompaniesManager.companies.findOne({ _id: data.organizationId }); if (!company) { throw new Error("Organization not found"); } if (String(company.wapServerId || "") !== data.serverId) { throw new Error("Organization is not assigned to this WAP server"); } const before = server.countBots ?? 0; const deleteResult = await this.deleteSysAdminRuntimeBot(baseUrl, encodedOrganizationId, true); company.wapServerId = undefined; await company.save(); const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId }); const after = assignedCompanies.length; server.countBots = after; await server.save(); return { serverId: data.serverId, organizationId: data.organizationId, ...deleteResult, before, after }; } } const WapServerList = new WapServerManager(); export default WapServerList;