Skip to content

Pop Context

Every command, snap, and event handler receives a pop object (or EventPop for event handlers) as its first argument. It bundles identity, reply helpers, caching, storage, i18n, and more into one place.

pop.user // discord.js User — who triggered this interaction
pop.member // GuildMember | null — the member in the current server
pop.guild // Guild | null — the current server
pop.channel // TextBasedChannel | null — the current channel
pop.client // PopiiClient — the Popii client instance
pop.locale // string — the user's Discord locale ("en-US", "fr", etc.)
// Simple text reply
await pop.reply("Hello!");
// With options
await pop.reply({ content: "Hello!", ephemeral: true });
// With embeds and components
await pop.reply({ embeds: [myEmbed], components: [actionRow] });
// Defer then follow up (for slow operations)
await pop.defer();
const data = await fetchSomethingSlow();
await pop.reply(data); // sends the follow-up

Options from the slash command are available via pop.options. These are the same getters as discord.js’s CommandInteractionOptionResolver:

pop.options.getString("name") // string | null
pop.options.getUser("user") // User | null
pop.options.getInteger("count") // number | null
pop.options.getNumber("amount") // number | null
pop.options.getBoolean("enabled") // boolean | null
pop.options.getChannel("channel") // Channel | null
pop.options.getRole("role") // Role | null
pop.options.getMember("user") // GuildMember | null

Pass true as the second argument to assert a required option (throws if absent, returns non-nullable type):

const target = pop.options.getUser("user", true); // User — not null

If you declared a schema, skip pop.options and use pop.input instead — it’s fully typed and validated:

pop.input.text // string — typed from your Zod schema
pop.input.count // number

Cache any async result with a TTL. The second call within the window returns the cached value without hitting the source again:

const profile = await pop.cache(`profile:${pop.user.id}`, 60_000, async () => {
return db.query("SELECT * FROM users WHERE id = ?").get(pop.user.id);
});

The cache is in-process and clears on restart. Use it for frequently-read, slow-to-fetch values within a single bot session.

The Cooler is a persistent KV store backed by Redis (if configured) or an in-process Map. Unlike the cache helper, values survive bot restarts when Redis is connected.

// Write with a TTL
await pop.cooler.set("last_seen:" + pop.user.id, Date.now(), 86_400_000); // 24h
// Read
const lastSeen = await pop.cooler.get<number>("last_seen:" + pop.user.id);
// Delete
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 });

Locale files live in src/locales/<locale>.json. Popii picks the right file based on pop.locale (the user’s Discord language setting).

src/locales/en-US.json
{
"welcome_message": "Welcome, {{username}}!"
}

Trigger a named task (from src/tasks/) immediately or after a delay:

await pop.schedule("send-report"); // run now
await pop.schedule("send-report", { userId: "123" }, 60_000); // run in 1 minute

Fetch JSON, text, or binary data without setting up headers manually:

const data = await pop.fetchJSON<WeatherData>("https://api.weather.com/current");
const readme = await pop.fetchText("https://raw.githubusercontent.com/org/repo/README.md");
const image = await pop.fetchBuffer("https://example.com/thumbnail.png");
await pop.reply({
content: "Here's your report:",
files: [pop.createAttachment("./exports/report.csv", "report.csv")],
});

Discord component customId fields are capped at 100 characters. pop.pack() encodes a JavaScript object into a compact string you can embed in a customId, and pop.snapData decodes it on the other side:

// In a command — embed context in the button
const customId = pop.pack("confirm-ban", { targetId: member.id, reason: "spam" });
// In the snap — read it back
export default snap({
prefix: "confirm-ban",
async do(pop) {
const { targetId, reason } = pop.snapData as { targetId: string; reason: string };
// proceed with the ban
},
});

pop.locals is the typed scratch pad populated by your middleware. Augment PopiiLocals to get types everywhere in your project:

src/types.d.ts
declare module "popii-framework" {
interface PopiiLocals {
dbUser: { id: string; balance: number; premium: boolean };
guildConfig: Record<string, string>;
}
}

After this, pop.locals.dbUser is typed in every command, snap, and middleware.

Declare a typed state object in popiiClient() for mutable values that belong to the bot’s lifetime — not a request:

const client = popiiClient({
token: process.env.DISCORD_TOKEN!,
state: {
totalCommandsRun: 0,
startedAt: Date.now(),
},
});
// In any command:
pop.state.totalCommandsRun++;

Emit and listen to custom events anywhere in your bot. Useful for cross-plugin communication:

// Emit from a command or event handler
await pop.client.global.emit("level-up", { userId: pop.user.id, newLevel: 10 });
// Listen from a plugin's setup hook
client.global.on("level-up", ({ userId, newLevel }) => {
announceInChannel(`<@${userId}> reached level ${newLevel}!`);
});