const { SlashCommandBuilder, MessageFlags } = require("discord.js"); /* * CONFIG */ const POKE_MAX_TIMES = 5; const POKE_DELAY_MS = 750; const POKE_COOLDOWN_MS = 30_000; /* * In-memory cooldowns. * * Key: * Discord user ID of the person sending the poke. * * Value: * Timestamp when they may poke again. */ const pokeCooldowns = new Map(); module.exports = { data: new SlashCommandBuilder() .setName("fun") .setDescription("Fun commands.") .addSubcommand(subcommand => subcommand .setName("poke") .setDescription( "Poke another server member in DMs." ) .addUserOption(option => option .setName("member") .setDescription( "The member you want to poke." ) .setRequired(true) ) .addIntegerOption(option => option .setName("times") .setDescription( `How many times to poke them. Max ${POKE_MAX_TIMES}.` ) .setMinValue(1) .setMaxValue( POKE_MAX_TIMES ) .setRequired(false) ) ), async execute(interaction) { switch ( interaction.options.getSubcommand() ) { case "poke": return poke( interaction ); } } }; /* * POKE */ async function poke(interaction) { /* * Guild only. */ if (!interaction.guild) { return interaction.reply({ content: "This command can only be used inside a server.", flags: MessageFlags.Ephemeral }); } /* * Options */ const user = interaction.options.getUser( "member", true ); const times = interaction.options.getInteger( "times" ) ?? 1; /* * Don't poke yourself. */ if ( user.id === interaction.user.id ) { return interaction.reply({ content: "You can't poke yourself! 👉", flags: MessageFlags.Ephemeral }); } /* * Don't poke bots. */ if (user.bot) { return interaction.reply({ content: "You can't poke a bot!", flags: MessageFlags.Ephemeral }); } /* * Make sure the target is actually * a member of this guild. */ let member; try { member = await interaction.guild .members .fetch( user.id ); } catch { return interaction.reply({ content: "I couldn't find that member in this server.", flags: MessageFlags.Ephemeral }); } /* * COOLDOWN */ const now = Date.now(); const cooldownUntil = pokeCooldowns.get( interaction.user.id ); if ( cooldownUntil && cooldownUntil > now ) { const remaining = Math.ceil( ( cooldownUntil - now ) / 1000 ); return interaction.reply({ content: `You need to wait **${remaining}s** before poking someone again.`, flags: MessageFlags.Ephemeral }); } /* * Defer before sending DMs. */ await interaction.deferReply({ flags: MessageFlags.Ephemeral }); /* * Set cooldown before sending. * * This prevents repeated invocations * while the DM loop is running. */ pokeCooldowns.set( interaction.user.id, now + POKE_COOLDOWN_MS ); /* * Sender name. * * Prefer their server display name. */ let senderName = interaction.user.displayName; try { const senderMember = await interaction.guild .members .fetch( interaction.user.id ); senderName = senderMember.displayName; } catch { /* * interaction.user.displayName * remains the fallback. */ } /* * SEND POKES */ let sent = 0; try { for ( let i = 0; i < times; i++ ) { await member.send({ content: `👉 **${senderName} poked you!**\n` + `From **${interaction.guild.name}**` }); sent++; /* * Don't sleep after the final DM. */ if ( i < times - 1 ) { await sleep( POKE_DELAY_MS ); } } } catch (error) { console.error( `Failed to poke ${user.tag}:`, error ); /* * If no messages made it through, * this is most likely closed DMs. */ if (sent === 0) { return interaction.editReply({ content: `I couldn't poke ${user}. They may have DMs disabled.` }); } /* * Some messages succeeded before * Discord rejected a later one. */ return interaction.editReply({ content: `I poked ${user} **${sent}/${times}** times before I couldn't send any more.` }); } /* * SUCCESS */ return interaction.editReply({ content: times === 1 ? `👉 Poked ${user}!` : `👉 Poked ${user} **${times} times!**` }); } /* * SLEEP */ function sleep(ms) { return new Promise( resolve => setTimeout( resolve, ms ) ); }