diff options
| author | Fuwn <[email protected]> | 2025-09-24 19:04:14 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-24 19:15:34 -0700 |
| commit | 3b08854f33c9944761367597e6850fe6e27e3af3 (patch) | |
| tree | 8b6169d08cc85b0a6a74c761615c2a8e560ac105 /packages/gateway/src/commands | |
| parent | refactor: Move interactions client to packages directory (diff) | |
| download | umabotdiscord-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.ts | 8 | ||||
| -rw-r--r-- | packages/gateway/src/commands/say.ts | 127 | ||||
| -rw-r--r-- | packages/gateway/src/commands/start.ts | 83 | ||||
| -rw-r--r-- | packages/gateway/src/commands/utilities.ts | 615 |
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, + ); + } +}; |