From c50c907e9d96b6a3b74b8fb02e47444f71c17f33 Mon Sep 17 00:00:00 2001
From: David Malchin <malchin459@gmail.com>
Date: Tue, 10 Sep 2024 05:10:13 +0300
Subject: [PATCH] Add zdiscord bridge

---
 discordbot.cfg                                |  3 +
 fxmanifest.lua                                |  2 +
 server/components/loader.ts                   |  7 +-
 .../zdiscord/events/guild-member-add.ts       | 19 +++++
 .../zdiscord/events/guild-member-remove.ts    | 14 ++++
 .../zdiscord/events/guild-member-update.ts    | 20 +++++
 server/components/zdiscord/exports.ts         | 73 +++++++++++++++++++
 server/components/zdiscord/index.ts           |  8 ++
 server/utils/env.ts                           |  6 +-
 9 files changed, 149 insertions(+), 3 deletions(-)
 create mode 100644 server/components/zdiscord/events/guild-member-add.ts
 create mode 100644 server/components/zdiscord/events/guild-member-remove.ts
 create mode 100644 server/components/zdiscord/events/guild-member-update.ts
 create mode 100644 server/components/zdiscord/exports.ts
 create mode 100644 server/components/zdiscord/index.ts

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<string, ApplicationCommand>(),
@@ -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<GatewayDispatchEvents.GuildMemberAdd>;
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<GatewayDispatchEvents.GuildMemberRemove>;
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<GatewayDispatchEvents.GuildMemberUpdate>;
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<Snowflake, ZDiscordMemberData>();
+
+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,
 });