import boxen, { type Options as BoxenOptions } from 'boxen'; import chalk from 'chalk'; import open from 'open'; import { shuffle } from 'd3-array'; import { z } from 'zod'; import got from '@lib/got'; import getOsDistro from '@lib/host/getOsDistro.js'; import { txDevEnv, txEnv, txHostConfig } from '@core/globalData'; import consoleFactory from '@lib/console'; import { addLocalIpAddress } from '@lib/host/isIpAddressLocal'; import { chalkInversePad } from '@lib/misc'; const console = consoleFactory(); const getPublicIp = async () => { const zIpValidator = z.string().ip(); const reqOptions = { timeout: { request: 2000 }, }; const httpGetter = async (url: string, jsonPath: string) => { const res = await got(url, reqOptions).json(); return zIpValidator.parse((res as any)[jsonPath]); }; const allApis = shuffle([ ['https://api.ipify.org?format=json', 'ip'], ['https://api.myip.com', 'ip'], ['https://ipv4.jsonip.com/', 'ip'], ['https://api.my-ip.io/v2/ip.json', 'ip'], ['https://www.l2.io/ip.json', 'ip'], ]); for await (const [url, jsonPath] of allApis) { try { return await httpGetter(url, jsonPath); } catch (error) { } } return false; }; const getOSMessage = async () => { const serverMessage = [ `To be able to access txAdmin from the internet open port ${txHostConfig.txaPort}`, 'on your OS Firewall as well as in the hosting company.', ]; const winWorkstationMessage = [ '[!] Home-hosting fxserver is not recommended [!]', 'You need to open the fxserver port (usually 30120) on Windows Firewall', 'and set up port forwarding on your router so other players can access it.', ]; if (txEnv.displayAds) { winWorkstationMessage.push('We recommend renting a server from ' + chalk.inverse(' https://zap-hosting.com/txAdmin ') + '.'); } //FIXME: use si.osInfo() instead const distro = await getOsDistro(); return (distro && distro.includes('Linux') || distro.includes('Server')) ? serverMessage : winWorkstationMessage; }; const awaitHttp = new Promise((resolve, reject) => { const tickLimit = 100; //if over 15 seconds let counter = 0; let interval: NodeJS.Timeout; const check = () => { counter++; if (txCore.webServer && txCore.webServer.isListening && txCore.webServer.isServing) { clearInterval(interval); resolve(true); } else if (counter == tickLimit) { clearInterval(interval); interval = setInterval(check, 2500); } else if (counter > tickLimit) { console.warn('The WebServer is taking too long to start:', { module: !!txCore.webServer, listening: txCore?.webServer?.isListening, serving: txCore?.webServer?.isServing, }); } }; interval = setInterval(check, 150); }); const awaitMasterPin = new Promise((resolve, reject) => { const tickLimit = 100; //if over 15 seconds let counter = 0; let interval: NodeJS.Timeout; const check = () => { counter++; if (txCore.adminStore && txCore.adminStore.admins !== null) { clearInterval(interval); const pin = (txCore.adminStore.admins === false) ? txCore.adminStore.addMasterPin : false; resolve(pin); } else if (counter == tickLimit) { clearInterval(interval); interval = setInterval(check, 2500); } else if (counter > tickLimit) { console.warn('The AdminStore is taking too long to start:', { module: !!txCore.adminStore, admins: txCore?.adminStore?.admins === null ? 'null' : 'not null', }); } }; interval = setInterval(check, 150); }); const awaitDatabase = new Promise((resolve, reject) => { const tickLimit = 100; //if over 15 seconds let counter = 0; let interval: NodeJS.Timeout; const check = () => { counter++; if (txCore.database && txCore.database.isReady) { clearInterval(interval); resolve(true); } else if (counter == tickLimit) { clearInterval(interval); interval = setInterval(check, 2500); } else if (counter > tickLimit) { console.warn('The Database is taking too long to start:', { module: !!txCore.database, ready: !!txCore?.database?.isReady, }); } }; interval = setInterval(check, 150); }); export const startReadyWatcher = async (cb: () => void) => { const [publicIpResp, msgRes, adminPinRes] = await Promise.allSettled([ getPublicIp(), getOSMessage(), awaitMasterPin as Promise, awaitHttp, awaitDatabase, ]); //Addresses let detectedUrls; if (txHostConfig.netInterface && txHostConfig.netInterface !== '0.0.0.0') { detectedUrls = [txHostConfig.netInterface]; } else { detectedUrls = [ (txEnv.isWindows) ? 'localhost' : 'your-public-ip', ]; if ('value' in publicIpResp && publicIpResp.value) { detectedUrls.push(publicIpResp.value); addLocalIpAddress(publicIpResp.value); } } const bannerUrls = txHostConfig.txaUrl ? [txHostConfig.txaUrl] : detectedUrls.map((addr) => `http://${addr}:${txHostConfig.txaPort}/`); //Admin PIN const adminMasterPin = 'value' in adminPinRes && adminPinRes.value ? adminPinRes.value : false; const adminPinLines = !adminMasterPin ? [] : [ '', 'Use the PIN below to register:', chalk.inverse(` ${adminMasterPin} `), ]; //Printing stuff const boxOptions = { padding: 1, margin: 1, align: 'center', borderStyle: 'bold', borderColor: 'cyan', } satisfies BoxenOptions; const boxLines = [ 'All ready! Please access:', ...bannerUrls.map(chalkInversePad), ...adminPinLines, ]; console.multiline(boxen(boxLines.join('\n'), boxOptions), chalk.bgGreen); if (!txDevEnv.ENABLED && !txHostConfig.netInterface && 'value' in msgRes && msgRes.value) { console.multiline(msgRes.value, chalk.bgBlue); } //Opening page if (txEnv.isWindows && adminMasterPin && bannerUrls[0]) { const linkUrl = new URL(bannerUrls[0]); linkUrl.pathname = '/addMaster/pin'; linkUrl.hash = adminMasterPin; open(linkUrl.href); } //Callback cb(); };