district.js
v0.12.0
Published
A powerful Node.js module for building District bots — post, react, run slash commands, and receive events.
Readme
district.js
A powerful Node.js module for building District bots — post, edit, react, run slash commands, and receive events with a clean, object-oriented API.
npm install district.jsRequires Node.js 18+. Create an app and a bot in the Developer Portal to get your token (
dbot_…) and signing secret (dsk_…).
Your first bot
import { Client } from "district.js";
const client = new Client({
token: process.env.DISTRICT_TOKEN, // dbot_…
signingSecret: process.env.DISTRICT_SECRET, // dsk_… (needed to receive events)
});
client.on("ready", (me) => console.log(`Logged in as ${me.displayName}`));
client.on("messageCreate", (message) => {
if (message.authoredByMe) return; // don't reply to yourself
if (message.content === "!ping") message.reply("pong 🏓");
});
// Starts an HTTP server that receives District's signed webhook events.
client.listen(3000);Point your app's Event Subscription (in the portal) at https://your-host:3000/ and tick the events you want. District signs every event; district.js verifies the signature for you before emitting it.
Sending & acting
// Send to a channel
const msg = await client.channels.send(channelId, "hello world");
// Act on a message you have
await msg.edit("hello, world!");
await msg.react("👍");
await msg.delete();
// Read history (returns a Collection)
const recent = await client.channel(channelId).fetchMessages(50);
const commands = recent.filter((m) => m.content?.startsWith("!"));Users, spaces & permissions
const user = await message.fetchAuthor(); // User
const space = await client.spaces.fetch(spaceId); // Space (app must be installed)
const members = await space.members(); // Collection<SpaceMember>
const roles = await space.roles(); // Collection<Role>
const named = members.filter((m) => m.nickname !== null);
const canBan = roles.some((r) => r.permissions.has("BanMembers"));
// Permissions
import { PermissionsBitField } from "district.js";
const perms = new PermissionsBitField(["SendMessages", "AddReactions"]);
perms.has(PermissionsBitField.Flags.SendMessages); // true
perms.toString(); // "32770" — use as the install-link permissions integer
// Markdown
import { Formatters } from "district.js";
channel.send(Formatters.bold("Heads up!") + " " + Formatters.code("v0.2.0"));Embeds
import { EmbedBuilder } from "district.js";
const embed = new EmbedBuilder()
.setTitle("Deploy complete")
.setUrl("https://example.com/builds/128")
.setColor("#c6a86b")
.setDescription("Build **#128** shipped to production.")
.addFields(
{ name: "Env", value: "prod", inline: true },
{ name: "By", value: "ci-bot", inline: true },
)
.setFooter({ text: "district.js" })
.setTimestamp();
await channel.send({ content: "New release 🎉", embeds: [embed] });The District app renders it as an embed card (accent bar, title, fields, image).
Buttons
import { ActionRowBuilder, ButtonBuilder } from "district.js";
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setLabel("Approve").setStyle("success").setCustomId("approve"),
new ButtonBuilder().setLabel("Deny").setStyle("danger").setCustomId("deny"),
new ButtonBuilder().setLabel("Docs").setUrl("https://usedistrict.org/developers/docs"),
);
await channel.send({ content: "Review this?", components: [row] });
// A click delivers an interactionCreate (needs an Event Subscription endpoint)
client.on("interactionCreate", (i) => {
if (i.isButton() && i.customId === "approve") i.reply("Approved ✅");
});Select menus & modals
import { ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ModalBuilder, TextInputBuilder } from "district.js";
// Dropdown
const menu = new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder().setCustomId("role").setPlaceholder("Pick a role")
.addOptions({ label: "Dev", value: "dev" }, { label: "Design", value: "design" }),
);
await channel.send({ content: "Choose:", components: [menu] });
// Modal (opens instantly on click; only the submit is delivered to the bot)
const modal = new ModalBuilder().setCustomId("feedback").setTitle("Feedback")
.addComponents(new TextInputBuilder().setCustomId("msg").setLabel("Your message").setStyle("paragraph"));
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setLabel("Give feedback").setStyle("primary").setModal(modal),
);
await channel.send({ components: [row] });
client.on("interactionCreate", (i) => {
if (i.isSelectMenu() && i.customId === "role") i.reply(`You picked ${i.values[0]}`);
if (i.isModalSubmit() && i.customId === "feedback") i.reply(`Thanks: ${i.field("msg")}`);
});Because District delivers interactions asynchronously, modals are attached to a button and shown instantly, client-side; only the submitted values are sent back to your bot.
Slash commands
Register the command in the portal (name + handler URL), then:
client.on("interactionCreate", (interaction) => {
if (interaction.commandName === "weather") {
interaction.reply(`It's sunny in ${interaction.args.text}`);
}
});File-based handlers (commands / events / components)
Prefer folders over one giant file? Drop handlers into commands/, events/, and
components/ and load them — the Client auto-routes slash commands and
button/select/modal interactions for you.
my-bot/
├─ index.js
├─ commands/
│ └─ ping.js
├─ events/
│ └─ ready.js
└─ components/
└─ vote.js// commands/ping.js
import { SlashCommandBuilder } from "district.js";
export default {
data: new SlashCommandBuilder().setName("ping").setDescription("Pong!"),
async execute(interaction, client) {
await interaction.reply("🏓 Pong!");
},
};// events/ready.js
export default {
name: "ready",
once: true,
execute(me, client) { console.log(`Logged in as ${me.displayName}`); },
};// components/vote.js — routes any customId starting with "vote:"
export default {
id: "vote:*", // exact string, "prefix*", or (id) => boolean
async execute(interaction, client) {
await interaction.reply({ content: `You picked ${interaction.customId.slice(5)}`, ephemeral: true });
},
};// index.js
import { Client } from "district.js";
const client = new Client({ token: "dbot_…", signingSecret: "dsk_…" });
await client.events.load("./events"); // bind event handlers
await client.commands.load("./commands"); // load + start routing slash commands
await client.commands.register(); // publish the slash defs to District
await client.components.load("./components"); // route buttons / selects / modals
await client.listen(3000);Notes: files starting with _ and subfolders are supported (subfolders load
recursively; _helpers.js is skipped). Every handler receives the client as a
trailing argument. client.commands.cache is a Collection of the loaded
commands, keyed by name.
Events
| Event | Argument |
| --- | --- |
| ready | ClientUser |
| messageCreate | Message |
| messageUpdate | Message |
| messageDelete | { id, channelId, spaceId } |
| messageReactionAdd | { messageId, channelId, userId, emoji, spaceId } |
| messageReactionRemove | { messageId, channelId, userId, emoji, spaceId } |
| interactionCreate | Interaction |
| error | Error |
Using with Express
If you already run a server, mount the middleware (feed it the raw body so the signature verifies):
import express from "express";
const app = express();
app.post("/district", express.text({ type: "*/*" }), client.middleware());
await client.login();
app.listen(3000);Scopes & permissions
A bot only acts where its app is installed, and each action needs a scope
(send_messages, read_messages, manage_messages). See the
full API docs.
License
MIT
