district.js 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,47 @@ import { Formatters } from "district.js";
69
69
  channel.send(Formatters.bold("Heads up!") + " " + Formatters.code("v0.2.0"));
70
70
  ```
71
71
 
72
+ ## Embeds
73
+
74
+ ```js
75
+ import { EmbedBuilder } from "district.js";
76
+
77
+ const embed = new EmbedBuilder()
78
+ .setTitle("Deploy complete")
79
+ .setUrl("https://example.com/builds/128")
80
+ .setColor("#c6a86b")
81
+ .setDescription("Build **#128** shipped to production.")
82
+ .addFields(
83
+ { name: "Env", value: "prod", inline: true },
84
+ { name: "By", value: "ci-bot", inline: true },
85
+ )
86
+ .setFooter({ text: "district.js" })
87
+ .setTimestamp();
88
+
89
+ await channel.send({ content: "New release 🎉", embeds: [embed] });
90
+ ```
91
+
92
+ The District app renders it as an embed card (accent bar, title, fields, image).
93
+
94
+ ## Buttons
95
+
96
+ ```js
97
+ import { ActionRowBuilder, ButtonBuilder } from "district.js";
98
+
99
+ const row = new ActionRowBuilder().addComponents(
100
+ new ButtonBuilder().setLabel("Approve").setStyle("success").setCustomId("approve"),
101
+ new ButtonBuilder().setLabel("Deny").setStyle("danger").setCustomId("deny"),
102
+ new ButtonBuilder().setLabel("Docs").setUrl("https://usedistrict.org/developers/docs"),
103
+ );
104
+
105
+ await channel.send({ content: "Review this?", components: [row] });
106
+
107
+ // A click delivers an interactionCreate (needs an Event Subscription endpoint)
108
+ client.on("interactionCreate", (i) => {
109
+ if (i.isButton() && i.customId === "approve") i.reply("Approved ✅");
110
+ });
111
+ ```
112
+
72
113
  ## Slash commands
73
114
 
74
115
  Register the command in the portal (name + handler URL), then:
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import http from "node:http";
3
3
  import { REST } from "./rest.js";
4
- import { ClientUser, Message, TextChannel, Interaction, User, Space, type ClientContext } from "./structures.js";
4
+ import { ClientUser, Message, TextChannel, Interaction, User, Space, type ClientContext, type MessageContent } from "./structures.js";
5
5
  import { type EventHeaders } from "./webhook.js";
6
6
  import type { ClientOptions, ReactionEvent, DeletedMessage } from "./types.js";
7
7
  /** The events a Client emits, and their argument tuples. */
@@ -30,8 +30,8 @@ export declare class Client extends EventEmitter implements ClientContext {
30
30
  readonly channels: {
31
31
  /** Fetch a channel's info (kind, name, space). */
32
32
  fetch: (id: string) => Promise<TextChannel>;
33
- /** Send a message to a channel. */
34
- send: (channelId: string, content: string) => Promise<Message>;
33
+ /** Send a message to a channel (text or { content, embeds }). */
34
+ send: (channelId: string, content: MessageContent) => Promise<Message>;
35
35
  };
36
36
  /** Fetch users by id. */
37
37
  readonly users: {
package/dist/client.js CHANGED
@@ -38,7 +38,7 @@ export class Client extends EventEmitter {
38
38
  channels = {
39
39
  /** Fetch a channel's info (kind, name, space). */
40
40
  fetch: (id) => new TextChannel(this, id).fetch(),
41
- /** Send a message to a channel. */
41
+ /** Send a message to a channel (text or { content, embeds }). */
42
42
  send: (channelId, content) => new TextChannel(this, channelId).send(content),
43
43
  };
44
44
  /** Fetch users by id. */
@@ -0,0 +1,34 @@
1
+ export type ButtonStyle = "primary" | "secondary" | "success" | "danger" | "link";
2
+ export interface ButtonData {
3
+ type: "button";
4
+ style: ButtonStyle;
5
+ label: string;
6
+ custom_id?: string;
7
+ url?: string;
8
+ emoji?: string;
9
+ disabled?: boolean;
10
+ }
11
+ export interface ActionRowData {
12
+ type: "action_row";
13
+ components: ButtonData[];
14
+ }
15
+ export declare class ButtonBuilder {
16
+ data: Partial<ButtonData>;
17
+ setLabel(label: string): this;
18
+ setStyle(style: ButtonStyle): this;
19
+ /** A custom id you'll match in the interactionCreate handler (non-link buttons). */
20
+ setCustomId(id: string): this;
21
+ /** A link button — opens the URL, fires no interaction. */
22
+ setUrl(url: string): this;
23
+ setEmoji(emoji: string): this;
24
+ setDisabled(disabled?: boolean): this;
25
+ toJSON(): ButtonData;
26
+ }
27
+ export declare class ActionRowBuilder {
28
+ data: ActionRowData;
29
+ /** Add up to 5 buttons to this row. */
30
+ addComponents(...buttons: (ButtonBuilder | ButtonData)[]): this;
31
+ toJSON(): ActionRowData;
32
+ }
33
+ export type ComponentResolvable = ActionRowBuilder | ActionRowData;
34
+ export declare function resolveComponent(c: ComponentResolvable): ActionRowData;
@@ -0,0 +1,29 @@
1
+ // Interactive message components — action rows of buttons. Built objects go in
2
+ // a message's payload.components; the District app renders them, and a click on
3
+ // a non-link button fires an `interactionCreate` (type "component") back to the
4
+ // bot with the button's customId.
5
+ export class ButtonBuilder {
6
+ data = { type: "button", style: "secondary" };
7
+ setLabel(label) { this.data.label = label; return this; }
8
+ setStyle(style) { this.data.style = style; return this; }
9
+ /** A custom id you'll match in the interactionCreate handler (non-link buttons). */
10
+ setCustomId(id) { this.data.custom_id = id; return this; }
11
+ /** A link button — opens the URL, fires no interaction. */
12
+ setUrl(url) { this.data.url = url; this.data.style = "link"; return this; }
13
+ setEmoji(emoji) { this.data.emoji = emoji; return this; }
14
+ setDisabled(disabled = true) { this.data.disabled = disabled; return this; }
15
+ toJSON() { return this.data; }
16
+ }
17
+ export class ActionRowBuilder {
18
+ data = { type: "action_row", components: [] };
19
+ /** Add up to 5 buttons to this row. */
20
+ addComponents(...buttons) {
21
+ for (const b of buttons)
22
+ this.data.components.push(b instanceof ButtonBuilder ? b.toJSON() : b);
23
+ return this;
24
+ }
25
+ toJSON() { return this.data; }
26
+ }
27
+ export function resolveComponent(c) {
28
+ return c instanceof ActionRowBuilder ? c.toJSON() : c;
29
+ }
@@ -0,0 +1,55 @@
1
+ export interface EmbedField {
2
+ name: string;
3
+ value: string;
4
+ inline?: boolean;
5
+ }
6
+ export interface EmbedData {
7
+ title?: string;
8
+ description?: string;
9
+ url?: string;
10
+ color?: string;
11
+ author?: {
12
+ name: string;
13
+ iconUrl?: string;
14
+ url?: string;
15
+ };
16
+ fields?: EmbedField[];
17
+ image?: {
18
+ url: string;
19
+ };
20
+ thumbnail?: {
21
+ url: string;
22
+ };
23
+ footer?: {
24
+ text: string;
25
+ iconUrl?: string;
26
+ };
27
+ timestamp?: string;
28
+ }
29
+ export declare class EmbedBuilder {
30
+ data: EmbedData;
31
+ constructor(data?: EmbedData);
32
+ setTitle(title: string): this;
33
+ setDescription(description: string): this;
34
+ setUrl(url: string): this;
35
+ setColor(color: string | number): this;
36
+ setAuthor(author: {
37
+ name: string;
38
+ iconUrl?: string;
39
+ url?: string;
40
+ }): this;
41
+ setImage(url: string): this;
42
+ setThumbnail(url: string): this;
43
+ setFooter(footer: {
44
+ text: string;
45
+ iconUrl?: string;
46
+ }): this;
47
+ setTimestamp(ts?: Date | string | number): this;
48
+ /** Add one or more fields. */
49
+ addFields(...fields: EmbedField[]): this;
50
+ /** The plain object to send (also used implicitly by JSON.stringify). */
51
+ toJSON(): EmbedData;
52
+ }
53
+ /** Accept either a builder or a plain embed object wherever embeds are taken. */
54
+ export type EmbedResolvable = EmbedBuilder | EmbedData;
55
+ export declare function resolveEmbed(e: EmbedResolvable): EmbedData;
package/dist/embed.js ADDED
@@ -0,0 +1,37 @@
1
+ // A rich embed builder (discord.js-style). The built object is sent in a
2
+ // message's payload.embeds and rendered by the District app as an embed card.
3
+ function toHex(color) {
4
+ if (typeof color === "number")
5
+ return "#" + color.toString(16).padStart(6, "0");
6
+ return color.startsWith("#") ? color : "#" + color;
7
+ }
8
+ export class EmbedBuilder {
9
+ data;
10
+ constructor(data = {}) {
11
+ this.data = { ...data };
12
+ }
13
+ setTitle(title) { this.data.title = title; return this; }
14
+ setDescription(description) { this.data.description = description; return this; }
15
+ setUrl(url) { this.data.url = url; return this; }
16
+ setColor(color) { this.data.color = toHex(color); return this; }
17
+ setAuthor(author) { this.data.author = author; return this; }
18
+ setImage(url) { this.data.image = { url }; return this; }
19
+ setThumbnail(url) { this.data.thumbnail = { url }; return this; }
20
+ setFooter(footer) { this.data.footer = footer; return this; }
21
+ setTimestamp(ts = new Date()) {
22
+ this.data.timestamp = ts instanceof Date ? ts.toISOString() : new Date(ts).toISOString();
23
+ return this;
24
+ }
25
+ /** Add one or more fields. */
26
+ addFields(...fields) {
27
+ this.data.fields = [...(this.data.fields ?? []), ...fields];
28
+ return this;
29
+ }
30
+ /** The plain object to send (also used implicitly by JSON.stringify). */
31
+ toJSON() {
32
+ return this.data;
33
+ }
34
+ }
35
+ export function resolveEmbed(e) {
36
+ return e instanceof EmbedBuilder ? e.toJSON() : e;
37
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { Client, type ClientEvents } from "./client.js";
2
2
  export { REST, DistrictAPIError, DEFAULT_API_BASE } from "./rest.js";
3
- export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, Attachment, Ban, Invite, Webhook, type ClientContext, } from "./structures.js";
3
+ export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, Attachment, Ban, Invite, Webhook, type ClientContext, type MessageContent, } from "./structures.js";
4
+ export { EmbedBuilder, resolveEmbed, type EmbedData, type EmbedField, type EmbedResolvable } from "./embed.js";
5
+ export { ButtonBuilder, ActionRowBuilder, resolveComponent, type ButtonStyle, type ButtonData, type ActionRowData, type ComponentResolvable, } from "./components.js";
4
6
  export { Collection } from "./collection.js";
5
7
  export { PermissionsBitField, type PermissionResolvable, type PermissionFlagName } from "./permissions.js";
6
8
  export { Formatters } from "./formatters.js";
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@
7
7
  export { Client } from "./client.js";
8
8
  export { REST, DistrictAPIError, DEFAULT_API_BASE } from "./rest.js";
9
9
  export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, Attachment, Ban, Invite, Webhook, } from "./structures.js";
10
+ export { EmbedBuilder, resolveEmbed } from "./embed.js";
11
+ export { ButtonBuilder, ActionRowBuilder, resolveComponent, } from "./components.js";
10
12
  export { Collection } from "./collection.js";
11
13
  export { PermissionsBitField } from "./permissions.js";
12
14
  export { Formatters } from "./formatters.js";
@@ -2,6 +2,14 @@ import type { REST } from "./rest.js";
2
2
  import type { RawMessage, RawSelfUser, RawUser, RawSpace, RawChannel, RawRole, RawMember, RawAttachment, RawBan, RawInvite, RawWebhook } from "./types.js";
3
3
  import { Collection } from "./collection.js";
4
4
  import { PermissionsBitField } from "./permissions.js";
5
+ import { type EmbedResolvable } from "./embed.js";
6
+ import { type ComponentResolvable } from "./components.js";
7
+ /** What you can pass to send/reply: a string, or content + embeds + components. */
8
+ export type MessageContent = string | {
9
+ content?: string;
10
+ embeds?: EmbedResolvable[];
11
+ components?: ComponentResolvable[];
12
+ };
5
13
  /** Minimal client surface the structures need (avoids a circular import). */
6
14
  export interface ClientContext {
7
15
  rest: REST;
@@ -30,8 +38,8 @@ export declare class Message {
30
38
  constructor(client: ClientContext, raw: RawMessage);
31
39
  /** True if this message was sent by the current bot. */
32
40
  get authoredByMe(): boolean;
33
- /** Reply to this message (threads it via replyTo). */
34
- reply(content: string): Promise<Message>;
41
+ /** Reply to this message (threads it via replyTo). Accepts text or { content, embeds }. */
42
+ reply(content: MessageContent): Promise<Message>;
35
43
  /** Edit this message (only the bot's own messages). */
36
44
  edit(content: string): Promise<this>;
37
45
  /** Delete this message. */
@@ -61,8 +69,8 @@ export declare class TextChannel {
61
69
  fetch(): Promise<this>;
62
70
  /** @internal populate info fields from a raw channel row. */
63
71
  applyInfo(raw: RawChannel): this;
64
- /** Send a message to this channel. */
65
- send(content: string): Promise<Message>;
72
+ /** Send a message to this channel. Accepts text or { content, embeds }. */
73
+ send(content: MessageContent): Promise<Message>;
66
74
  /** Fetch recent messages (oldest → newest), up to 100. */
67
75
  fetchMessages(limit?: number): Promise<Collection<string, Message>>;
68
76
  /** The channel's webhooks. */
@@ -197,20 +205,33 @@ export declare class Role {
197
205
  }
198
206
  export declare class Interaction {
199
207
  private client;
208
+ /** "command" (slash command) or "component" (button click). */
209
+ type: "command" | "component";
200
210
  commandName: string;
201
211
  commandId: string;
212
+ /** The clicked button's customId (component interactions). */
213
+ customId: string;
214
+ /** The message the component is on (component interactions). */
215
+ messageId: string | null;
202
216
  spaceId: string;
203
217
  channelId: string;
204
218
  userId: string;
205
219
  args: Record<string, unknown>;
206
220
  constructor(client: ClientContext, raw: {
207
- command: string;
208
- command_id: string;
221
+ type?: string;
222
+ command?: string;
223
+ command_id?: string;
224
+ custom_id?: string;
225
+ message_id?: string;
209
226
  space_id: string;
210
227
  channel_id: string;
211
228
  user_id: string;
212
- args: Record<string, unknown>;
229
+ args?: Record<string, unknown>;
213
230
  });
231
+ /** True if this is a button click. */
232
+ isButton(): boolean;
233
+ /** True if this is a slash command. */
234
+ isCommand(): boolean;
214
235
  /** Respond to the command by posting a message in its channel. */
215
- reply(content: string): Promise<Message>;
236
+ reply(content: MessageContent): Promise<Message>;
216
237
  }
@@ -2,6 +2,17 @@
2
2
  // act on it directly: message.reply(), channel.send(), interaction.reply().
3
3
  import { Collection } from "./collection.js";
4
4
  import { PermissionsBitField } from "./permissions.js";
5
+ import { resolveEmbed } from "./embed.js";
6
+ import { resolveComponent } from "./components.js";
7
+ function toPayload(input) {
8
+ if (typeof input === "string")
9
+ return { content: input };
10
+ return {
11
+ content: input.content,
12
+ embeds: input.embeds?.map(resolveEmbed),
13
+ components: input.components?.map(resolveComponent),
14
+ };
15
+ }
5
16
  export class ClientUser {
6
17
  id;
7
18
  handle;
@@ -40,10 +51,11 @@ export class Message {
40
51
  }
41
52
  /** True if this message was sent by the current bot. */
42
53
  get authoredByMe() { return !!this.client.user && this.authorId === this.client.user.id; }
43
- /** Reply to this message (threads it via replyTo). */
54
+ /** Reply to this message (threads it via replyTo). Accepts text or { content, embeds }. */
44
55
  async reply(content) {
45
- const { message } = await this.client.rest.post(`/channels/${this.channelId}/messages`, { content, replyTo: this.id });
46
- return new Message(this.client, { ...message, content, author_id: this.client.user?.id ?? "" });
56
+ const p = toPayload(content);
57
+ const { message } = await this.client.rest.post(`/channels/${this.channelId}/messages`, { ...p, replyTo: this.id });
58
+ return new Message(this.client, { ...message, content: p.content ?? "", author_id: this.client.user?.id ?? "" });
47
59
  }
48
60
  /** Edit this message (only the bot's own messages). */
49
61
  async edit(content) {
@@ -105,10 +117,11 @@ export class TextChannel {
105
117
  this.position = raw.position ?? null;
106
118
  return this;
107
119
  }
108
- /** Send a message to this channel. */
120
+ /** Send a message to this channel. Accepts text or { content, embeds }. */
109
121
  async send(content) {
110
- const { message } = await this.client.rest.post(`/channels/${this.id}/messages`, { content });
111
- return new Message(this.client, { ...message, content, author_id: this.client.user?.id ?? "" });
122
+ const p = toPayload(content);
123
+ const { message } = await this.client.rest.post(`/channels/${this.id}/messages`, p);
124
+ return new Message(this.client, { ...message, content: p.content ?? "", author_id: this.client.user?.id ?? "" });
112
125
  }
113
126
  /** Fetch recent messages (oldest → newest), up to 100. */
114
127
  async fetchMessages(limit = 50) {
@@ -363,24 +376,38 @@ export class Role {
363
376
  }
364
377
  export class Interaction {
365
378
  client;
379
+ /** "command" (slash command) or "component" (button click). */
380
+ type;
366
381
  commandName;
367
382
  commandId;
383
+ /** The clicked button's customId (component interactions). */
384
+ customId;
385
+ /** The message the component is on (component interactions). */
386
+ messageId;
368
387
  spaceId;
369
388
  channelId;
370
389
  userId;
371
390
  args;
372
391
  constructor(client, raw) {
373
392
  this.client = client;
374
- this.commandName = raw.command;
375
- this.commandId = raw.command_id;
393
+ this.type = raw.type === "component" ? "component" : "command";
394
+ this.commandName = raw.command ?? "";
395
+ this.commandId = raw.command_id ?? "";
396
+ this.customId = raw.custom_id ?? "";
397
+ this.messageId = raw.message_id ?? null;
376
398
  this.spaceId = raw.space_id;
377
399
  this.channelId = raw.channel_id;
378
400
  this.userId = raw.user_id;
379
401
  this.args = raw.args ?? {};
380
402
  }
403
+ /** True if this is a button click. */
404
+ isButton() { return this.type === "component"; }
405
+ /** True if this is a slash command. */
406
+ isCommand() { return this.type === "command"; }
381
407
  /** Respond to the command by posting a message in its channel. */
382
408
  async reply(content) {
383
- const { message } = await this.client.rest.post(`/channels/${this.channelId}/messages`, { content });
384
- return new Message(this.client, { ...message, content, author_id: this.client.user?.id ?? "" });
409
+ const p = toPayload(content);
410
+ const { message } = await this.client.rest.post(`/channels/${this.channelId}/messages`, p);
411
+ return new Message(this.client, { ...message, content: p.content ?? "", author_id: this.client.user?.id ?? "" });
385
412
  }
386
413
  }
package/dist/types.d.ts CHANGED
@@ -146,12 +146,15 @@ export type DistrictEvent = {
146
146
  emoji: string;
147
147
  } | {
148
148
  event: "interaction.create";
149
- command: string;
150
- command_id: string;
149
+ type?: "command" | "component";
150
+ command?: string;
151
+ command_id?: string;
152
+ custom_id?: string;
153
+ message_id?: string;
151
154
  space_id: string;
152
155
  channel_id: string;
153
156
  user_id: string;
154
- args: Record<string, unknown>;
157
+ args?: Record<string, unknown>;
155
158
  };
156
159
  export interface ReactionEvent {
157
160
  spaceId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "district.js",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A powerful Node.js module for building District bots — post, react, run slash commands, and receive events.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",