@m-programmation/discord-giveaways
v0.2.0
Published
A powerful, extensible Discord giveaways module with fully customizable embeds and complete history tracking.
Maintainers
Readme
@m-programmation/discord-giveaways
A discord.js module for running advanced giveaways: fully customizable embeds, reaction- or button-based entries, automatic ending, and a pluggable storage architecture so you can keep a complete history of every giveaway.
🚧 The module is currently at its base version (
0.x): the core (create/end/reroll a giveaway, customizable embeds, storage providers) is functional, but the API may still evolve before 1.0. Feedback and issues are welcome!
Features
- ✅ Create, end and reroll giveaways
- ✅
start/end/rerollembeds are fully customizable - ✅ Automatic ending when a giveaway expires
- ✅ Reaction-based or native button-based entries (configurable), with join/leave tracking
- ✅ Optional
canJoingate + custom denial handler, e.g. to restrict entries to certain roles - ✅ 100% TypeScript, typed events
- ✅ Built-in SQL storage (
SqlProvider): SQLite, MySQL, MariaDB, Postgres — connected, verified and cached automatically on startup - ✅ Pluggable storage (
GiveawaysProvider) so you can plug in any other database and keep a complete history - 🔜 Advanced history/queries, ready-to-use commands
Installation
npm install @m-programmation/discord-giveaways discord.jsdiscord.js (^14.14.0) is a peer dependency: install it yourself and provide your own Client.
Quick start
import { Client, GatewayIntentBits } from "discord.js";
import { GiveawaysManager } from "@m-programmation/discord-giveaways";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessageReactions],
});
const giveaways = new GiveawaysManager(client);
client.once("ready", async () => {
// Loads existing giveaways and starts the automatic end-checking loop
await giveaways.init();
});
// Start a giveaway
await giveaways.start({
channelId: "123456789012345678",
prize: "Discord Nitro",
winnerCount: 1,
duration: 60 * 60 * 1000, // 1 hour
hostedBy: "987654321098765432",
});
// Listen to events
giveaways.on("giveawayEnd", (giveaway, winners) => {
console.log(`Giveaway "${giveaway.prize}" ended, winners:`, winners);
});API
GiveawaysManager
| Method | Description |
| ----------------------------- | ------------------------------------------------------------- |
| init() | Loads giveaways from the provider, starts the auto-end loop |
| start(options) | Creates and posts a new giveaway |
| end(messageId) | Ends a giveaway and draws the winners |
| reroll(messageId, options?) | Draws one or more new winners |
| delete(messageId) | Removes a giveaway from the provider and the cache |
| get(messageId) | Retrieves a giveaway by its message ID |
| getAll() | All known giveaways (active and ended) |
| stop() | Stops the auto-end loop and releases the provider's resources |
Events
giveawayStart, giveawayEnd, giveawayReroll, giveawayDelete — all typed, with the Giveaway (and the winners, where relevant) as arguments.
Customizing embeds
Each embed (start, end, reroll) can be replaced individually:
import { EmbedBuilder } from "discord.js";
import { GiveawaysManager } from "@m-programmation/discord-giveaways";
const giveaways = new GiveawaysManager(client, {
embeds: {
start: (giveaway) => new EmbedBuilder().setTitle(`🎁 ${giveaway.prize}`).setColor("Random"),
},
});Button-based entries
By default the manager reacts to the giveaway message with reaction (👍 the original behaviour, unchanged). Set entryMode: "button" to post a native join/leave button instead — no reaction is added, and participants are tracked in Giveaway.participants (persisted through the provider on every click) rather than read from message reactions:
import { GiveawaysManager } from "@m-programmation/discord-giveaways";
const giveaways = new GiveawaysManager(client, {
entryMode: "button",
// Optional: gate entries (e.g. required roles) — return false to deny.
canJoin: (giveaway, userId) => {
const requiredRoles = giveaway.extra.requiredRoles as string[] | undefined;
if (!requiredRoles?.length) return true;
const member = client.guilds.cache.get(giveaway.guildId)?.members.cache.get(userId);
return requiredRoles.some((roleId) => member?.roles.cache.has(roleId));
},
// Optional: fully custom denial reply instead of the default text.
onJoinDenied: async (interaction, giveaway) => {
await interaction.reply({ content: `You're missing a required role for "${giveaway.prize}".`, ephemeral: true });
},
// Optional: override the built-in ephemeral texts / button label ({count} = participant count).
entryMessages: {
joined: "🎉 You're in!",
left: "You left the giveaway.",
buttonLabel: "Enter ({count})",
},
});Clicking the button toggles membership (click again to leave), the button label updates live with the participant count, and it's automatically disabled once the giveaway ends. canJoin and onJoinDenied are only used in "button" mode — reaction-based giveaways have no entry gate (Discord doesn't let a bot restrict who can react).
Persistence & history
By default, GiveawaysManager uses a MemoryProvider (in-memory, not persistent — handy for testing, but everything is lost on restart).
Built-in SQL provider (SQLite, MySQL, MariaDB, Postgres)
SqlProvider covers all four out of the box. Pick a dialect and pass it to the manager — on giveaways.init() it opens the connection, runs a clean connectivity check (a SELECT 1, wrapped in a clear GiveawayError if it fails), creates the giveaways table if it doesn't exist yet, and warms up an in-memory cache so reads never hit the database again:
import { GiveawaysManager, SqlProvider } from "@m-programmation/discord-giveaways";
// SQLite (local file)
const provider = new SqlProvider({ dialect: "sqlite", filename: "./data/giveaways.sqlite" });
// MySQL / MariaDB
// const provider = new SqlProvider({
// dialect: "mysql", // or "mariadb"
// host: "localhost",
// port: 3306,
// user: "bot",
// password: "secret",
// database: "giveaways",
// });
// Postgres
// const provider = new SqlProvider({
// dialect: "postgres",
// host: "localhost",
// port: 5432,
// user: "bot",
// password: "secret",
// database: "giveaways",
// });
const giveaways = new GiveawaysManager(client, { provider });
client.once("ready", async () => {
await giveaways.init(); // connects, checks the connection, migrates, warms the cache
});SqlProvider is built on Knex and only requires the driver matching your dialect as a peer dependency: better-sqlite3 for SQLite, mysql2 for MySQL/MariaDB, pg for Postgres.
npm install better-sqlite3 # sqlite
npm install mysql2 # mysql / mariadb
npm install pg # postgresCall await giveaways.stop() on shutdown to close the underlying connection cleanly.
Custom providers
To plug in anything else (a different ORM, Redis, a REST backend, ...), implement the GiveawaysProvider interface and pass it to the manager:
import type { GiveawaysProvider } from "@m-programmation/discord-giveaways";
const provider: GiveawaysProvider = {/* init?, getAll, create, update, delete, dispose? */};
const giveaways = new GiveawaysManager(client, { provider });giveaways.getAll() returns every giveaway known to the manager (active and ended) — the foundation to build a full history view on top of.
Contributing / reporting a bug
Source code, issues and discussions live on GitHub.
