Snaps handle Buttons, Select Menus, and Modals by their customId. Unlike one-shot collectors, Snaps survive bot restarts because the handler is registered by pattern, not by a per-message listener.
// src/snaps/confirm.ts
import { snap } from "popii";
export default snap({
customId: "confirm",
async do(pop) {
await pop.reply({ content: "Confirmed!", ephemeral: true });
}
});
Any button or component with customId: "confirm" will trigger this handler.
Use a RegExp for dynamic IDs that carry data:
// src/snaps/delete-message.ts
import { snap } from "popii";
export default snap({
customId: /^delete:(\d+)$/,
async do(pop) {
const messageId = pop.snapMatches?.[1];
// delete the message with that ID
await pop.reply({ content: "Deleted!", ephemeral: true });
}
});
Use prefix instead of a regex for a simpler pattern:
import { snap } from "popii";
export default snap({
prefix: "vote:", // matches "vote:yes", "vote:no", etc.
async do(pop) {
const choice = pop.interaction.customId.split(":")[1];
await pop.reply(`You voted: ${choice}`);
}
});
Use pop.pack() in your command to embed data, and read it back in your snap:
// In a command:
const button = {
type: 2, // Button
style: 1, // Primary
label: "Confirm",
custom_id: pop.pack("confirm-action", { targetId: "123", action: "ban" })
};
// In the snap:
export default snap({
prefix: "confirm-action",
async do(pop) {
// pop.snapData is the decoded object: { targetId: "123", action: "ban" }
}
});
// src/snaps/role-select.ts
import { snap } from "popii";
export default snap({
customId: "role-picker",
async do(pop) {
const interaction = pop.interaction as any;
const selected: string[] = interaction.values;
await pop.reply(`You selected: ${selected.join(", ")}`);
}
});
// src/snaps/feedback-modal.ts
import { snap } from "popii";
export default snap({
customId: "feedback-modal",
async do(pop) {
const interaction = pop.interaction as any;
const feedback = interaction.fields.getTextInputValue("feedback");
await pop.reply({ content: `Thanks for your feedback: "${feedback}"`, ephemeral: true });
}
});
Set expiresIn (in ms) to auto-remove the handler after a period:
export default snap({
customId: "one-time-action",
expiresIn: 300_000, // 5 minutes
async do(pop) {
await pop.reply("Done!");
}
});
Snaps support the same guards as commands:
export default snap({
customId: "admin-action",
ownerOnly: true,
permissions: ["Administrator"],
cooldown: 5000,
async do(pop) {
// only bot owners with Administrator can trigger this
}
});