diff --git a/discordbot.cfg b/discordbot.cfg index b9e552b..1b05b1d 100644 --- a/discordbot.cfg +++ b/discordbot.cfg @@ -9,3 +9,6 @@ set discordbot:botToken "YOUR_BOT_TOKEN_HERE" # The ID of the Discord server to use for server-specific commands. set discordbot:guildId "YOUR_SERVER_ID_HERE" + +# Whether to provide zdiscord exports. +set discordbot:zdiscordBridge false diff --git a/fxmanifest.lua b/fxmanifest.lua index f9d9ec2..9b268c3 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -11,3 +11,5 @@ server_only 'yes' server_scripts { 'dist/server.js', } + +provide 'zdiscord' diff --git a/server/components/loader.ts b/server/components/loader.ts index 181115f..7c5193f 100644 --- a/server/components/loader.ts +++ b/server/components/loader.ts @@ -5,7 +5,7 @@ import { } from '@discordjs/core'; import EventEmitter from 'node:events'; import { inspect } from 'node:util'; -import { client, gateway, rest } from '../utils/env.js'; +import { client, gateway, rest, zdiscordBridge } from '../utils/env.js'; import { isStatefulInteraction } from '../utils/stateful.js'; import core from './core/index.js'; import ping from './ping/index.js'; @@ -17,6 +17,7 @@ import { MessageComponent, Modal, } from './types.js'; +import zdiscord from './zdiscord/index.js'; export const interactions = { commands: new Collection(), @@ -85,4 +86,8 @@ function loadComponent({ export function loadComponents() { loadComponent(core); loadComponent(ping); + if (zdiscordBridge) { + loadComponent(zdiscord); + console.log('zdiscord bridge enabled'); + } } diff --git a/server/components/zdiscord/events/guild-member-add.ts b/server/components/zdiscord/events/guild-member-add.ts new file mode 100644 index 0000000..54d3396 --- /dev/null +++ b/server/components/zdiscord/events/guild-member-add.ts @@ -0,0 +1,19 @@ +import { GatewayDispatchEvents } from '@discordjs/core'; +import { guildId } from '../../../utils/env.js'; +import { GatewayEvent } from '../../types.js'; +import { members } from '../exports.js'; + +export const guildMemberAdd = { + name: GatewayDispatchEvents.GuildMemberAdd, + type: 'on', + async execute({ data: member }) { + if (!guildId || member.guild_id !== guildId) return; + + members.set(member.user.id, { + nick: member.nick, + globalName: member.user.global_name, + username: member.user.username, + roles: member.roles, + }); + }, +} satisfies GatewayEvent; diff --git a/server/components/zdiscord/events/guild-member-remove.ts b/server/components/zdiscord/events/guild-member-remove.ts new file mode 100644 index 0000000..49e78d0 --- /dev/null +++ b/server/components/zdiscord/events/guild-member-remove.ts @@ -0,0 +1,14 @@ +import { GatewayDispatchEvents } from '@discordjs/core'; +import { guildId } from '../../../utils/env.js'; +import { GatewayEvent } from '../../types.js'; +import { members } from '../exports.js'; + +export const guildMemberRemove = { + name: GatewayDispatchEvents.GuildMemberRemove, + type: 'on', + async execute({ data: member }) { + if (!guildId || member.guild_id !== guildId) return; + + members.delete(member.user.id); + }, +} satisfies GatewayEvent; diff --git a/server/components/zdiscord/events/guild-member-update.ts b/server/components/zdiscord/events/guild-member-update.ts new file mode 100644 index 0000000..0f2f630 --- /dev/null +++ b/server/components/zdiscord/events/guild-member-update.ts @@ -0,0 +1,20 @@ +import { GatewayDispatchEvents } from '@discordjs/core'; +import { guildId } from '../../../utils/env.js'; +import { GatewayEvent } from '../../types.js'; +import { members } from '../exports.js'; + +export const guildMemberUpdate = { + name: GatewayDispatchEvents.GuildMemberUpdate, + type: 'on', + async execute({ data }) { + if (!guildId || data.guild_id !== guildId) return; + + const member = members.get(data.user.id)!; + member.nick = data.nick; + member.globalName = data.user.global_name; + member.username = data.user.username; + member.roles = data.roles; + + console.log(data.nick); + }, +} satisfies GatewayEvent; diff --git a/server/components/zdiscord/exports.ts b/server/components/zdiscord/exports.ts new file mode 100644 index 0000000..93deebb --- /dev/null +++ b/server/components/zdiscord/exports.ts @@ -0,0 +1,73 @@ +import { Collection } from '@discordjs/collection'; +import { Snowflake } from '@discordjs/core'; +import { zdiscordBridge } from '../../utils/env.js'; + +function zdiscordExports(name: string, cb: Function) { + if (!zdiscordBridge) return; + + on(`__cfx_export_zdiscord_${name}`, (setCB: (cb: Function) => void) => + setCB(cb), + ); +} + +export interface ZDiscordMemberData { + nick: string | null | undefined; + globalName: string | null; + username: string; + roles: string[]; +} + +export const members = new Collection(); + +function getMember(userId: string): ZDiscordMemberData | false { + return members.get(userId) ?? false; +} + +function getMemberFromSource(playerId: number): ZDiscordMemberData | false { + const userId = GetPlayerIdentifierByType(`${playerId}`, 'discord'); + if (!userId) return false; + + return getMember(userId); +} + +function parseMember(memberId: string | number): ZDiscordMemberData | false { + if (!memberId) return false; + + return typeof memberId === 'number' + ? getMemberFromSource(memberId) + : getMember(memberId); +} + +zdiscordExports( + 'isRolePresent', + (memberId: string | number, role: string | string[]): boolean => { + if (!memberId || !role) return false; + + const member = parseMember(memberId); + if (!member) return false; + + return typeof role === 'object' + ? member.roles.filter((r) => role.includes(r)).length > 0 + : member.roles.includes(role); + }, +); + +zdiscordExports('getMemberRoles', (memberId: string | number): string[] => { + if (!memberId) return []; + + const member = parseMember(memberId); + if (!member) return []; + + return member.roles; +}); + +zdiscordExports('getName', (memberId: string | number): string | false => { + const member = parseMember(memberId); + if (!member) return false; + + return member.nick ?? member.globalName ?? member.username; +}); + +zdiscordExports('getDiscordId', (playerId: number): string | false => { + return GetPlayerIdentifierByType(`${playerId}`, 'discord') || false; +}); diff --git a/server/components/zdiscord/index.ts b/server/components/zdiscord/index.ts new file mode 100644 index 0000000..52ec5d1 --- /dev/null +++ b/server/components/zdiscord/index.ts @@ -0,0 +1,8 @@ +import { Component } from '../types.js'; +import { guildMemberAdd } from './events/guild-member-add.js'; +import { guildMemberRemove } from './events/guild-member-remove.js'; +import { guildMemberUpdate } from './events/guild-member-update.js'; + +export default { + gatewayEvents: [guildMemberAdd, guildMemberRemove, guildMemberUpdate], +} satisfies Component; diff --git a/server/utils/env.ts b/server/utils/env.ts index febf8aa..e502944 100644 --- a/server/utils/env.ts +++ b/server/utils/env.ts @@ -1,14 +1,16 @@ -import { Client } from '@discordjs/core'; +import { Client, GatewayIntentBits } from '@discordjs/core'; import { REST } from '@discordjs/rest'; import { WebSocketManager } from '@discordjs/ws'; export const botToken = GetConvar('discordbot:botToken', ''); export const guildId = GetConvar('discordbot:guildId', ''); +export const zdiscordBridge = + GetConvarInt('discordbot:zdiscordBridge', 0) === 1; export const rest = new REST({ version: '10' }).setToken(botToken); export const gateway = new WebSocketManager({ token: botToken, - intents: 0, + intents: zdiscordBridge ? GatewayIntentBits.GuildMembers : 0, rest: rest, });