const { Client, GatewayIntentBits, ChannelType, SlashCommandBuilder, MessageFlags, PermissionsBitField, Events, } = require('discord.js'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, ], }); const GUILD_ID = '1450887049426108563'; const STAFF_ROLE_IDS = [ '1450887049803464746', '1450887049786953981', '1450887049786953975', '1450887049786953980', '1454585821209559164', '1454585713428529344', '1454585598009675816', '1453130780619575316', ]; const BASE_THREAD_NAME = 'staff-notes'; client.once(Events.ClientReady, async () => { console.log(`Logged in as ${client.user.tag}`); const command = new SlashCommandBuilder() .setName('staffnote') .setDescription('Add a private staff-only note to this ticket') .addStringOption((option) => option .setName('note') .setDescription('The note content') .setRequired(true) ) .toJSON(); await client.application.commands.create(command, GUILD_ID); console.log('Slash command registered!'); }); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; if (interaction.guildId !== GUILD_ID) return; if (interaction.commandName !== 'staffnote') return; const hasStaffRole = STAFF_ROLE_IDS.some((roleId) => interaction.member.roles.cache.has(roleId) ); if (!hasStaffRole) { return interaction.reply({ content: 'Only staff can use this command!', flags: MessageFlags.Ephemeral, }); } const noteText = interaction.options.getString('note'); await interaction.deferReply({ flags: MessageFlags.Ephemeral }); let thread = null; let wasNewlyCreated = false; let addedCount = 0; let totalStaff = 0; let skippedNoView = 0; try { const activeThreads = await interaction.channel.threads.fetchActive().catch(() => null); const threadCollection = activeThreads?.threads ?? interaction.channel.threads.cache; thread = threadCollection.find( (t) => t.type === ChannelType.GuildPrivateThread && t.name.startsWith(BASE_THREAD_NAME) ); if (!thread) { thread = await interaction.channel.threads.create({ name: BASE_THREAD_NAME, type: ChannelType.GuildPrivateThread, autoArchiveDuration: 1440, reason: 'Staff-only notes for this ticket', }); wasNewlyCreated = true; console.log(`Successfully created new staff thread in ticket ${interaction.channel.id}`); await interaction.guild.members.fetch(); const staffMemberIds = new Set(); for (const roleId of STAFF_ROLE_IDS) { const role = interaction.guild.roles.cache.get(roleId); if (!role) continue; role.members.forEach((member) => staffMemberIds.add(member.id)); } totalStaff = staffMemberIds.size; addedCount = 0; skippedNoView = 0; for (const memberId of staffMemberIds) { try { const member = await interaction.guild.members.fetch(memberId).catch(() => null); if (!member) continue; const canViewParent = interaction.channel .permissionsFor(member) ?.has(PermissionsBitField.Flags.ViewChannel); if (!canViewParent) { skippedNoView++; console.warn(`Skipping ${member.user.tag} (${memberId}) - cannot view parent channel`); continue; } await thread.members.add(memberId); addedCount++; await new Promise((resolve) => setTimeout(resolve, 250)); } catch (err) { console.warn(`Could not add member ${memberId}: ${err?.message ?? err}`); } } console.log(`Added ${addedCount} out of ${totalStaff} staff members. Skipped (no ViewChannel): ${skippedNoView}`); } else { console.log(`Reusing existing staff thread "${thread.name}".`); } await thread.send( `**Note by ${interaction.user.tag}** (${new Date().toLocaleString()}):\n${noteText}` ); let successMsg = '✅ Staff note added to the private thread!'; if (wasNewlyCreated) { successMsg += `\nAdded ${addedCount} out of ${totalStaff} staff members.`; if (skippedNoView > 0) { successMsg += `\nSkipped ${skippedNoView} staff member(s) who cannot view this ticket channel.`; } } else { successMsg += `\n(Using existing thread – new staff not auto-added to old tickets)`; } await interaction.editReply({ content: successMsg }); } catch (error) { console.error('Failed during staffnote command:', error); await interaction.editReply({ content: '❌ Something went wrong. Check bot console.', }); } }); client.login('MTQ1NzQwMDcwMTUwNzM0MjQ4Nw.Gh4arC.uJb4qnBFNoTOqrK-kJKvz65PTqUU_urQ4lDyig');