Every command, snap, and event handler receives a pop (or EventPop) object as its first argument. This is your main interface to everything Popii provides.
pop.user // discord.js User — who triggered the command
pop.member // GuildMember | null — the member in this server
pop.guild // Guild | null — the server, if applicable
pop.channel // TextBasedChannel | null — the channel
pop.client // PopiiClient — the bot client
pop.locale // string — the user's Discord locale (e.g. "en-US")
await pop.reply("Hello!");
await pop.reply({ content: "Hello!", ephemeral: true });
await pop.reply({ embeds: [myEmbed], components: [actionRow] });
await pop.defer({ ephemeral: true }); // acknowledge within 3s, reply later
await pop.reply("Done!"); // follow-up after defer
const name = pop.options.getString("name"); // string | null
const target = pop.options.getUser("user"); // User | null
const count = pop.options.getInteger("count"); // number | null
const channel = pop.options.getChannel("chan"); // TextBasedChannel | null
const role = pop.options.getRole("role"); // Role | null
const flag = pop.options.getBoolean("enabled"); // boolean | null
With a schema, use pop.input for fully-typed values instead.
Cache the result of an async call for a given TTL. Subsequent calls within the TTL return the cached value without re-fetching:
const user = await pop.cache(`user:${pop.user.id}`, 60_000, async () => {
return db.query("SELECT * FROM users WHERE id = ?").get(pop.user.id);
});
The Cooler is a shared key-value store backed by Redis (if configured) or an in-process Map. Use it for state that needs to persist across commands and restarts:
await pop.cooler.set("last_seen:" + pop.user.id, Date.now(), 86_400_000); // 24h TTL
const lastSeen = await pop.cooler.get<number>("last_seen:" + pop.user.id);
await pop.cooler.delete("last_seen:" + pop.user.id);
// pop.t() is shorthand for pop.translate()
const msg = pop.t("welcome_message", { username: pop.user.username });
// looks up the key in the locale file for pop.locale
Locale files live in src/locales/<locale>.json (e.g. en-US.json, fr.json).
Trigger a named background task immediately or with a delay:
await pop.schedule("send-report"); // run now
await pop.schedule("send-report", { userId: "123" }, 60_000); // run after 1 minute
const data = await pop.fetchJSON<MyType>("https://api.example.com/data");
const text = await pop.fetchText("https://example.com/readme");
const buf = await pop.fetchBuffer("https://example.com/image.png");
await pop.reply({
content: "Here's your image:",
files: [pop.createAttachment("./assets/banner.png", "banner.png")]
});
Encode structured data into a Discord customId string (max 100 chars):
// In a command — create a button with embedded data
const customId = pop.pack("confirm-ban", { targetId: member.id, reason: "spam" });
// → "confirm-ban:eyJ0YXJnZXRJZCI6Ii4..."
// In the matching snap
export default snap({
prefix: "confirm-ban",
do(pop) {
const { targetId, reason } = pop.snapData;
// ...
}
});
pop.locals is the per-request scratch pad populated by your middlewares. Augment the global PopiiLocals interface to get type safety across your entire project:
// src/types.d.ts
declare module "popii" {
interface PopiiLocals {
dbUser: { id: string; balance: number; premium: boolean };
guildConfig: Record<string, string>;
}
}
After that, pop.locals.dbUser is typed in every command and middleware.
Provide a typed state object in popiiClient() for global mutable bot state:
const client = popiiClient({
token: process.env.DISCORD_TOKEN!,
state: { totalCommandsRun: 0 }
});
// In any command:
pop.state.totalCommandsRun++;
Emit and listen to custom events across your bot (useful for plugin communication):
// Emit from anywhere:
await pop.client.global.emit("xp-gained", { userId: pop.user.id, amount: 50 });
// Listen from anywhere (e.g. in a plugin's setup):
client.global.on("xp-gained", ({ userId, amount }) => {
console.log(`${userId} gained ${amount} XP`);
});