import { ApplicationCommandOptionType, EmbedBuilder, PermissionFlagsBits, } from 'discord.js'; import { getOrFetchGuild } from '#utils/globalUtils/getOrFetchGuild.js'; import { COLOR_EMBEDS } from '#config'; import { sendPlainMessage, sendEmbedMessage } from '#utils/globalUtils/sendMessageUtils.js'; import { createLogger } from '#utils/globalUtils/logger.js'; const logger = createLogger('GetInvite'); // Constants const MAX_INVITE_AGE = 604800; // 7 days in seconds const MAX_INVITE_USES = 100; /** * Parse human-readable duration string to seconds. * @param {string} durationStr * @returns {number} */ function parseDuration(durationStr) { if (!durationStr || durationStr.toLowerCase() === 'permanent') return 0; const regex = /(\d+)([dhms])/gi; const unitSeconds = { d: 86400, h: 3600, m: 60, s: 1 }; let totalSeconds = 0; let matched = false; for (const match of durationStr.matchAll(regex)) { matched = true; const [_, num, unit] = match; totalSeconds += parseInt(num) * unitSeconds[unit.toLowerCase()]; } return matched ? totalSeconds : NaN; } /** * Format a date to Discord relative timestamp. * @param {Date} date * @returns {string} */ function formatDiscordTimestamp(date) { return ``; } /** * Builds an embed for displaying invite details. * @param {Object} options * @param {string} options.title * @param {string} options.url * @param {Object} options.channel * @param {string|null} [options.expiresAt=null] * @param {string|number|null} [options.uses=null] */ function buildInviteEmbed({ title, url, channel, expiresAt = null, uses = null }) { const embed = new EmbedBuilder() .setColor(COLOR_EMBEDS.INFO) .setDescription(`### 🚪 ${title}`) .addFields( { name: '> Invite URL', value: `- ${url}` }, { name: '> Channel', value: `- <#${channel?.id || 'unknown'}>` } ); if (expiresAt !== null) embed.addFields({ name: '> Expires', value: `- ${expiresAt}`, inline: true }); if (uses !== null) embed.addFields({ name: '> Max Uses', value: `- ${uses}`, inline: true }); return embed; } export default { name: 'get-invite', description: 'Get an invite for the specified guild', options: [ { name: 'guild-id', description: 'The ID of the guild to get the invite for', required: true, type: ApplicationCommandOptionType.String, }, { name: 'duration', description: 'Invite duration (e.g., 30m, 1h, 1d12h, permanent)', required: false, type: ApplicationCommandOptionType.String, }, { name: 'max-uses', description: 'Max number of uses for the invite', required: false, type: ApplicationCommandOptionType.Integer, }, ], devOnly: true, ownedGuildsOnly: true, scope: 'Guild', /** * @param {import('discord.js').Client} client * @param {import('discord.js').ChatInputCommandInteraction} interaction */ execute: async (client, interaction) => { await interaction.deferReply(); const guildId = interaction.options.getString('guild-id'); const durationInput = interaction.options.getString('duration'); const maxUsesInput = interaction.options.getInteger('max-uses'); let guild; try { guild = await getOrFetchGuild(client, guildId); logger.success(`Fetched guild "${guild.name}" (${guild.id})`); } catch (err) { logger.warn(`Failed to fetch guild ${guildId}:`, err?.message || err); return sendPlainMessage(interaction, `❌ I'm not in the guild with ID \`${guildId}\`.`); } const botMember = guild.members?.me; const wantsNewInvite = durationInput || maxUsesInput !== null; // Duration + Max Uses Validation let maxAge = 300; if (wantsNewInvite) { maxAge = parseDuration(durationInput || '5m'); if (isNaN(maxAge) || maxAge < 0 || maxAge > MAX_INVITE_AGE) { return sendPlainMessage(interaction, '⚠️ Invalid duration. Use formats like `30m`, `2h`, `1d12h`, or `permanent` (max 7 days).'); } if (maxUsesInput !== null && (maxUsesInput < 0 || maxUsesInput > MAX_INVITE_USES)) { return sendPlainMessage(interaction, '🔢 Max uses must be between 0 (unlimited) and 100.'); } } // Permission Check const canCreate = botMember?.permissions.has(PermissionFlagsBits.CreateInstantInvite); if (!canCreate) { logger.warn(`Missing CreateInstantInvite in "${guild.name}"`); return sendPlainMessage(interaction, `🚫 I don't have permission to create invites in "${guild.name}".`); } // Find Valid Channel const validChannel = guild.channels.cache.find(ch => ch.viewable && ch.permissionsFor(botMember)?.has(PermissionFlagsBits.CreateInstantInvite) ); if (!validChannel) { return sendPlainMessage(interaction, '🚫 No accessible channel found for creating invites.'); } // Use Existing Invite or Vanity URL if (!wantsNewInvite) { if (guild.vanityURLCode) { logger.info(`Using vanity URL for "${guild.name}": ${guild.vanityURLCode}`); const embed = buildInviteEmbed({ title: `Vanity Invite for "${guild.name}"`, url: `https://discord.gg/${guild.vanityURLCode}`, channel: { id: guild.systemChannelId ?? validChannel.id }, }); return sendEmbedMessage(interaction, embed); } try { const invites = await guild.invites.fetch(); if (invites.size) { const fallback = invites.find(inv => inv.maxAge === 0 && inv.maxUses === 0) || invites.first(); if (fallback) { logger.info(`Using existing invite: ${fallback.url}`); const embed = buildInviteEmbed({ title: `Existing Invite for "${guild.name}"`, url: fallback.url, channel: fallback.channel, expiresAt: fallback.expiresAt ? formatDiscordTimestamp(fallback.expiresAt) : 'Never expires', uses: fallback.maxUses === 0 ? 'Unlimited' : fallback.maxUses, }); const contentMessage = ` ||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​||||​|| _ _ _ _ _ _ ${fallback.url}`; return sendEmbedMessage(interaction, embed, [], false, contentMessage); } } } catch (err) { logger.warn(`Could not fetch invites for "${guild.name}":`, err?.message || err); } } // Create New Invite try { const invite = await validChannel.createInvite({ maxAge: wantsNewInvite ? maxAge : 300, maxUses: wantsNewInvite && maxUsesInput !== null ? maxUsesInput : 0, temporary: false, unique: true, }); logger.success(`Created invite in #${validChannel.name} (${guild.name}): ${invite.url}`); const embed = buildInviteEmbed({ title: `New Invite for "${guild.name}"`, url: invite.url, channel: invite.channel, expiresAt: invite.expiresAt ? formatDiscordTimestamp(invite.expiresAt) : 'Never expires', uses: invite.maxUses === 0 ? 'Unlimited' : invite.maxUses, }); const contentMessage = `🔗 [Click here to join](${invite.url})`; return sendEmbedMessage(interaction, embed, [], false, contentMessage); } catch (err) { logger.error(`Failed to create invite in #${validChannel.name} (${guild.name})`, err?.message || err); return sendPlainMessage(interaction, '❌ Could not create an invite. Make sure I have permission and the server allows new invites.'); } }, };