summaryrefslogtreecommitdiff
path: root/packages/gateway/src/commands
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-24 19:04:14 -0700
committerFuwn <[email protected]>2025-09-24 19:15:34 -0700
commit3b08854f33c9944761367597e6850fe6e27e3af3 (patch)
tree8b6169d08cc85b0a6a74c761615c2a8e560ac105 /packages/gateway/src/commands
parentrefactor: Move interactions client to packages directory (diff)
downloadumabotdiscord-3b08854f33c9944761367597e6850fe6e27e3af3.tar.xz
umabotdiscord-3b08854f33c9944761367597e6850fe6e27e3af3.zip
feat: Integrate gateway client
Diffstat (limited to 'packages/gateway/src/commands')
-rw-r--r--packages/gateway/src/commands/index.ts8
-rw-r--r--packages/gateway/src/commands/say.ts127
-rw-r--r--packages/gateway/src/commands/start.ts83
-rw-r--r--packages/gateway/src/commands/utilities.ts615
4 files changed, 833 insertions, 0 deletions
diff --git a/packages/gateway/src/commands/index.ts b/packages/gateway/src/commands/index.ts
new file mode 100644
index 0000000..c94db00
--- /dev/null
+++ b/packages/gateway/src/commands/index.ts
@@ -0,0 +1,8 @@
+import { Client } from "discord.js";
+import { handleSayCommand } from "./say";
+import { handleStartCommand } from "./start";
+
+export const handleCommands = (client: Client) => {
+ handleSayCommand(client);
+ handleStartCommand(client);
+};
diff --git a/packages/gateway/src/commands/say.ts b/packages/gateway/src/commands/say.ts
new file mode 100644
index 0000000..4696574
--- /dev/null
+++ b/packages/gateway/src/commands/say.ts
@@ -0,0 +1,127 @@
+import { Client, Events, Message } from "discord.js";
+import { GUILD_ID } from "../constants";
+
+export const handleSayCommand = (client: Client) => {
+ client.on(Events.MessageCreate, async (message: Message) => {
+ if (message.author.bot) return;
+
+ if (message.content.toLowerCase().startsWith("uma!say")) {
+ const application = await client.application?.fetch();
+ const ownerId = application?.owner?.id;
+
+ if (message.author.id !== ownerId) return;
+
+ const parameters = message.content.split(" ").slice(1);
+
+ if (parameters.length < 2) {
+ await message.reply(
+ "❌ Usage: `uma!say <channel_mention_or_message_id> <message>`\nExamples:\n- `uma!say #general Hello everyone!`\n- `uma!say 1234567890123456789 Thanks for the info!`",
+ );
+
+ return;
+ }
+
+ const firstParameter = parameters[0];
+ const messageContent = parameters.slice(1).join(" ");
+ let targetChannel: any;
+ let targetMessage: any = null;
+ const messageIdMatch = firstParameter.match(/^\d{17,19}$/);
+
+ if (messageIdMatch) {
+ try {
+ const guild = client.guilds.cache.get(GUILD_ID);
+
+ if (!guild) {
+ await message.reply("❌ Guild not found.");
+
+ return;
+ }
+
+ let foundMessage = null;
+
+ for (const channel of guild.channels.cache.values()) {
+ if (channel.isTextBased()) {
+ try {
+ foundMessage = await channel.messages.fetch(firstParameter);
+
+ if (foundMessage) {
+ targetChannel = channel;
+ targetMessage = foundMessage;
+
+ break;
+ }
+ } catch {
+ continue;
+ }
+ }
+ }
+
+ if (!foundMessage) {
+ await message.reply("❌ Message not found.");
+
+ return;
+ }
+ } catch {
+ await message.reply("❌ Error finding message.");
+
+ return;
+ }
+ } else {
+ const channelMatch = firstParameter.match(/<#(\d+)>/);
+
+ if (!channelMatch) {
+ await message.reply(
+ "❌ Please mention a channel or provide a message ID. Example: `#general` or `1234567890123456789`",
+ );
+
+ return;
+ }
+
+ const channelId = channelMatch[1];
+
+ targetChannel = client.channels.cache.get(channelId);
+
+ if (!targetChannel || !targetChannel.isTextBased()) {
+ await message.reply("❌ Channel not found or is not a text channel.");
+
+ return;
+ }
+ }
+
+ try {
+ await message.delete();
+
+ const baseDuration = Math.max(1, messageContent.length / 20);
+ const complexityMultiplier =
+ (messageContent.match(/[.!?]/g) || []).length * 0.5;
+ const wordCount = messageContent.split(" ").length;
+ const wordComplexityMultiplier = Math.min(wordCount / 10, 2);
+ const typingDuration = Math.min(
+ baseDuration + complexityMultiplier + wordComplexityMultiplier,
+ 8,
+ );
+
+ await (targetChannel as any).sendTyping();
+ await new Promise((resolve) =>
+ setTimeout(resolve, typingDuration * 1000),
+ );
+
+ if (targetMessage) {
+ await targetMessage.reply(messageContent);
+ } else {
+ await (targetChannel as any).send(messageContent);
+ }
+ } catch (error) {
+ console.error("Error executing say command:", error);
+
+ try {
+ await message.reply(
+ "❌ Failed to execute the say command. Please check permissions.",
+ );
+ } catch (replyError) {
+ console.error("Failed to send error reply:", replyError);
+ }
+ }
+ }
+ });
+};
diff --git a/packages/gateway/src/commands/start.ts b/packages/gateway/src/commands/start.ts
new file mode 100644
index 0000000..4a771ed
--- /dev/null
+++ b/packages/gateway/src/commands/start.ts
@@ -0,0 +1,83 @@
+import { Client, Events, Message } from "discord.js";
+import { sendProgressUpdate, executeBulkRoleAssignment } from "./utilities";
+
+export const handleStartCommand = (client: Client) => {
+ client.on(Events.MessageCreate, async (message: Message) => {
+ if (message.author.bot) return;
+
+ if (message.content.toLowerCase().startsWith("uma!start")) {
+ const application = await client.application?.fetch();
+ const ownerId = application?.owner?.id;
+
+ if (message.author.id !== ownerId) return;
+
+ const parameters = message.content.split(" ").slice(1);
+
+ if (parameters.length < 3) {
+ await message.reply(
+ "❌ Usage: `uma!start <role_mention> <channel_mention_or_category_id> <update_channel_id> [action]`\nExample: `uma!start @Participant #general 1415599617214513254 execute`",
+ );
+
+ return;
+ }
+
+ const roleMention = parameters[0];
+ const channelOrCategory = parameters[1];
+ const updateChannelId = parameters[2];
+ const action = parameters[3] || "execute";
+ const roleMatch = roleMention.match(/<@&(\d+)>/);
+
+ if (!roleMatch) {
+ await message.reply(
+ "❌ Please mention a role. Example: `@Participant`",
+ );
+
+ return;
+ }
+
+ const roleId = roleMatch[1];
+ let channelId: string | undefined;
+ let categoryId: string | undefined;
+ const channelMatch = channelOrCategory.match(/<#(\d+)>/);
+
+ if (channelMatch) {
+ channelId = channelMatch[1];
+ } else {
+ categoryId = channelOrCategory;
+ }
+
+ if (action !== "preview" && action !== "execute") {
+ await message.reply("❌ Action must be either `preview` or `execute`");
+
+ return;
+ }
+
+ if (action === "preview") {
+ await message.reply(
+ "📋 Preview mode - this would check the specified channel(s) for users who have sent messages.",
+ );
+
+ return;
+ }
+
+ await message.reply(
+ "🚀 Bulk role operation started! Check the progress channel for updates.",
+ );
+
+ executeBulkRoleAssignment(
+ client,
+ roleId,
+ updateChannelId,
+ channelId,
+ categoryId,
+ ).catch((error) => {
+ console.error("Bulk role assignment failed:", error);
+ sendProgressUpdate(
+ client,
+ "❌ Bulk role assignment failed due to an error",
+ updateChannelId,
+ );
+ });
+ }
+ });
+};
diff --git a/packages/gateway/src/commands/utilities.ts b/packages/gateway/src/commands/utilities.ts
new file mode 100644
index 0000000..38d7bc2
--- /dev/null
+++ b/packages/gateway/src/commands/utilities.ts
@@ -0,0 +1,615 @@
+import { Client, ChannelType } from "discord.js";
+import { GUILD_ID } from "../constants";
+
+export const AUDIT_LOG_GUILD_ID = "1419211292396224575";
+export const AUDIT_LOG_CHANNEL_ID = "1419211778793144411";
+
+export const sendProgressUpdate = async (
+ client: Client,
+ message: string,
+ channelId: string,
+): Promise<void> => {
+ try {
+ const channel = client.channels.cache.get(channelId);
+
+ if (channel && "send" in channel) await (channel as any).send(message);
+ } catch (error) {
+ console.error("Failed to send progress update:", error);
+ }
+};
+
+export const sendAuditLog = async (
+ client: Client,
+ embed: any,
+ additionalContent?: string,
+ customChannelId?: string,
+): Promise<void> => {
+ try {
+ const channelId = customChannelId || AUDIT_LOG_CHANNEL_ID;
+ const channel = client.channels.cache.get(channelId);
+
+ if (channel && "send" in channel) {
+ await (channel as any).send({ embeds: [embed] });
+
+ if (additionalContent) {
+ const maxLength = 1900;
+ const codeBlockStart = "```\n";
+ const codeBlockEnd = "\n```";
+ const availableLength =
+ maxLength - codeBlockStart.length - codeBlockEnd.length;
+
+ if (additionalContent.length <= availableLength) {
+ await (channel as any).send(
+ `${codeBlockStart}${additionalContent}${codeBlockEnd}`,
+ );
+ } else {
+ const chunks = [];
+ let remaining = additionalContent;
+
+ while (remaining.length > 0) {
+ if (remaining.length <= availableLength) {
+ chunks.push(remaining);
+
+ break;
+ }
+
+ let breakPoint = availableLength;
+ const lastNewline = remaining.lastIndexOf("\n", availableLength);
+ const lastSpace = remaining.lastIndexOf(" ", availableLength);
+
+ if (lastNewline > availableLength * 0.8) {
+ breakPoint = lastNewline;
+ } else if (lastSpace > availableLength * 0.8) {
+ breakPoint = lastSpace;
+ }
+
+ chunks.push(remaining.substring(0, breakPoint));
+
+ remaining = remaining.substring(breakPoint).trim();
+ }
+
+ for (let i = 0; i < chunks.length; i++) {
+ const chunk = chunks[i];
+ const header =
+ chunks.length > 1 ? `Part ${i + 1}/${chunks.length}:\n` : "";
+
+ await (channel as any).send(
+ `${codeBlockStart}${header}${chunk}${codeBlockEnd}`,
+ );
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Failed to send audit log:", error);
+ }
+};
+
+export const getChannelName = (channel: any): string => {
+ return channel.name || channel.id || "Unknown Channel";
+};
+
+export const fetchMessagesFromChannel = async (
+ client: Client,
+ channel: any,
+ userIds: Set<string>,
+ updateChannelId: string,
+): Promise<{ messageCount: number; batchCount: number }> => {
+ let lastMessageId: string | undefined;
+ let hasMoreMessages = true;
+ let messageCount = 0;
+ let batchCount = 0;
+
+ while (hasMoreMessages) {
+ try {
+ const messages = await channel.messages.fetch({
+ limit: 100,
+ before: lastMessageId,
+ });
+
+ if (messages.size === 0) {
+ hasMoreMessages = false;
+
+ break;
+ }
+
+ for (const message of messages.values())
+ if (message.author && message.author.id) userIds.add(message.author.id);
+
+ messageCount += messages.size;
+ batchCount += 1;
+
+ if (batchCount % 10 === 0)
+ await sendProgressUpdate(
+ client,
+ `📊 Progress for ${getChannelName(channel)}: ${messageCount} messages processed, ${userIds.size} unique users found`,
+ updateChannelId,
+ );
+
+ lastMessageId = messages.last()?.id;
+
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ } catch (error) {
+ console.error(
+ `Error fetching messages from channel ${channel.id}:`,
+ error,
+ );
+
+ hasMoreMessages = false;
+ }
+ }
+
+ return { messageCount, batchCount };
+};
+
+export const fetchForumPosts = async (
+ client: Client,
+ forumChannel: any,
+ userIds: Set<string>,
+ updateChannelId: string,
+): Promise<{ messageCount: number; batchCount: number }> => {
+ let totalMessageCount = 0;
+ let totalBatchCount = 0;
+
+ try {
+ const threads = await forumChannel.threads.fetchActive();
+ const archivedThreads = await forumChannel.threads.fetchArchived();
+
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ await sendProgressUpdate(
+ client,
+ `🔍 Found ${allThreads.length} forum posts in ${getChannelName(forumChannel)}`,
+ updateChannelId,
+ );
+
+ for (const thread of allThreads) {
+ try {
+ const { messageCount, batchCount } = await fetchMessagesFromChannel(
+ client,
+ thread,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += messageCount;
+ totalBatchCount += batchCount;
+
+ await sendProgressUpdate(
+ client,
+ `📝 Processed forum post "${getChannelName(thread)}": ${messageCount} messages`,
+ updateChannelId,
+ );
+ } catch (error) {
+ console.error(`Error processing forum post ${thread.id}:`, error);
+ }
+ }
+ } catch (error) {
+ console.error(`Error fetching forum posts from ${forumChannel.id}:`, error);
+ }
+
+ return { messageCount: totalMessageCount, batchCount: totalBatchCount };
+};
+
+export const fetchChannelThreads = async (
+ client: Client,
+ textChannel: any,
+ userIds: Set<string>,
+ updateChannelId: string,
+): Promise<{ messageCount: number; batchCount: number }> => {
+ let totalMessageCount = 0;
+ let totalBatchCount = 0;
+
+ try {
+ const threads = await textChannel.threads.fetchActive();
+ const archivedThreads = await textChannel.threads.fetchArchived();
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ if (allThreads.length > 0) {
+ await sendProgressUpdate(
+ client,
+ `🔍 Found ${allThreads.length} threads in ${getChannelName(textChannel)}`,
+ updateChannelId,
+ );
+
+ for (const thread of allThreads) {
+ try {
+ const { messageCount, batchCount } = await fetchMessagesFromChannel(
+ client,
+ thread,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += messageCount;
+ totalBatchCount += batchCount;
+
+ await sendProgressUpdate(
+ client,
+ `🧵 Processed thread "${getChannelName(thread)}": ${messageCount} messages`,
+ updateChannelId,
+ );
+ } catch (error) {
+ console.error(`Error processing thread ${thread.id}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error(`Error fetching threads from ${textChannel.id}:`, error);
+ }
+
+ return { messageCount: totalMessageCount, batchCount: totalBatchCount };
+};
+
+export const fetchMessageAuthors = async (
+ client: Client,
+ channelId: string,
+ channelName: string | undefined,
+ updateChannelId: string,
+): Promise<Set<string>> => {
+ const userIds = new Set<string>();
+ let totalMessageCount = 0;
+
+ await sendProgressUpdate(
+ client,
+ `🔍 Starting comprehensive analysis of ${channelName || `channel ${channelId}`} ...`,
+ updateChannelId,
+ );
+
+ const channel = client.channels.cache.get(channelId);
+
+ if (!channel) {
+ await sendProgressUpdate(
+ client,
+ `❌ Channel ${channelId} not found`,
+ updateChannelId,
+ );
+
+ return userIds;
+ }
+
+ const channelDisplayName = channelName || getChannelName(channel);
+
+ switch (channel.type) {
+ case ChannelType.GuildText: {
+ await sendProgressUpdate(
+ client,
+ `📝 Processing text channel: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: channelMessages } = await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += channelMessages;
+
+ const { messageCount: threadMessages } = await fetchChannelThreads(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += threadMessages;
+
+ break;
+ }
+
+ case ChannelType.GuildForum: {
+ await sendProgressUpdate(
+ client,
+ `📋 Processing forum channel: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: forumMessages } = await fetchForumPosts(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += forumMessages;
+
+ break;
+ }
+
+ case ChannelType.PublicThread:
+ case ChannelType.PrivateThread:
+ case ChannelType.AnnouncementThread: {
+ await sendProgressUpdate(
+ client,
+ `🧵 Processing thread: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: threadMessageCount } =
+ await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += threadMessageCount;
+
+ break;
+ }
+
+ case ChannelType.GuildAnnouncement: {
+ await sendProgressUpdate(
+ client,
+ `📢 Processing announcement channel: ${channelDisplayName}`,
+ updateChannelId,
+ );
+
+ const { messageCount: announcementMessages } =
+ await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += announcementMessages;
+
+ const { messageCount: announcementThreadMessages } =
+ await fetchChannelThreads(client, channel, userIds, updateChannelId);
+
+ totalMessageCount += announcementThreadMessages;
+
+ break;
+ }
+
+ case ChannelType.GuildVoice:
+ case ChannelType.GuildStageVoice: {
+ await sendProgressUpdate(
+ client,
+ `🔇 Voice channel ${channelDisplayName} has no text messages`,
+ updateChannelId,
+ );
+
+ break;
+ }
+
+ default:
+ await sendProgressUpdate(
+ client,
+ `❓ Unknown channel type for ${channelDisplayName}, attempting to fetch messages ...`,
+ updateChannelId,
+ );
+
+ if (channel.isTextBased()) {
+ const { messageCount: unknownMessages } =
+ await fetchMessagesFromChannel(
+ client,
+ channel,
+ userIds,
+ updateChannelId,
+ );
+
+ totalMessageCount += unknownMessages;
+ }
+
+ break;
+ }
+
+ await sendProgressUpdate(
+ client,
+ `✅ Completed comprehensive analysis of ${channelDisplayName}: ${totalMessageCount} messages processed, ${userIds.size} unique users found`,
+ updateChannelId,
+ );
+
+ return userIds;
+};
+
+export const getAllChannelsInCategory = async (
+ client: Client,
+ categoryId: string,
+): Promise<{ channels: string[]; channelNames: { [key: string]: string } }> => {
+ const guild = client.guilds.cache.get(GUILD_ID);
+
+ if (!guild) return { channels: [], channelNames: {} };
+
+ const channels: string[] = [];
+ const channelNames: { [key: string]: string } = {};
+
+ const categoryChannels = guild.channels.cache.filter(
+ (channel) => channel.parentId === categoryId,
+ );
+
+ for (const channel of categoryChannels.values()) {
+ channels.push(channel.id);
+
+ channelNames[channel.id] = `#${getChannelName(channel)}`;
+
+ if (
+ channel.type === ChannelType.GuildText ||
+ channel.type === ChannelType.GuildAnnouncement
+ ) {
+ try {
+ const threads = await channel.threads.fetchActive();
+ const archivedThreads = await channel.threads.fetchArchived();
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ for (const thread of allThreads) {
+ channels.push(thread.id);
+
+ channelNames[thread.id] = `🧵 ${getChannelName(thread)}`;
+ }
+ } catch (error) {
+ console.error(
+ `Error fetching threads for channel ${channel.id}:`,
+ error,
+ );
+ }
+ }
+
+ if (channel.type === ChannelType.GuildForum) {
+ try {
+ const threads = await channel.threads.fetchActive();
+ const archivedThreads = await channel.threads.fetchArchived();
+ const allThreads = [
+ ...threads.threads.values(),
+ ...archivedThreads.threads.values(),
+ ];
+
+ for (const thread of allThreads) {
+ channels.push(thread.id);
+
+ channelNames[thread.id] = `📝 ${getChannelName(thread)}`;
+ }
+ } catch (error) {
+ console.error(
+ `Error fetching forum posts for channel ${channel.id}:`,
+ error,
+ );
+ }
+ }
+ }
+
+ return { channels, channelNames };
+};
+
+export const executeBulkRoleAssignment = async (
+ client: Client,
+ roleId: string,
+ customUpdateChannelId: string,
+ channelId?: string,
+ categoryId?: string,
+): Promise<void> => {
+ try {
+ const guild = client.guilds.cache.get(GUILD_ID);
+
+ if (!guild) {
+ await sendProgressUpdate(
+ client,
+ "❌ Guild not found",
+ customUpdateChannelId,
+ );
+
+ return;
+ }
+
+ const role = guild.roles.cache.get(roleId);
+
+ if (!role) {
+ await sendProgressUpdate(
+ client,
+ "❌ Role not found",
+ customUpdateChannelId,
+ );
+
+ return;
+ }
+
+ let channelsToCheck: string[] = [];
+ let channelNames: { [key: string]: string } = {};
+
+ if (channelId) {
+ channelsToCheck = [channelId];
+
+ const channel = guild.channels.cache.get(channelId);
+
+ if (channel) channelNames[channelId] = `#${getChannelName(channel)}`;
+ } else if (categoryId) {
+ const result = await getAllChannelsInCategory(client, categoryId);
+
+ channelsToCheck = result.channels;
+ channelNames = result.channelNames;
+ }
+
+ await sendProgressUpdate(
+ client,
+ `🚀 Starting comprehensive bulk role operation: ${channelsToCheck.length} channel(s)/thread(s) to process`,
+ customUpdateChannelId,
+ );
+
+ const userIds = new Set<string>();
+
+ for (const channelId of channelsToCheck) {
+ const channelUserIds = await fetchMessageAuthors(
+ client,
+ channelId,
+ channelNames[channelId],
+ customUpdateChannelId,
+ );
+
+ channelUserIds.forEach((userId) => userIds.add(userId));
+ }
+
+ const uniqueUserIds = Array.from(userIds);
+
+ await sendProgressUpdate(
+ client,
+ `🎯 Starting role assignment phase: ${uniqueUserIds.length} users to process\n📝 Note: Some users may have left the guild since sending messages`,
+ customUpdateChannelId,
+ );
+
+ let successCount = 0;
+ let errorCount = 0;
+ let notInGuildCount = 0;
+
+ for (let i = 0; i < uniqueUserIds.length; i++) {
+ const userId = uniqueUserIds[i];
+
+ try {
+ const member = await guild.members.fetch(userId);
+ const currentRoles = member.roles.cache.map((role) => role.id);
+
+ if (!currentRoles.includes(roleId)) {
+ await member.roles.add(role);
+
+ successCount += 1;
+ } else {
+ successCount += 1;
+ }
+
+ if ((i + 1) % 25 === 0)
+ await sendProgressUpdate(
+ client,
+ `📈 Role assignment progress: ${i + 1}/${uniqueUserIds.length} users processed (${successCount} successful, ${errorCount} errors, ${notInGuildCount} not in guild)`,
+ customUpdateChannelId,
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ } catch (error: any) {
+ console.error(`Error processing user ${userId}:`, error);
+
+ if (error.code === 10007 || error.message?.includes("Unknown Member")) {
+ notInGuildCount += 1;
+
+ console.log(`User ${userId} is no longer in the guild, skipping ...`);
+ } else {
+ errorCount += 1;
+ }
+ }
+ }
+
+ await sendProgressUpdate(
+ client,
+ `🏁 Bulk role operation completed!\n\n📊 **Final Results:**\n- ✅ **${successCount}** users processed successfully\n- ❌ **${errorCount}** errors occurred\n- 🚪 **${notInGuildCount}** users no longer in guild\n\n💡 **Note:** The "successful" count includes users who already had the role. Users who left the guild are automatically skipped.`,
+ customUpdateChannelId,
+ );
+ } catch (error) {
+ console.error("Error in bulk role assignment:", error);
+ await sendProgressUpdate(
+ client,
+ "❌ An error occurred during bulk role assignment",
+ customUpdateChannelId,
+ );
+ }
+};