├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── client └── main.js ├── config.jsonc ├── env.example.js ├── fxmanifest.lua ├── package.json ├── permissions.cfg └── server ├── commands └── playerList.soon ├── events ├── interactionCreate.js └── ready.js ├── main.js └── register.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn.installed 3 | env.js 4 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Concept Collective 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 | Discord Status 8 |

9 | 10 |
11 | 12 |

🎮 ccDiscordWrapper - Discord ➡️ FiveM

13 |

14 | ccDiscordWrapper is a powerful resource that simplifies Discord integration for your FiveM server. It allows you to seamlessly connect your server to Discord, enabling exciting features for your community. Whether you want to send messages, retrieve player Discord information, or sync roles, ccDiscordWrapper has got you covered! 15 |
16 |

17 | 18 | [Documentation](https://docs.conceptcollective.net/docs/category/-cc-discord-wrapper) | [Report Bug](https://github.com/Concept-Collective/ccDiscordWrapper/issues) | [Download Now](https://github.com/Concept-Collective/ccDiscordWrapper/releases) 19 | 20 | Discord Invite 21 |
22 | 23 | ## 🎨 Showcase 24 | 25 | Screenshots or videos of ccDiscordWrapper in action: 26 | 27 |
28 | Screenshots 29 |
30 | 31 | ![image|620x500](upload://kNBuwJpWR8bnvaxU9qE1idT91BR.png) 32 | 33 | ![image|690x393](upload://fH2HEIDdwOyjljCn4u2FVdqvY72.jpeg) 34 | 35 | ![image|390x500](upload://h9YUoneWbk1ApWLYpIaH9TRahDz.png) 36 | 37 |
38 |
39 |

40 | 41 | ## Dependency Note: Yarn 42 | 43 | ccDiscordWrapper requires Yarn as a dependency for its proper functioning. However, there's no need to worry as Yarn is built into FiveM, and the server will automatically handle the installation. 44 | 45 | You don't have to install Yarn separately. When you add ccDiscordWrapper to your resources folder and start your FiveM server, the server will handle the dependency and ensure smooth operation. 46 | 47 | If you encounter any issues related to Yarn, please ensure you have the latest version of FiveM server artifacts installed and update your server accordingly. 48 | 49 | :warning: **Important Notice** 50 | 51 | If ccDiscordWrapper is a dependency for another resource, you can ignore this notice. 52 | 53 | Please be aware that ccDiscordWrapper does not include any pre-written commands or chat messages. You'll need to create them yourself. This resource is designed for experienced scripters who have some knowledge of scripting. Only those familiar with scripting will receive support. 54 | 55 | :white_check_mark: **Features** 56 | 57 | - Discord Message Sending: Send Discord messages using the configured Discord Bot Token or Webhook URL. 58 | - Player Discord Information: Retrieve in-game players' Discord Avatars and highest prioritized Discord Roles. 59 | - Discord Role Sync: Synchronize in-game roles with corresponding Discord roles. 60 | - Player Verification: Implement a user verification system to link players to their Discord accounts. 61 | - Customizable Chat Integration: Customize and integrate Discord chat with your FiveM server. 62 | - Server Status Updates: Send periodic status updates to a designated Discord channel. 63 | - Error Logging: Log server errors and critical events to a specified Discord channel. 64 | - Command Handling: Implement a command handling system to register and manage custom Discord commands. 65 | 66 | :hammer_and_wrench: **Installation** 67 | 68 | Follow the [official guide](https://docs.conceptcollective.net/docs/fivem-resources/Free/ccDiscordWrapper/Introduction/) to install ccDiscordWrapper. 69 | 70 | **Download** 71 | 72 | Download the latest release [here](https://github.com/Concept-Collective/ccDiscordWrapper/releases). 73 | 74 | **Install** 75 | 76 | 1. Create a new folder in your `resources` folder named `ccDiscordWrapper`. 77 | 2. Extract the contents of the archive to your `ccDiscordWrapper` folder. 78 | 3. Add `start ccDiscordWrapper` in your `server.cfg`. 79 | 80 | :seedling: **Source Code** 81 | 82 | ccDiscordWrapper's source code is available on [GitHub](https://github.com/Concept-Collective/ccDiscordWrapper). 83 | 84 | You are free to use and modify this code as long as you provide proper credit and never claim it as your own. Selling ccDiscordWrapper or any code derived from it is not allowed. If you create your own version, please link to the original GitHub repo or release it via a Forked repo. 85 | 86 | We're thrilled to share ccDiscordWrapper with the FiveM community. Feel free to use it, experiment with it, and let us know your feedback! 87 | 88 | Happy coding and Discord integration! 🚀😄 89 | 90 | 91 | ## Roadmap (Builtin Examples) 92 | 93 | - FiveM Player Count -> Discord Channel Embed ✅ 94 | - FiveM Player Count -> Discord Channel Name ✅ 95 | - Discord GuildMember Information -> FiveM ⏳✅ 96 | - more integrations to come soon! 97 | 98 | ## Support 99 | 100 | For support, please join our [Discord](https://discord.conceptcollective.net) server. 101 | 102 | 103 | ## Documentation 104 | 105 | [Coming Soon](https://docs.conceptcollective.net) 106 | 107 | 108 | ## Environment Variables 109 | 110 | In order to run CC-discordStatus's Discord player count module, you will need to add the following environment variables to your env.js file 111 | 112 | `sv_config.Discord_Token` 113 | 114 | ## FAQ 115 | 116 | #### Coming 117 | 118 | Soon. 119 | 120 | #### Coming 121 | 122 | Soon. 123 | 124 | ## License 125 | 126 | [MIT](https://choosealicense.com/licenses/mit/) 127 | 128 | 129 | ## Contributing 130 | 131 | Contributions are always welcome! 132 | 133 | See `contributing.md` for ways to get started. 134 | 135 | Please adhere to this project's `code of conduct`. 136 | 137 | 138 | ## Feedback 139 | 140 | If you have any feedback, please reach out to us via our [Discord](https://discord.conceptcollective.net). 141 | 142 | ## Authors 143 | 144 | - [@69u](https://www.github.com/69u) 145 | 146 | 147 | ## Installation 148 | 149 | Coming Soon 150 | ## Features 151 | 152 | - Basic functionality ✅ 153 | - More to come! 154 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | onNet('onServerConfigLoad', (config) => { 2 | SetDiscordAppId(config.General.appID); 3 | SetDiscordRichPresenceAction(0, config.DiscordRPC.actions.action1.name, config.DiscordRPC.actions.action1.url); 4 | SetDiscordRichPresenceAction(1, config.DiscordRPC.actions.action2.name, config.DiscordRPC.actions.action2.url); 5 | SetDiscordRichPresenceAsset(config.DiscordRPC.assetLarge); 6 | SetDiscordRichPresenceAssetText(config.DiscordRPC.textLarge); 7 | SetDiscordRichPresenceAssetSmall(config.DiscordRPC.assetSmall); 8 | SetDiscordRichPresenceAssetSmallText(config.DiscordRPC.textSmall); 9 | SetRichPresence(`${GetNumberOfPlayers()} player(s) connected`); 10 | }); -------------------------------------------------------------------------------- /config.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "Debug": false, 3 | // Discord bot module configurations 4 | "General": { 5 | "IsServerUsingESX": false, // Set to true if using ESX 6 | "IsServerUsingQBCore": false, // Set to true if using QBCore 7 | "IsDiscordRequired": true, // Set to true if Discord is required to play on the server 8 | "IsSteamRequired": false, // Set to true if Steam is required to play on the server 9 | "appID": "1131898765368905768", // Discord Application ID (https://discord.com/developers/applications) 10 | "serverID": "730086035643564183", // Discord Server ID (https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-) 11 | "serverInviteURL": "https://discord.com/invite/xdV7w87Hk2", // Discord Server Invite URL (https://support.discord.com/hc/en-us/articles/208866998-Invites-101) 12 | "disableHardCap": true, // Setting to false **Not Recommended* will disable the hardcap resource (Limits the number of players to the amount set by sv_maxclients in your server.cfg) 13 | "compatibilityMode": true // Setting to false will disable the compatibility mode module (This module is used to stop known incompatible resources) 14 | }, 15 | // Discord Bot required env.js file with sv_token.Discord_Token configured 16 | "DiscordBot": { 17 | "enabled": true, // Setting to false will disable the Discord Bot module **Must be enabled for PlayerStatus** 18 | // player status message & channel configurations 19 | "PlayerStatus": { 20 | "enabled": false, // Setting to false will disable the player status message 21 | "channelID": "1060555091838521445", // Channel ID to send player status messages to 22 | "channelCountID": "1060555091838521445", // Channel ID to update name with current player count set to false to disable 23 | "listPlayers": true, // Setting to false will only show the total player count 24 | "embedConfig": { 25 | "title": "CC Server Status", // Title of the embed 26 | "url": "https://conceptcollective.net", // URL of the embed 27 | "thumbnail": "https://cdn.discordapp.com/attachments/672308435534086149/1053879929848209418/idk.png" // Thumbnail of the embed 28 | } 29 | }, 30 | "PlayerRoles": { 31 | "notInDiscord": "🏖️ Visitor", // Default role name to give players who are not in the Discord server. 32 | "DiscordnotFound": "❌ Discord" // Default role name to give players that do not have Discord linked to their FiveM. 33 | }, 34 | "PlayerAcePermissions": { 35 | "enabled": false, // Setting to false will disable the player ace permissions module 36 | "roleList": [ 37 | {"roleID": "736519152558276608", "aceGroup": "group.owner"} 38 | ] 39 | }, 40 | "DiscordWhitelist": { 41 | "enabled": false, // Setting to false will disable the Discord Whitelist module 42 | "roleID": "736519152558276608" // Role ID to whitelist 43 | }, 44 | "DiscordConnectQueue": { 45 | "enabled": false, // Setting to false will disable the Discord Connect Queue module 46 | "rolePriority": ["736519152558276608", "736519208871133185", "736519424785514586", "736529899463901185"] // Role ID's in order of priority (First role in the array has the highest priority) 47 | } 48 | }, 49 | "DiscordRPC": { 50 | "enabled": true, // Setting to false will disable the Discord Rich Presence module 51 | "assetLarge": "cclogo", // Large asset name (https://discord.com/developers/applications) 52 | "textLarge": "Large Text", // Large text, appears on Large Asset hover 53 | "assetSmall": "cclogo", // Small asset name (https://discord.com/developers/applications) 54 | "textSmall": "Small Text", // Small text, appears on Small Asset hover 55 | "actions": { 56 | "enabled": true, // Setting to false will disable the Discord Rich Presence actions 57 | "action1": { 58 | "name": "Website", // Name of the action 59 | "url": "https://conceptcollective.net" // URL of the action 60 | }, 61 | "action2": { 62 | "name": "Join Now", // Name of the action 63 | "url": "fivem://connect/cfx.re/join/8a3535" // URL of the action 64 | } 65 | } 66 | }, 67 | "onJoinAdaptiveCard": { 68 | "enabled": true, // Setting to false will disable the on join adaptive card module 69 | "mainTitle": "Now Joining: Concept Collective Test Server", // Main title for the adaptive card 70 | "mainDescription": "Welcome to the Concept Collective Test Server!", // Main description for the adaptive card 71 | "otherDescription": "Please make sure to read the rules and have fun!", // Other description for the adaptive card 72 | "logoURL": "https://conceptcollective.net/img/icon.png", // Logo URL for the adaptive card 73 | "logoText": "ccDiscordWrapper", // Logo text for the adaptive card 74 | "timeLocale": { 75 | "timeZone": "Australia/Perth", // Time zone for the adaptive card 76 | "timeFormat": "en-AU" // Time format for the adaptive card 77 | } // Time locale for the adaptive card 78 | 79 | }, 80 | "supportChecker": true, // Setting to false will disable the support checker module **Only change if you are experienced** 81 | "versionChecker": true // Setting to false will disable the version checker module **Only change if you are experienced** 82 | 83 | } -------------------------------------------------------------------------------- /env.example.js: -------------------------------------------------------------------------------- 1 | let sv_config = {}; 2 | 3 | sv_config.Discord_Token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // Discord Application Token Here (https://discord.com/developers/applications) 4 | sv_config.Discord_Webhook = 'https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' // Discord Webhook URL Here (https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) 5 | 6 | module.exports = sv_config; -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | -- Resource Metadata 2 | fx_version 'cerulean' 3 | games { 'rdr3', 'gta5' } 4 | 5 | author 'Concept Collective ' 6 | description 'ccDiscordWrapper - Discord Wrapper for FiveM' 7 | version '1.2.3' 8 | 9 | -- What to run 10 | client_scripts { 11 | 'client/main.js', 12 | } 13 | server_scripts { 14 | 'server/main.js', 15 | 'env.js', 16 | } 17 | 18 | files { 19 | 'config.jsonc' 20 | } 21 | 22 | server_exports { 23 | 'sendNewMessage', 24 | 'webhookSendNewMessage', 25 | 'getPlayerDiscordAvatar', 26 | 'getPlayerDiscordHighestRole', 27 | 'isPlayerInDiscord', 28 | 'checkIfPlayerHasRole' 29 | } 30 | 31 | dependency { 32 | 'yarn' 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "discord.js": "14.11.0", 4 | "jsonc-parser": "^3.2.0", 5 | "node-fetch": ">=2.6.7 <3.0.0" 6 | }, 7 | "resolutions": { 8 | "@discordjs/builders": "1.6.3", 9 | "@discordjs/formatters": "0.3.1", 10 | "@discordjs/collection": "1.5.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /permissions.cfg: -------------------------------------------------------------------------------- 1 | ## This is a basic example of how to add a principal to a group ## 2 | ## This is not a full list of all the ace permissions ## 3 | ## More Information can be found here: https://forum.cfx.re/t/basic-aces-principals-overview-guide/90917 ## 4 | 5 | add_principal group.owner group.admin -------------------------------------------------------------------------------- /server/commands/playerList.soon: -------------------------------------------------------------------------------- 1 | // Coming Soon -------------------------------------------------------------------------------- /server/events/interactionCreate.js: -------------------------------------------------------------------------------- 1 | const { Events } = require('discord.js'); 2 | 3 | module.exports = { 4 | name: Events.InteractionCreate, 5 | async execute(interaction) { 6 | if (!interaction.isChatInputCommand()) return; 7 | 8 | const command = interaction.client.commands.get(interaction.commandName); 9 | 10 | if (!command) { 11 | console.warn(`No command matching "/${interaction.commandName}" was found, If you are using this token with multiple connections you can safely ignore this message.`); 12 | return; 13 | } 14 | 15 | try { 16 | await command.execute(interaction); 17 | } catch (error) { 18 | console.error(`Error executing ${interaction.commandName}`); 19 | console.error(error); 20 | } 21 | }, 22 | }; -------------------------------------------------------------------------------- /server/events/ready.js: -------------------------------------------------------------------------------- 1 | const { Events } = require('discord.js'); 2 | 3 | module.exports = { 4 | name: Events.ClientReady, 5 | once: true, 6 | async execute(client) { 7 | console.log(`^2Ready! ^1Logged in as: ^0${client.user.tag}`); 8 | 9 | const serverGuild = await client.guilds.cache.get(client.config.General.serverID); 10 | const guildChannel = await client.channels.cache.get(client.config.DiscordBot.PlayerStatus.channelID); 11 | const guildStatusChannel = await client.channels.cache.get(client.config.DiscordBot.PlayerStatus.channelCountID); 12 | 13 | if (serverGuild === undefined || guildChannel === undefined || guildStatusChannel === undefined) { 14 | if (serverGuild === undefined) { 15 | throw console.error('Unable to get your Discord Server - Please verify that the config.jsonc is correctly configured!') 16 | } if (guildChannel === undefined) { 17 | console.error('Unable to get your Player Status Channel - Please verify that the config.jsonc is correctly configured!') 18 | } if (guildStatusChannel === undefined) { 19 | console.error('Unable to get your Player Status Count Channel - Please verify that the config.jsonc is correctly configured!') 20 | } 21 | } 22 | 23 | await guildChannel.bulkDelete(1) 24 | let statusEmbed = new client.discord.EmbedBuilder() 25 | .setColor(0x0099FF) 26 | .setTitle(`${client.config.DiscordBot.PlayerStatus.embedConfig.title}`) 27 | .setURL(`${client.config.DiscordBot.PlayerStatus.embedConfig.url}`) 28 | .setAuthor({ name: `${client.user.username}`, iconURL: `${client.user.displayAvatarURL()}`, url: 'https://conceptcollective.net' }) 29 | .setDescription('Server has been started ✅') 30 | .setThumbnail(`${client.config.DiscordBot.PlayerStatus.embedConfig.thumbnail}`) 31 | .setTimestamp() 32 | .setFooter({ text: 'This message was generated by ccDiscordWrapper', iconURL: 'https://conceptcollective.net/img/icon.png', URL: 'https://conceptcollective.net' }); 33 | let playerCountNum = 0; 34 | 35 | if (client.config.DiscordBot.PlayerStatus.channelCountID !== false){ 36 | updateChannelName(playerCountNum) 37 | } 38 | let playerCount = []; 39 | let players = '**Currently Connected Player(s)**:\n'; 40 | let nameString = ''; 41 | await guildChannel.bulkDelete(1).catch(e => console.warn('Failed to delete previous message - probably becuase there isn\'t one!')); 42 | await guildChannel.send({embeds: [statusEmbed]}).then(async sentMessage => { 43 | client.statusMessage = await sentMessage; 44 | }); 45 | 46 | on('playerJoining', async (source) => { 47 | if (client.config.DiscordBot.enabled === true) { 48 | let playerDiscordID = ''; 49 | if (client.config.General.IsServerUsingQBCore === true) { 50 | playerDiscordID = client.QBCore.Functions.GetIdentifier(source, 'discord').substring(8) 51 | } else { 52 | playerDiscordID = GetPlayerIdentifierByType(source, 'discord').substring(8) 53 | } 54 | let playerName = GetPlayerName(source); 55 | if (playerDiscordID) { 56 | try { 57 | let discordMember = await serverGuild.members.fetch(playerDiscordID) 58 | client.players[playerName] = { 59 | id: discordMember.id, 60 | avatarURL: discordMember.user.avatarURL(), 61 | roles: discordMember.roles.cache.map(role => role), 62 | joinedAt: discordMember.joinedTimestamp, 63 | username: discordMember.user.username, 64 | nickname: discordMember.nickname, 65 | inGuild: true, 66 | } 67 | } catch (e) { 68 | client.players[playerName] = { 69 | id: playerDiscordID, 70 | avatarURL: null, 71 | roles: [{ "name": client.config.DiscordBot.PlayerRoles.notInDiscord }], 72 | joinedAt: null, 73 | username: null, 74 | nickname: null, 75 | inGuild: false, 76 | } 77 | console.warn('Failed to fetch member from Discord API - is the player in your Discord server?') 78 | } 79 | } else { 80 | console.warn('Unable to fetch players Discord ID from FiveM...') 81 | return client.players[playerName] = { 82 | id: null, 83 | avatarURL: null, 84 | roles: [{ "name": client.config.DiscordBot.PlayerRoles.DiscordnotFound }], 85 | joinedAt: null, 86 | username: null, 87 | nickname: null, 88 | inGuild: null, 89 | } 90 | } 91 | 92 | if (client.config.Debug === true) { 93 | console.log(`[DEBUG] Player data: ${JSON.stringify(client.players)}`) 94 | } 95 | 96 | playerCountNum++; 97 | let playerActualDiscord = client.players[playerName].id 98 | 99 | if (client.config.DiscordBot.PlayerStatus.listPlayers === true){ 100 | nameString = `‣ ${playerName} - <@${playerActualDiscord}>`; 101 | playerCount.push(nameString) 102 | playerCount.forEach(player => { 103 | players += `${player}\n`; 104 | }) 105 | updatePlayerNames(players, statusEmbed) 106 | } if (client.config.DiscordBot.PlayerStatus.channelCountID !== false){ 107 | updateChannelName(playerCountNum) 108 | } else { 109 | statusEmbed.setDescription(`There are currently ${playerCountNum} players connected to the server!`) 110 | } 111 | 112 | if (client.config.DiscordBot.PlayerAcePermissions.enabled === true) { 113 | client.config.DiscordBot.PlayerAcePermissions.roleList.forEach(role => { 114 | let playerCheck = doesPlayerHaveRole(role.roleID, source) 115 | let playerRole = client.players[playerName].roles.filter(rolef => rolef.id === role.roleID)[0] 116 | if (playerCheck === true) { 117 | console.log(`^2${playerName} ^0has the role ^3${playerRole.name} ^0- ^8adding ACE permission ^1${role.aceGroup}^0!`) 118 | ExecuteCommand(`add_principal identifier.discord:${playerDiscordID} ${role.aceGroup}`) 119 | } 120 | }) 121 | } 122 | } 123 | if (client.config.DiscordRPC.enabled === true) { 124 | emitNet("onServerConfigLoad", source, client.config) 125 | } 126 | }) 127 | on('playerDropped', (reason) => { 128 | playerCountNum--; 129 | let playerActualDiscord = GetPlayerIdentifierByType(source, 'discord').substring(8) 130 | let playerName = GetPlayerName(source); 131 | delete client.players[playerName]; 132 | if (client.config.Debug === true) { 133 | console.log(`[DEBUG] Player data: ${JSON.stringify(client.players)}`) 134 | } 135 | if (client.config.DiscordBot.PlayerStatus.listPlayers === true){ 136 | nameString = `‣ ${GetPlayerName(source)} - <@${playerActualDiscord}>`; 137 | playerCount = playerCount.filter( e => e !== nameString); 138 | players = '**Currently Connected Player(s)**:\n'; 139 | playerCount.forEach(player => { 140 | players += `${player}\n`; 141 | }) 142 | updatePlayerNames(players, statusEmbed) 143 | } if (client.config.DiscordBot.PlayerStatus.channelCountID !== false){ 144 | updateChannelName(playerCountNum) 145 | } else { 146 | statusEmbed.setDescription(`There are currently ${playerCount} players connected to the server!`) 147 | } 148 | }) 149 | 150 | function updateChannelName(playerCountNumber){ 151 | if (playerCountNumber === 1){ 152 | guildStatusChannel.edit({ name: `🦲|${playerCountNumber}-player` }); 153 | } else { 154 | guildStatusChannel.edit({ name: `🦲|${playerCountNumber}-players` }); 155 | } 156 | } 157 | 158 | function updatePlayerNames(players, statusEmbed) { 159 | if (players === '**Currently Connected Player(s)**:\n'){ 160 | statusEmbed.setDescription('**There are currently no players connected!**'); 161 | } else { 162 | statusEmbed.setDescription(players); 163 | }; 164 | client.statusMessage.edit({embeds: [statusEmbed]}); 165 | } 166 | 167 | function doesPlayerHaveRole(roleID, source){ 168 | let playerName = GetPlayerName(source); 169 | let playerRoles = client.players[playerName].roles 170 | let playerHasRole = false 171 | playerRoles.forEach(role => { 172 | if (role.id === roleID){ 173 | playerHasRole = true 174 | } 175 | }) 176 | return playerHasRole 177 | } 178 | 179 | }, 180 | }; -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | const fetch = require('node-fetch'); 4 | const sleep = m => new Promise(r => setTimeout(r, m)) 5 | 6 | // Serverside Configuration 7 | const root = GetResourcePath(GetCurrentResourceName()); 8 | let env_config = require(path.join(root, 'env.js')); 9 | 10 | // Requires external module due to JSONC not being a standard JSON format 11 | const parser = require('jsonc-parser'); 12 | const config = parser.parse(LoadResourceFile('ccDiscordWrapper', 'config.jsonc')) 13 | 14 | // Framework stuff 15 | let ESX = undefined 16 | let QBCore = undefined 17 | 18 | if (config.General.IsServerUsingQBCore === true) { 19 | QBCore = exports['qb-core'].GetCoreObject() 20 | } if (config.General.IsServerUsingESX === true) { 21 | ESX = exports['es_extended'].getSharedObject() 22 | } 23 | 24 | // Discord.js initialisation for Discord Bot Module 25 | const { Client, Collection, GatewayIntentBits, EmbedBuilder, WebhookClient } = require('discord.js'); 26 | const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMembers] }); 27 | 28 | // Support Checker - Checks if the resource is named correctly 29 | on("onResourceStart", async (resourceName) => { 30 | if (GetCurrentResourceName() !== "ccDiscordWrapper" && config.supportChecker === true) { 31 | return console.warn(`^6[Warning]^0 For better support, it is recommended that "${GetCurrentResourceName()}" be renamed to "ccDiscordWrapper"^0`); 32 | } 33 | if (GetCurrentResourceName() === resourceName && config.versionChecker === true){ 34 | const response = await fetch('https://api.github.com/repos/Concept-Collective/ccDiscordWrapper/releases/latest') 35 | const json = await response.json() 36 | if (json.message && json.message.includes('API rate limit exceeded')) { 37 | return console.warn(`^6[Warning]^0 ccDiscordWrapper version checker is currently unavailable due to GitHub API rate limiting. Please try again later.^0`) 38 | } 39 | if (json.tag_name !== `v${GetResourceMetadata(GetCurrentResourceName(), 'version', 0)}`){ 40 | console.warn(`^3[WARNING]^0 ccDiscordWrapper is out of date! Please update to the latest version: ^2${json.tag_name}^0`) 41 | } else { 42 | console.log(`^2[INFO]^0 ccDiscordWrapper is up to date [^2v${GetResourceMetadata(GetCurrentResourceName(), 'version', 0)}^0]!`) 43 | } 44 | } 45 | 46 | if (GetCurrentResourceName() === resourceName && config.General.disableHardCap === true) { 47 | StopResource("hardcap"); 48 | } 49 | 50 | if (GetCurrentResourceName() === resourceName && config.General.compatibilityMode === true) { 51 | StopResource("connectqueue"); 52 | StopResource("zqueue"); 53 | StopResource("bad-discordqueue"); 54 | } 55 | }); 56 | 57 | // Discord Bot Module 58 | if (config.DiscordBot.enabled === true){ 59 | discordProcess(); 60 | } 61 | 62 | function discordProcess() { 63 | client.commands = new Collection(); 64 | client.QBCore = QBCore; 65 | client.ESX = ESX; 66 | client.discord = require('discord.js'); 67 | client.config = config; 68 | client.players = {}; 69 | client.statusMessage = null; 70 | 71 | const commandsPath = path.join(root, 'server', 'commands'); 72 | const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); 73 | 74 | for (const file of commandFiles) { 75 | const filePath = path.join(commandsPath, file); 76 | const command = require(filePath); 77 | // Set a new item in the Collection with the key as the command name and the value as the exported module 78 | if ('data' in command && 'execute' in command) { 79 | client.commands.set(command.data.name, command); 80 | } else { 81 | console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); 82 | } 83 | } 84 | 85 | const eventsPath = path.join(root, 'server', 'events'); 86 | const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js')); 87 | 88 | for (const file of eventFiles) { 89 | const filePath = path.join(eventsPath, file); 90 | const event = require(filePath); 91 | if (event.once) { 92 | client.once(event.name, (...args) => event.execute(...args)); 93 | } else { 94 | client.on(event.name, (...args) => event.execute(...args)); 95 | } 96 | } 97 | 98 | client.login(env_config.Discord_Token); 99 | 100 | // Discord Bot Module - senNewMessage function 101 | async function botSendNewMessage(channelId, message){ 102 | const channel = await client.channels.cache.get(channelId); 103 | getGuildUsersandRolesforIngamePlayers(channel.guild.id) 104 | let newEmbed = new client.discord.EmbedBuilder() 105 | .setColor(0x0099FF) 106 | .setTitle(`${message[0]}`) 107 | .setDescription(`${message[1]}`) 108 | .setTimestamp() 109 | .setFooter({ text: 'This message was generated by ccDiscordWrapper', iconURL: 'https://conceptcollective.net/img/icon.png', URL: 'https://conceptcollective.net' }); 110 | await channel.send({embeds: [newEmbed]}); 111 | } 112 | 113 | async function webhookSendNewMessage(color, name, message, footer) { 114 | const webhookClient = new WebhookClient({ url: env_config.Discord_Webhook }); 115 | let embed = new EmbedBuilder() 116 | .setColor(color) 117 | .setTitle(name) 118 | .setDescription(message) 119 | .setTimestamp() 120 | .setFooter({ text: `${footer} | Generated by ccDiscordWrapper`, iconURL: 'https://conceptcollective.net/img/icon.png', URL: 'https://conceptcollective.net' }); 121 | 122 | webhookClient.send({embeds: [embed]}); 123 | } 124 | 125 | function isPlayerInDiscord(source) { 126 | let playerName = GetPlayerName(source); 127 | let isPlayerInGuild = client.players[playerName].inGuild 128 | return isPlayerInGuild 129 | } 130 | 131 | function getPlayerDiscordAvatar(source) { 132 | let playerName = GetPlayerName(source); 133 | let avatarURL = client.players[playerName].avatarURL 134 | return avatarURL 135 | } 136 | 137 | function getPlayerDiscordRoles(source) { 138 | let playerName = GetPlayerName(source); 139 | let roles = client.players[playerName].roles 140 | return roles 141 | } 142 | 143 | function getPlayerDiscordHighestRole(source, type) { 144 | let playerName = GetPlayerName(source); 145 | if (type === "name") { 146 | let highestRole = client.players[playerName].roles[0].name 147 | return highestRole 148 | } else { 149 | let highestRole = client.players[playerName].roles[0] 150 | return highestRole 151 | } 152 | } 153 | 154 | function checkIfPlayerHasRole(source, role, type) { 155 | let playerName = GetPlayerName(source); 156 | if (type === "name"){ 157 | let hasRole = client.players[playerName].roles.filter(rolef => rolef.name === role) 158 | return hasRole 159 | } else if (type === "id"){ 160 | let hasRole = client.players[playerName].roles.filter(rolef => rolef.id === role) 161 | return hasRole 162 | } 163 | } 164 | 165 | function checkIfPlayerIsWhitelisted(discordID, response) { 166 | const serverGuild = client.guilds.cache.get(client.config.General.serverID); 167 | const serverGuildMember = serverGuild.members.cache.get(discordID); 168 | const doesPlayerHaveRole = serverGuildMember.roles.cache.get(client.config.DiscordBot.DiscordWhitelist.roleID) 169 | 170 | if (config.Debug === true) { 171 | console.log(`[DEBUG] Guild Data: ${JSON.stringify(serverGuild.toJSON())}`); 172 | console.log(`[DEBUG] Guild Member Data: ${JSON.stringify(serverGuildMember.toJSON())}`) 173 | } 174 | if (response === 'roles'){ 175 | let memberRoles = serverGuildMember.roles.cache.map(role => role) 176 | return JSON.stringify(memberRoles) 177 | } 178 | if (doesPlayerHaveRole !== undefined) { 179 | if (doesPlayerHaveRole.id === client.config.DiscordBot.DiscordWhitelist.roleID) { 180 | return true 181 | } 182 | } else { 183 | return false 184 | } 185 | } 186 | 187 | exports('botSendNewMessage', botSendNewMessage); 188 | exports('webhookSendNewMessage', webhookSendNewMessage); 189 | exports('getPlayerDiscordAvatar', getPlayerDiscordAvatar); 190 | exports('getPlayerDiscordHighestRole', getPlayerDiscordHighestRole); 191 | exports('isPlayerInDiscord', isPlayerInDiscord); 192 | exports('checkIfPlayerHasRole', checkIfPlayerHasRole); 193 | exports('getPlayerDiscordRoles', getPlayerDiscordRoles); 194 | exports('checkIfPlayerIsWhitelisted', checkIfPlayerIsWhitelisted) 195 | } 196 | 197 | let Queue = {} 198 | Queue.MaxPlayers = GetConvarInt("sv_maxclients", 48) 199 | Queue.Players = [] 200 | 201 | Queue.QueuePriority = config.DiscordBot.DiscordConnectQueue.rolePriority.length 202 | if (config.onJoinAdaptiveCard.enabled === true){ 203 | 204 | on('playerConnecting', async (playerName, setKickReason, deferrals) => { 205 | let playerDiscordID = ''; 206 | let playerSteamID = ''; 207 | if (config.General.IsServerUsingQBCore === true) { 208 | playerDiscordID = QBCore.Functions.GetIdentifier(source, 'discord') 209 | } else { 210 | playerDiscordID = GetPlayerIdentifierByType(source, 'discord') 211 | } 212 | if (config.General.IsDiscordRequired === true){ 213 | if (!playerDiscordID) { 214 | setKickReason(`\n\n🚧 Border Patrol\n\nDiscord was not found please relaunch FiveM with Discord running\n\nFor further support visit ${config.General.serverInviteURL}!`) 215 | CancelEvent() 216 | return 217 | } 218 | } 219 | if (config.General.IsSteamRequired === true){ 220 | if (config.General.IsServerUsingQBCore === true) { 221 | playerSteamID = QBCore.Functions.GetIdentifier(source, 'steam') 222 | } else { 223 | playerSteamID = GetPlayerIdentifierByType(source, 'steam') 224 | } 225 | if (!playerSteamID) { 226 | if (config.General.IsServerUsingQBCore === true) { 227 | console.warn('[WARN] QBCore has removed the ability to get steam identifiers, please use a different framework or disable the steam requirement in config.jsonc!') 228 | } else { 229 | setKickReason(`\n\n🚧 Border Patrol\n\nSteam was not found please relaunch FiveM with Steam running\n\nFor further support visit ${config.General.serverInviteURL}!`) 230 | CancelEvent() 231 | return 232 | } 233 | } 234 | } 235 | playerDiscordID = playerDiscordID.substring(8) 236 | playerSteamID = playerSteamID.substring(7) 237 | if (config.DiscordBot.DiscordWhitelist.enabled === true && config.DiscordBot.enabled === true){ 238 | let isPlayerWhitelisted = exports.ccDiscordWrapper.checkIfPlayerIsWhitelisted(playerDiscordID, 'boolean') 239 | if (isPlayerWhitelisted === false){ 240 | console.warn(`^5Player ^3${playerName} ^5has attempted to join the server but ^8is not whitelisted and has been kicked^0!`) 241 | setKickReason(`\n\n🚧 Border Patrol\n\nYou are not whitelisted therefore access to this server is prohibited\n\nFor further support visit ${config.General.serverInviteURL}!`) 242 | CancelEvent() 243 | return 244 | } 245 | } else if (config.DiscordBot.DiscordWhitelist.enabled === true && config.DiscordBot.enabled !== true) { 246 | return console.warn('Discord Whitelist is enabled but Discord Bot module is not enabled therefor it will not work!') 247 | } 248 | let adaptiveCard = { 249 | "type": "AdaptiveCard", 250 | "version": "1.6", 251 | "body": [ 252 | { 253 | "type": "ColumnSet", 254 | "columns": [ 255 | { 256 | "type": "Column", 257 | "width": "20px" 258 | }, 259 | { 260 | "type": "Column", 261 | "width": "stretch", 262 | "items": [ 263 | { 264 | "type": "TextBlock", 265 | "text": `${config.onJoinAdaptiveCard.mainTitle}`, 266 | "wrap": true, 267 | "style": "heading", 268 | "horizontalAlignment": "Center", 269 | "size": "ExtraLarge", 270 | "maxLines": 1 271 | } 272 | ] 273 | } 274 | ] 275 | }, 276 | { 277 | "type": "Container", 278 | "items": [ 279 | { 280 | "type": "ColumnSet", 281 | "columns": [ 282 | { 283 | "type": "Column", 284 | "width": "auto", 285 | "items": [ 286 | { 287 | "type": "Image", 288 | "url": `${config.onJoinAdaptiveCard.logoURL}`, 289 | "size": "Medium", 290 | "altText": "Concept Collective Logo", 291 | "spacing": "None", 292 | "horizontalAlignment": "Center" 293 | }, 294 | { 295 | "type": "TextBlock", 296 | "text": `${config.onJoinAdaptiveCard.logoText}`, 297 | "horizontalAlignment": "Center", 298 | "weight": "Bolder", 299 | "wrap": true 300 | } 301 | ] 302 | }, 303 | { 304 | "type": "Column", 305 | "width": "stretch", 306 | "separator": true, 307 | "spacing": "Medium", 308 | "items": [ 309 | { 310 | "type": "TextBlock", 311 | "text": `${new Date().toLocaleString(config.onJoinAdaptiveCard.timeLocale.timeFormat, { timeZone: config.onJoinAdaptiveCard.timeLocale.timeZone })}`, 312 | "horizontalAlignment": "Center", 313 | "wrap": true 314 | }, 315 | { 316 | "type": "TextBlock", 317 | "text": `${config.onJoinAdaptiveCard.mainDescription}`, 318 | "size": "Medium", 319 | "horizontalAlignment": "Center", 320 | "wrap": true 321 | }, 322 | { 323 | "type": "TextBlock", 324 | "text": `${config.onJoinAdaptiveCard.otherDescription}`, 325 | "size": "Large", 326 | "horizontalAlignment": "Center", 327 | "style": "heading", 328 | "wrap": true 329 | } 330 | ] 331 | }, 332 | { 333 | "type": "Column", 334 | "width": "100px" 335 | } 336 | ] 337 | } 338 | ] 339 | }, 340 | { 341 | "type": "ColumnSet", 342 | "columns": [ 343 | { 344 | "type": "Column", 345 | "width": "240px" 346 | }, 347 | { 348 | "type": "Column", 349 | "width": "150px", 350 | "items": [ 351 | { 352 | "type": "ActionSet", 353 | "actions": [ 354 | { 355 | "type": "Action.OpenUrl", 356 | "title": "Discord", 357 | "url": "https://discord.conceptcollective.net", 358 | "style": "positive" 359 | } 360 | ], 361 | "spacing": "None", 362 | "horizontalAlignment": "Center" 363 | } 364 | ] 365 | }, 366 | { 367 | "type": "Column", 368 | "width": "135px", 369 | "items": [ 370 | { 371 | "type": "ActionSet", 372 | "actions": [ 373 | { 374 | "type": "Action.Submit", 375 | "title": "Play Now!", 376 | "style": "positive", 377 | "id": "playSubmit" 378 | } 379 | ] 380 | } 381 | ] 382 | }, 383 | { 384 | "type": "Column", 385 | "width": "150px", 386 | "items": [ 387 | { 388 | "type": "ActionSet", 389 | "actions": [ 390 | { 391 | "type": "Action.OpenUrl", 392 | "title": "Website", 393 | "style": "positive", 394 | "url": "https://conceptcollective.net" 395 | } 396 | ] 397 | } 398 | ] 399 | } 400 | ] 401 | } 402 | ] 403 | } 404 | deferrals.defer() 405 | deferrals.update(`Hello ${playerName}. Your Discord ID is being checked...`) 406 | await sleep(1000); 407 | deferrals.presentCard(adaptiveCard, function(data, rawData) { 408 | if (config.Debug === true) { 409 | console.log(`[DEBUG] Adaptive Card Data: ${JSON.stringify(data)}`) 410 | } 411 | if (data.submitId === 'playSubmit') { 412 | if (config.DiscordBot.DiscordConnectQueue.enabled === true) { 413 | deferrals.update(`You have been added to the queue - Please wait...`) 414 | let playerDiscordRoles = JSON.parse(exports.ccDiscordWrapper.checkIfPlayerIsWhitelisted(playerDiscordID, 'roles')) 415 | let queuePriority = config.DiscordBot.DiscordConnectQueue.rolePriority.length + 1 416 | config.DiscordBot.DiscordConnectQueue.rolePriority.forEach((role, index) => { 417 | let doesPlayerHavePriorityRole = playerDiscordRoles.filter(rolef => rolef.id === role) 418 | if (doesPlayerHavePriorityRole.length > 0) { 419 | if (index < queuePriority) { 420 | queuePriority = index / Queue.QueuePriority.length 421 | queueMath = Math.round(queuePriority * Queue.Players.length) 422 | let newArray = [...Queue.Players.slice(0, queueMath), playerName, ...Queue.Players.slice(queueMath)] 423 | Queue.Players = newArray 424 | } 425 | } 426 | }) 427 | if (Queue.Players.filter(player => player === playerName).length === 0) { 428 | Queue.Players.push(playerName) 429 | } 430 | const queueCheck = setInterval(function() { 431 | if (Queue.Players[0] === playerName) { 432 | if (config.Debug === true) { 433 | console.log(`[DEBUG] Player ${playerName} has just finished in the queue.`) 434 | } 435 | deferrals.done() 436 | Queue.Players.shift() 437 | clearInterval(queueCheck) 438 | } else { 439 | deferrals.update(`You are currently ${Queue.Players.indexOf(playerName) + 1} out of ${Queue.Players.length} in the queue - Please wait...`) 440 | } 441 | }, 1000) 442 | } else { 443 | deferrals.done() 444 | } 445 | } 446 | }) 447 | }); 448 | 449 | } -------------------------------------------------------------------------------- /server/register.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Concept-Collective/ccDiscordWrapper/3b7cefaafd013520d524991c4454ca6df3069c6b/server/register.js --------------------------------------------------------------------------------