Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: modified discord i/o in the core package #62

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 216 additions & 5 deletions packages/core/src/core/io/discord.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {
ChannelType,
Client,
Events,
GatewayIntentBits,
Events,
Message,
Partials,
TextChannel,
Message,
GuildMember,
type Channel,
} from "discord.js";
import { z } from "zod";
import { Logger } from "../../core/logger";
import {
HandlerRole,
Expand All @@ -16,7 +18,6 @@ import {
type ProcessableContent,
} from "../types";
import { env } from "../../core/env";
import { z } from "zod";

export interface DiscordCredentials {
discord_token: string;
Expand All @@ -30,6 +31,32 @@ export interface MessageData {
sendBy?: string;
}

export interface ReactionData {
emoji: string;
messageId: string;
userId: string;
}

export interface GuildMemberData {
userId: string;
username: string;
action: "join" | "leave";
}

export interface RoleData {
guildId: string;
userId: string;
roleId: string;
action: "add" | "remove";
}

export interface VoiceStateData {
userId: string;
channelId: string;
action: "join" | "leave" | "move";
}

// Schema for message output validation
export const messageSchema = z.object({
content: z.string().describe("The content of the message"),
channelId: z.string().describe("The channel ID where the message is sent"),
Expand Down Expand Up @@ -138,6 +165,24 @@ export class DiscordClient {
this.logger.info("DiscordClient", "Client destroyed");
}

/**
* Create an output for monitoring joining or leaving of guild members
*/
public createGuildMemberInput() {
return {
name: "discord_guild_members",
handler: async () => {
return this.monitorGuildMembers();
},
response: {
type: "string",
userId: "string",
username: "string",
action: "string",
},
};
}

/**
* Create an output for sending messages (useful for Orchestrator OUTPUT handlers).
*/
Expand All @@ -155,6 +200,39 @@ export class DiscordClient {
};
}

/**
* Create an output for managing reactions on messages
*/
public createRoleOutput() {
return {
name: "discord_role",
handler: async (data: RoleData) => {
return await this.manageRole(data);
},
response: {
success: "boolean",
},
};
}

/**
* Create an output for monitoring voice states
*/
public createVoiceStateInput() {
return {
name: "discord_voice_states",
handler: async () => {
return this.monitorVoiceStates();
},
response: {
type: "string",
userId: "string",
channelId: "string",
action: "string",
},
};
}

private getIsValidTextChannel(channel?: Channel): channel is TextChannel {
return channel?.type === ChannelType.GuildText;
}
Expand Down Expand Up @@ -207,8 +285,9 @@ export class DiscordClient {
throw error;
}

const sentMessage = await channel.send(data.content);

const sentMessage = await channel.send(
data.content
);
return {
success: true,
messageId: sentMessage.id,
Expand All @@ -229,4 +308,136 @@ export class DiscordClient {
};
}
}

private async monitorGuildMembers(): Promise<GuildMemberData> {
return new Promise((resolve, reject) => {
try {
this.logger.debug(
"DiscordClient.monitorGuildMembers",
"Monitoring guild members"
);

const handleMemberEvent = (
member: GuildMember,
action: "join" | "leave"
) => {
const memberData: GuildMemberData = {
userId: member.id,
username: member.user.username,
action,
};
resolve(memberData);
};

this.client?.on("guildMemberAdd", (member) =>
handleMemberEvent(member as GuildMember, "join")
);
this.client?.on("guildMemberRemove", (member) =>
handleMemberEvent(member as GuildMember, "leave")
);
} catch (error) {
this.logger.error(
"DiscordClient.monitorGuildMembers",
"Error monitoring guild members",
{ error }
);
reject(error);
}
});
}
Comment on lines +312 to +347
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix memory leak in guild member monitoring.

The monitorGuildMembers method doesn't clean up event listeners, potentially causing memory leaks.

 private async monitorGuildMembers(): Promise<GuildMemberData> {
     return new Promise((resolve, reject) => {
         try {
             this.logger.debug(
                 "DiscordClient.monitorGuildMembers",
                 "Monitoring guild members"
             );

+            const cleanup = () => {
+                this.client?.off("guildMemberAdd", handleMemberAdd);
+                this.client?.off("guildMemberRemove", handleMemberRemove);
+            };
+
+            const handleMemberAdd = (member: GuildMember) => {
+                cleanup();
+                handleMemberEvent(member, "join");
+            };
+
+            const handleMemberRemove = (member: GuildMember) => {
+                cleanup();
+                handleMemberEvent(member, "leave");
+            };

             const handleMemberEvent = (
                 member: GuildMember,
                 action: "join" | "leave"
             ) => {
                 const memberData: GuildMemberData = {
                     userId: member.id,
                     username: member.user.username,
                     action,
                 };
                 resolve(memberData);
             };

-            this.client?.on("guildMemberAdd", (member) =>
-                handleMemberEvent(member as GuildMember, "join")
-            );
-            this.client?.on("guildMemberRemove", (member) =>
-                handleMemberEvent(member as GuildMember, "leave")
-            );
+            this.client?.on("guildMemberAdd", handleMemberAdd);
+            this.client?.on("guildMemberRemove", handleMemberRemove);
+
+            // Add timeout to prevent hanging
+            setTimeout(() => {
+                cleanup();
+                reject(new Error('Guild member monitoring timed out'));
+            }, 60000);
         } catch (error) {
+            cleanup?.();
             this.logger.error(
                 "DiscordClient.monitorGuildMembers",
                 "Error monitoring guild members",
                 { error }
             );
             reject(error);
         }
     });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async monitorGuildMembers(): Promise<GuildMemberData> {
return new Promise((resolve, reject) => {
try {
this.logger.debug(
"DiscordClient.monitorGuildMembers",
"Monitoring guild members"
);
const handleMemberEvent = (
member: GuildMember,
action: "join" | "leave"
) => {
const memberData: GuildMemberData = {
userId: member.id,
username: member.user.username,
action,
};
resolve(memberData);
};
this.client?.on("guildMemberAdd", (member) =>
handleMemberEvent(member as GuildMember, "join")
);
this.client?.on("guildMemberRemove", (member) =>
handleMemberEvent(member as GuildMember, "leave")
);
} catch (error) {
this.logger.error(
"DiscordClient.monitorGuildMembers",
"Error monitoring guild members",
{ error }
);
reject(error);
}
});
}
private async monitorGuildMembers(): Promise<GuildMemberData> {
return new Promise((resolve, reject) => {
try {
this.logger.debug(
"DiscordClient.monitorGuildMembers",
"Monitoring guild members"
);
const cleanup = () => {
this.client?.off("guildMemberAdd", handleMemberAdd);
this.client?.off("guildMemberRemove", handleMemberRemove);
};
const handleMemberAdd = (member: GuildMember) => {
cleanup();
handleMemberEvent(member, "join");
};
const handleMemberRemove = (member: GuildMember) => {
cleanup();
handleMemberEvent(member, "leave");
};
const handleMemberEvent = (
member: GuildMember,
action: "join" | "leave"
) => {
const memberData: GuildMemberData = {
userId: member.id,
username: member.user.username,
action,
};
resolve(memberData);
};
this.client?.on("guildMemberAdd", handleMemberAdd);
this.client?.on("guildMemberRemove", handleMemberRemove);
// Add timeout to prevent hanging
setTimeout(() => {
cleanup();
reject(new Error('Guild member monitoring timed out'));
}, 60000);
} catch (error) {
cleanup?.();
this.logger.error(
"DiscordClient.monitorGuildMembers",
"Error monitoring guild members",
{ error }
);
reject(error);
}
});
}


private async manageRole(data: RoleData) {
try {
this.logger.info("DiscordClient.manageRole", "Would manage role", {
data,
});

if (env.DRY_RUN) {
return {
success: true,
};
}

const guild = this.client.guilds.cache.get(data.guildId);
if (!guild) {
throw new Error("Guild not found");
}

const member = await guild.members.fetch(data.userId);
const role = await guild.roles.fetch(data.roleId);

if (!role) {
throw new Error("Role not found");
}

if (data.action === "add") {
await member.roles.add(role);
} else if (data.action === "remove") {
await member.roles.remove(role);
}

return {
success: true,
};
} catch (error) {
this.logger.error(
"DiscordClient.manageRole",
"Error managing role",
{
error,
}
);
throw error;
}
}

private async monitorVoiceStates() {
try {
this.logger.debug(
"DiscordClient.monitorVoiceStates",
"Monitoring voice states"
);

const voiceStates: VoiceStateData[] = [];

this.client.on("voiceStateUpdate", (oldState, newState) => {
if (oldState.channelId !== newState.channelId) {
voiceStates.push({
userId: newState.id,
channelId: newState.channelId || "unknown",
action: oldState.channelId
? newState.channelId
? "move"
: "leave"
: "join",
});
}
});

return voiceStates;
} catch (error) {
this.logger.error(
"DiscordClient.monitorVoiceStates",
"Error monitoring voice states",
{
error,
}
);
throw error;
}
}
}

// Example usage:
/*
const discord = new DiscordClient({
discord_token: process.env.DISCORD_TOKEN || "",
});

// Register inputs
core.createMessageInput("CHANNEL_ID");
core.createGuildMemberInput();

// Register output
core.registerOutput(discord.createMessageOutput());
*/