├── .gitignore ├── src ├── structures │ ├── index.js │ ├── Server.js │ └── Player.js ├── index.js ├── util │ ├── Error.js │ └── Util.js └── DiscordFivemApi.js ├── package.json ├── LICENSE.md ├── yarn.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules 3 | /npm-debug.log 4 | /*.iml 5 | /.idea -------------------------------------------------------------------------------- /src/structures/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; // Enforcing strict mode for better error checking and security 2 | 3 | // Import the Player and Server classes from their respective files 4 | const Player = require('./Player'); 5 | const Server = require('./Server'); 6 | 7 | // Export the Player and Server classes to make them accessible when this module is required 8 | module.exports = { 9 | Player, 10 | Server, 11 | }; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Importing the version number from the package.json file 2 | const { version } = require('../package.json'); 3 | 4 | // Importing the main DiscordFivemApi class 5 | const DiscordFivemApi = require('./DiscordFivemApi'); 6 | 7 | // Importing the Player and Server classes from the structures directory 8 | const { Player, Server } = require('./structures/index'); 9 | 10 | /** 11 | * Exports the version number, main API class, and structure classes. 12 | * 13 | * - `version`: The current version of the package. 14 | * - `DiscordFivemApi`: The primary API interface for interacting with FiveM servers. 15 | * - `Player`: Class representing player data in the server. 16 | * - `Server`: Class representing server data. 17 | */ 18 | module.exports = { 19 | version, 20 | DiscordFivemApi, 21 | Player, 22 | Server, 23 | }; 24 | -------------------------------------------------------------------------------- /src/structures/Server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; // Enforcing strict mode for better error handling and security 2 | 3 | /** 4 | * Class representing server data with encapsulated properties. 5 | */ 6 | class ServerData { 7 | #data; // Private field to store server data 8 | 9 | /** 10 | * Constructs a new ServerData instance. 11 | * 12 | * @param {Object} data - The raw server data object. 13 | */ 14 | constructor(data) { 15 | this.#data = data; 16 | 17 | // Define read-only properties for each key in the server data object 18 | for (const key in this.#data) { 19 | Object.defineProperty(this, key, { 20 | writable: false, // Ensure the property is read-only 21 | enumerable: true, // Ensure the property is included in loops (e.g., for...in, Object.keys) 22 | value: data[key], // Set the value of the property from the data object 23 | }); 24 | } 25 | } 26 | } 27 | 28 | // Export the ServerData class to make it available for external modules 29 | module.exports = ServerData; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-fivem-api", 3 | "version": "2.0.6", 4 | "description": "A Node.js package to interact with FiveM servers, allowing you to retrieve server data, player information, and resources. Ideal for Discord bot integration or standalone Node.js applications.", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "FiveM", 11 | "fivem stats", 12 | "FiveM API", 13 | "discord fivem", 14 | "discord-fivem-api", 15 | "discord", 16 | "discordapp", 17 | "discord.js", 18 | "fivem cfx", 19 | "cfx.re api", 20 | "cfx api" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git@github.com:xliel/discord-fivem-api.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/xliel/discord-fivem-api/issues" 28 | }, 29 | "author": "xliel ", 30 | "license": "MIT", 31 | "dependencies": { 32 | "axios": "^1.6.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 xliel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/structures/Player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; // Enforcing strict mode for better error handling and security 2 | 3 | // Import the flatten function from the Util.js file 4 | const { flatten } = require('../util/Util'); 5 | 6 | /** 7 | * Represents a player with structured data and utility methods. 8 | */ 9 | class Player { 10 | #data; // Private field to store the player's data 11 | 12 | /** 13 | * Constructs a new Player instance. 14 | * 15 | * @param {Object} data - The raw player data object. 16 | */ 17 | constructor(data) { 18 | this.#data = data; 19 | 20 | // Iterate over each key in the player data object 21 | for (const key in this.#data) { 22 | // Special handling for the 'identifiers' key to split it into key-value pairs 23 | if (key === 'identifiers') { 24 | let playerIdentifiers = {}; 25 | for (const identifier of this.#data[key]) { 26 | if (!identifier.includes(':')) continue; // Skip invalid identifiers 27 | const [idType, idValue] = identifier.split(':'); 28 | playerIdentifiers[idType] = idValue; // Store identifier in key-value format 29 | } 30 | this.#data[key] = playerIdentifiers; // Replace original identifiers array with parsed object 31 | } 32 | 33 | // Define a read-only property for each key in the player data object 34 | Object.defineProperty(this, key, { 35 | writable: false, 36 | enumerable: true, 37 | value: data[key], 38 | }); 39 | } 40 | } 41 | 42 | /** 43 | * Returns the player's name, or 'Unknown' if no name is available. 44 | * 45 | * @returns {string} - The player's name or 'Unknown'. 46 | */ 47 | toString() { 48 | return this.#data?.name ?? 'Unknown'; 49 | } 50 | 51 | /** 52 | * Returns a flattened JSON representation of the player object. 53 | * 54 | * @param {...any} props - Additional properties to include in the flattened output. 55 | * @returns {Object} - The flattened player data object. 56 | */ 57 | toJSON(...props) { 58 | return flatten(this, ...props); 59 | } 60 | } 61 | 62 | // Export the Player class to make it available for external modules 63 | module.exports = Player; 64 | -------------------------------------------------------------------------------- /src/util/Error.js: -------------------------------------------------------------------------------- 1 | // Symbol used to store the error code, keeping it private to avoid property collisions 2 | const kCode = Symbol('code'); 3 | 4 | /** 5 | * Creates a custom error class that extends a base error class (Error or TypeError). 6 | * 7 | * @param {Error} [ErrorBase=global.Error] - The base error class to extend (default is the built-in Error). 8 | * @returns {class} - A custom error class with enhanced functionality. 9 | */ 10 | function createErrorMessage(ErrorBase = global.Error) { 11 | // Define a new error class extending the provided base error class 12 | class DiscordFivemApiError extends ErrorBase { 13 | /** 14 | * Constructs a new DiscordFivemApiError instance. 15 | * 16 | * @param {string} key - A custom error code or identifier. 17 | * @param {...any} args - Additional arguments passed to the base error class. 18 | */ 19 | constructor(key, ...args) { 20 | // Call the parent Error constructor with provided arguments 21 | super(...args); 22 | 23 | // Assign the custom error code (key) to the private symbol 24 | this[kCode] = key; 25 | 26 | // Capture the stack trace for debugging, if the environment supports it 27 | if (Error.captureStackTrace) 28 | Error.captureStackTrace(this, DiscordFivemApiError); 29 | } 30 | 31 | /** 32 | * Getter for the error name, including the custom error code. 33 | * @returns {string} - The formatted error name with the error code. 34 | */ 35 | get name() { 36 | return `[DiscordFivemApi] ${super.name} [${this[kCode]}]`; 37 | } 38 | 39 | /** 40 | * Getter for the error code. 41 | * @returns {string} - The error code assigned to the instance. 42 | */ 43 | get code() { 44 | return this[kCode]; 45 | } 46 | 47 | /** 48 | * Overrides the default toString method to include the error code. 49 | * @returns {string} - The string representation of the error with the custom code. 50 | */ 51 | toString() { 52 | return `[DiscordFivemApi] ${super.toString()} [${this[kCode]}]`; 53 | } 54 | } 55 | 56 | // Return the custom error class 57 | return DiscordFivemApiError; 58 | } 59 | 60 | // Export the createErrorMessage function, along with pre-defined instances for Error and TypeError 61 | module.exports = { 62 | createErrorMessage, 63 | Error: createErrorMessage(Error), // Custom Error class 64 | TypeError: createErrorMessage(TypeError), // Custom TypeError class 65 | }; 66 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | asynckit@^0.4.0: 6 | version "0.4.0" 7 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 8 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 9 | 10 | axios@^1.6.7: 11 | version "1.6.7" 12 | resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" 13 | integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== 14 | dependencies: 15 | follow-redirects "^1.15.4" 16 | form-data "^4.0.0" 17 | proxy-from-env "^1.1.0" 18 | 19 | combined-stream@^1.0.8: 20 | version "1.0.8" 21 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 22 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 23 | dependencies: 24 | delayed-stream "~1.0.0" 25 | 26 | delayed-stream@~1.0.0: 27 | version "1.0.0" 28 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 29 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 30 | 31 | follow-redirects@^1.15.4: 32 | version "1.15.5" 33 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" 34 | integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== 35 | 36 | form-data@^4.0.0: 37 | version "4.0.0" 38 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 39 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 40 | dependencies: 41 | asynckit "^0.4.0" 42 | combined-stream "^1.0.8" 43 | mime-types "^2.1.12" 44 | 45 | mime-db@1.52.0: 46 | version "1.52.0" 47 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 48 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 49 | 50 | mime-types@^2.1.12: 51 | version "2.1.35" 52 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 53 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 54 | dependencies: 55 | mime-db "1.52.0" 56 | 57 | proxy-from-env@^1.1.0: 58 | version "1.1.0" 59 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 60 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 61 | -------------------------------------------------------------------------------- /src/util/Util.js: -------------------------------------------------------------------------------- 1 | // Utility function to check if a value is an object and not null 2 | const isObject = (d) => typeof d === 'object' && d !== null; 3 | 4 | /** 5 | * Flattens an object and optionally renames its properties. 6 | * 7 | * @param {Object} obj - The object to be flattened. 8 | * @param {...Object} props - Optional property mappings to rename keys in the flattened object. 9 | * @returns {Object} - A new object with flattened properties. 10 | */ 11 | function flatten(obj, ...props) { 12 | // If the input is not a valid object, return the value unchanged. 13 | if (!isObject(obj)) return obj; 14 | 15 | // Extract object keys that do not start with an underscore (i.e., not private) 16 | const objProps = Object.keys(obj) 17 | .filter((k) => !k.startsWith('_')) // Ignore private fields starting with '_' 18 | .map((k) => ({ [k]: true })); // Prepare the property to retain its key 19 | 20 | // Merge object properties with any additional property mappings passed in. 21 | props = objProps.length 22 | ? Object.assign(...objProps, ...props) 23 | : Object.assign({}, ...props); // Ensure an empty object if no props provided 24 | 25 | // Output object for storing flattened key-value pairs. 26 | const out = {}; 27 | 28 | // Iterate over the properties and apply renaming/flattening logic. 29 | for (let [prop, newProp] of Object.entries(props)) { 30 | // If the new property mapping is set to false, skip this property. 31 | if (!newProp) continue; 32 | 33 | // If the new property mapping is true, retain the original property name. 34 | newProp = newProp === true ? prop : newProp; 35 | 36 | // Get the value of the current property and check if it's an object. 37 | const element = obj[prop]; 38 | const elemIsObj = isObject(element); 39 | 40 | // If the element has a valueOf method, retrieve its primitive value. 41 | const valueOf = 42 | elemIsObj && typeof element.valueOf === 'function' 43 | ? element.valueOf() 44 | : null; 45 | 46 | // Check if the element has a toJSON method. 47 | const hasToJSON = elemIsObj && typeof element.toJSON === 'function'; 48 | 49 | // Handle arrays: flatten or serialize each element. 50 | if (Array.isArray(element)) 51 | out[newProp] = element.map((e) => e.toJSON?.() ?? flatten(e)); 52 | // If the valueOf method returns a non-object, use that value directly. 53 | else if (typeof valueOf !== 'object') out[newProp] = valueOf; 54 | // If the element has a toJSON method, use its JSON representation. 55 | else if (hasToJSON) out[newProp] = element.toJSON(); 56 | // If the element is a nested object, recursively flatten it. 57 | else if (typeof element === 'object') out[newProp] = flatten(element); 58 | // If the element is neither an object nor requires special handling, copy it directly. 59 | else if (!elemIsObj) out[newProp] = element; 60 | } 61 | 62 | // Return the fully flattened object. 63 | return out; 64 | } 65 | 66 | /** 67 | * Wraps a promise with a timeout. If the promise doesn't resolve within the specified time, 68 | * it will reject with a 'TIMEOUT' error. 69 | * 70 | * @param {Promise} promise - The promise to wait for. 71 | * @param {number} ms - The timeout duration in milliseconds. 72 | * @returns {Promise} - A promise that resolves or rejects depending on the timeout. 73 | */ 74 | async function timeoutPromise(promise, ms) { 75 | return await Promise.race([ 76 | promise, // The original promise 77 | new Promise( 78 | (_, reject) => setTimeout(() => reject(new Error('TIMEOUT')), ms) // Timeout error 79 | ), 80 | ]); 81 | } 82 | 83 | // Export the functions for external use 84 | module.exports = { 85 | flatten, 86 | timeoutPromise, 87 | }; 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Install the `discord-fivem-api` package using either npm or yarn: 4 | 5 | ```bash 6 | $ npm install discord-fivem-api 7 | # or 8 | $ yarn add discord-fivem-api 9 | ``` 10 | 11 | ## Usage 12 | 13 | First, import the `DiscordFivemApi` class: 14 | 15 | ```javascript 16 | const DiscordFivemApi = require('discord-fivem-api'); 17 | ``` 18 | 19 | Next, create a new instance of the class with the desired configuration options: 20 | 21 | ```javascript 22 | const options = { 23 | address: 'localhost', // IP or hostname of the FiveM server 24 | port: 30120, // Port number (default: 30120) 25 | useStructure: true, // Whether to use the custom `Player` and `Server` structures (default: true) 26 | interval: 5000, // Interval in ms between server updates (default: 2500) 27 | }; 28 | 29 | const api = new DiscordFivemApi(options, true); // 'true' initializes the API immediately 30 | ``` 31 | 32 | Configuration Options 33 | The `options` object supports the following properties: 34 | - `address` (required): The IP address or hostname of the FiveM server. 35 | - `port` (optional, default: `30120`): The port number of the FiveM server. 36 | - `useStructure`: (optional, default: `true`): Whether to use the custom `Player` and `Server` classes from the structures directory. 37 | - `interval`: (optional, default: `2500`): The interval (in milliseconds) between server data updates. 38 | 39 | The second argument (`init`) specifies whether to initialize the API immediately (`true`) or not (`false`, default). 40 | 41 | 42 | ### Methods 43 | Once the `DiscordFivemApi` instance is created, you can use its methods to retrieve server information: 44 | 45 | ```javascript 46 | api.getStatus() 47 | .then((status) => console.log(`Server status: ${status}`)) 48 | .catch((err) => console.error(err)); 49 | 50 | api.getServerData() 51 | .then((serverData) => console.log(serverData)) 52 | .catch((err) => console.error(err)); 53 | 54 | api.getServerPlayers() 55 | .then((players) => console.log(players)) 56 | .catch((err) => console.error(err)); 57 | 58 | api.getPlayersOnline() 59 | .then((count) => console.log(`Players online: ${count}`)) 60 | .catch((err) => console.error(err)); 61 | 62 | api.getMaxPlayers() 63 | .then((maxPlayers) => console.log(`Max players: ${maxPlayers}`)) 64 | .catch((err) => console.error(err)); 65 | ``` 66 | 67 | ### Method Details 68 | 69 | - `.getStatus()` Returns a promise that resolves to 'online' or 'offline'. 70 | 71 | - `.getServerData()` Returns a promise that resolves to an object containing server information. If useStructure is true, it returns an instance of the Server class. 72 | 73 | - `.getServerPlayers()` Returns a promise that resolves to an array of player objects. If useStructure is true, each object is an instance of the Player class. 74 | 75 | - `.getPlayersOnline()` Returns a promise that resolves to the number of players currently online. 76 | 77 | - `.getMaxPlayers()` Returns a promise that resolves to the server's maximum player capacity. 78 | 79 | ### Events 80 | ```javascript 81 | api.on('ready', () => console.log('API initialized')); 82 | api.on('readyPlayers', (players) => console.log(`Players: ${players.length}`)); 83 | api.on('readyResources', (resources) => console.log(`Resources: ${resources.length}`)); 84 | api.on('playerJoin', (player) => console.log(`${player.name} joined the server`)); 85 | api.on('playerLeave', (player) => console.log(`${player.name} left the server`)); 86 | api.on('resourceAdd', (resource) => console.log(`Resource added: ${resource}`)); 87 | api.on('resourceRemove', (resource) => console.log(`Resource removed: ${resource}`)); 88 | ``` 89 | 90 | ### Event Details 91 | 92 | - `ready`: Emitted when the API is initialized. 93 | - `readyPlayers`: Emitted when player data is available. 94 | - `readyResources`: Emitted when resource data is available. 95 | - `playerJoin`: Emitted when a player joins the server. 96 | - `playerLeave`: Emitted when a player leaves the server. 97 | - `resourceAdd`: Emitted when a new resource is added to the server. 98 | - `resourceRemove`: Emitted when a resource is removed from the server. 99 | 100 | ## License 101 | 102 | This package is licensed under the MIT License. -------------------------------------------------------------------------------- /src/DiscordFivemApi.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | const { EventEmitter } = require('events'); 3 | const axios = require('axios'); 4 | const { Player, Server: ServerData } = require('./structures/index'); 5 | const { Error: DfaError, TypeError: DfaTypeError } = require('./util/Error'); 6 | 7 | // Define DiscordFivemApi class 8 | class DiscordFivemApi extends EventEmitter { 9 | options; 10 | 11 | // Constructor 12 | constructor(options, init = false) { 13 | super(); 14 | 15 | // Set default options if not provided 16 | if (!this.options) this.options = {}; 17 | if (!this.options?.port) this.options.port = 30120; 18 | if (!this.options?.useStructure) this.options.useStructure = false; 19 | if (!this.options?.interval) this.options.interval = 2500; 20 | this.options = options; 21 | 22 | // Validate options 23 | if (!this?.options?.address) { 24 | throw new DfaError('NO_ADDRESS', 'No address was provided.'); 25 | } 26 | if (!this?.options?.port) { 27 | throw new DfaError('NO_PORT', 'No port was provided.'); 28 | } 29 | 30 | if (typeof this?.options?.address !== 'string') { 31 | throw new DfaTypeError( 32 | 'INVALID_ADDRESS', 33 | 'The address option must be a string.' 34 | ); 35 | } 36 | 37 | if (typeof this?.options?.port !== 'number') { 38 | throw new DfaTypeError( 39 | 'INVALID_PORT', 40 | 'The port option must be a number.' 41 | ); 42 | } 43 | 44 | if (typeof this?.options?.interval !== 'number') { 45 | throw new DfaTypeError( 46 | 'INVALID_INTERVAL', 47 | 'The interval option must be a number.' 48 | ); 49 | } 50 | 51 | if (typeof init !== 'boolean') { 52 | throw new DfaTypeError( 53 | 'INVALID_INIT', 54 | 'The init option must be a boolean.' 55 | ); 56 | } 57 | if ( 58 | this?.options?.useStructure !== undefined && 59 | typeof this?.options?.useStructure !== 'boolean' 60 | ) { 61 | throw new DfaTypeError( 62 | 'INVALID_USE_STRUCTURE', 63 | 'The useStructure option must be a boolean.' 64 | ); 65 | } 66 | 67 | // Initialize properties 68 | this._players = []; 69 | this.useStructure = options?.useStructure; 70 | 71 | this.address = options.address; 72 | this.port = options.port || 30120; 73 | 74 | // Call _init method if init is true 75 | if (init) this._init(); 76 | } 77 | 78 | // Getters and setters 79 | get players() { 80 | return this._players; 81 | } 82 | 83 | set players(players) { 84 | this._players = players; 85 | } 86 | 87 | // Get server status 88 | getStatus() { 89 | return new Promise((resolve, reject) => { 90 | axios 91 | .get(`http://${this.address}:${this.port}/info.json`, { 92 | timeout: 5000, 93 | }) 94 | .then((res) => { 95 | resolve('online'); 96 | }) 97 | .catch(() => { 98 | resolve('offline'); 99 | }); 100 | }); 101 | } 102 | 103 | // Get server data 104 | getServerData() { 105 | return new Promise((resolve, reject) => { 106 | axios 107 | .get(`http://${this.address}:${this.port}/info.json`, { 108 | timeout: 5000, 109 | }) 110 | .then((res) => { 111 | if (this.useStructure) { 112 | resolve(new ServerData(res.data)); 113 | } else resolve(res.data); 114 | }) 115 | .catch((err) => { 116 | reject({ 117 | error: { 118 | message: err.message, 119 | stack: err.stack, 120 | }, 121 | data: {}, 122 | }); 123 | }); 124 | }); 125 | } 126 | 127 | // Get server players 128 | getServerPlayers() { 129 | return new Promise((resolve, reject) => { 130 | axios 131 | .get(`http://${this.address}:${this.port}/players.json`, { 132 | timeout: 5000, 133 | }) 134 | .then((res) => { 135 | if (this.useStructure) { 136 | const players = []; 137 | for (const player of res.data) { 138 | players.push(new Player(player)); 139 | } 140 | this.players = players; 141 | 142 | resolve(players); 143 | } else resolve(res.data); 144 | }) 145 | .catch((err) => { 146 | reject({ 147 | error: { 148 | message: err.message, 149 | stack: err.stack, 150 | }, 151 | players: [], 152 | }); 153 | }); 154 | }); 155 | } 156 | 157 | // Get number of players online 158 | getPlayersOnline() { 159 | return new Promise((resolve, reject) => { 160 | axios 161 | .get(`http://${this.address}:${this.port}/players.json`, { 162 | timeout: 5000, 163 | }) 164 | .then((res) => { 165 | resolve(res.data.length); 166 | }) 167 | .catch((err) => { 168 | reject({ 169 | error: { 170 | message: err.message, 171 | stack: err.stack, 172 | }, 173 | playersOnline: 0, 174 | }); 175 | }); 176 | }); 177 | } 178 | 179 | // Get maximum number of players 180 | getMaxPlayers() { 181 | return new Promise((resolve, reject) => { 182 | axios 183 | .get(`http://${this.address}:${this.port}/info.json`, { 184 | timeout: 5000, 185 | }) 186 | .then((res) => { 187 | resolve(res.data.vars.sv_maxClients); 188 | }) 189 | .catch((err) => { 190 | reject({ 191 | error: { 192 | message: err.message, 193 | stack: err.stack, 194 | }, 195 | maxPlayers: 0, 196 | }); 197 | }); 198 | }); 199 | } 200 | 201 | 202 | // Initialize the API 203 | async _init() { 204 | this.emit('ready'); 205 | 206 | const [serverData, players] = await Promise.all([ 207 | this.getServerData().catch(() => {}), 208 | this.getServerPlayers().catch(() => []), 209 | ]); 210 | 211 | this.players = players; 212 | this.resources = serverData?.resources ?? []; 213 | 214 | this.emit('readyPlayers', players); 215 | this.emit('readyResources', this.resources); 216 | 217 | setInterval(async () => { 218 | const newPlayers = await this.getServerPlayers().catch(() => []); 219 | if (this.players.length != newPlayers.length) { 220 | if (this.players.length < newPlayers.length) { 221 | for (const player of newPlayers) { 222 | if (this.players.find((p) => p?.id == player?.id)) continue; 223 | this.emit('playerJoin', player); 224 | } 225 | } else { 226 | for (const player of this.players) { 227 | if (newPlayers.find((p) => p?.id == player?.id)) continue; 228 | this.emit('playerLeave', player); 229 | } 230 | } 231 | 232 | this.players = newPlayers; 233 | } 234 | 235 | const serverData2 = await this.getServerData().catch(() => {}); 236 | const newResources = serverData2?.resources ?? []; 237 | if (this.resources?.length != newResources?.length) { 238 | if (this.resources?.length < newResources?.length) { 239 | for (const resource of newResources) { 240 | if (this.resources.find((r) => r == resource)) continue; 241 | this.emit('resourceAdd', resource); 242 | } 243 | } else { 244 | for (const resource of this.resources) { 245 | if (newResources.find((r) => r == resource)) continue; 246 | this.emit('resourceRemove', resource); 247 | } 248 | } 249 | } 250 | }, this.options?.interval || 2500); 251 | } 252 | } 253 | 254 | module.exports = DiscordFivemApi; 255 | 256 | /** 257 | * The DiscordFivemApi class. 258 | * @typedef {DiscordFivemApi} DiscordFivemApi 259 | * @property {Object} options The options for the API. 260 | * @property {string} options.address The IP address of the FiveM server. 261 | * @property {number} options.port The port of the FiveM server. 262 | * @property {boolean} options.useStructure Whether to use the structures or not. 263 | * @property {number} [options.interval=2500] The interval to check for player and resource changes. 264 | */ 265 | --------------------------------------------------------------------------------