district.js 0.3.0 → 0.6.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,75 @@ 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
+
113
+ ## Select menus & modals
114
+
115
+ ```js
116
+ import { ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ModalBuilder, TextInputBuilder } from "district.js";
117
+
118
+ // Dropdown
119
+ const menu = new ActionRowBuilder().addComponents(
120
+ new StringSelectMenuBuilder().setCustomId("role").setPlaceholder("Pick a role")
121
+ .addOptions({ label: "Dev", value: "dev" }, { label: "Design", value: "design" }),
122
+ );
123
+ await channel.send({ content: "Choose:", components: [menu] });
124
+
125
+ // Modal (opens instantly on click; only the submit is delivered to the bot)
126
+ const modal = new ModalBuilder().setCustomId("feedback").setTitle("Feedback")
127
+ .addComponents(new TextInputBuilder().setCustomId("msg").setLabel("Your message").setStyle("paragraph"));
128
+ const row = new ActionRowBuilder().addComponents(
129
+ new ButtonBuilder().setLabel("Give feedback").setStyle("primary").setModal(modal),
130
+ );
131
+ await channel.send({ components: [row] });
132
+
133
+ client.on("interactionCreate", (i) => {
134
+ if (i.isSelectMenu() && i.customId === "role") i.reply(`You picked ${i.values[0]}`);
135
+ if (i.isModalSubmit() && i.customId === "feedback") i.reply(`Thanks: ${i.field("msg")}`);
136
+ });
137
+ ```
138
+
139
+ > 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.
140
+
72
141
  ## Slash commands
73
142
 
74
143
  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,95 @@
1
+ export type ButtonStyle = "primary" | "secondary" | "success" | "danger" | "link";
2
+ export interface TextInputData {
3
+ custom_id: string;
4
+ label: string;
5
+ style: "short" | "paragraph";
6
+ placeholder?: string;
7
+ required?: boolean;
8
+ max_length?: number;
9
+ }
10
+ export interface ModalData {
11
+ custom_id: string;
12
+ title: string;
13
+ fields: TextInputData[];
14
+ }
15
+ export interface ButtonData {
16
+ type: "button";
17
+ style: ButtonStyle;
18
+ label: string;
19
+ custom_id?: string;
20
+ url?: string;
21
+ emoji?: string;
22
+ disabled?: boolean;
23
+ /** A modal shown (instantly, client-side) when the button is clicked. */
24
+ modal?: ModalData;
25
+ }
26
+ export interface SelectOption {
27
+ label: string;
28
+ value: string;
29
+ description?: string;
30
+ }
31
+ export interface SelectData {
32
+ type: "select";
33
+ custom_id: string;
34
+ options: SelectOption[];
35
+ placeholder?: string;
36
+ min_values?: number;
37
+ max_values?: number;
38
+ disabled?: boolean;
39
+ }
40
+ export type ComponentData = ButtonData | SelectData;
41
+ export interface ActionRowData {
42
+ type: "action_row";
43
+ components: ComponentData[];
44
+ }
45
+ export declare class ButtonBuilder {
46
+ data: Partial<ButtonData>;
47
+ setLabel(label: string): this;
48
+ setStyle(style: ButtonStyle): this;
49
+ /** A custom id you'll match in the interactionCreate handler (non-link buttons). */
50
+ setCustomId(id: string): this;
51
+ /** A link button — opens the URL, fires no interaction. */
52
+ setUrl(url: string): this;
53
+ setEmoji(emoji: string): this;
54
+ setDisabled(disabled?: boolean): this;
55
+ /** Attach a modal — clicking the button opens it instantly; submit fires a
56
+ * modal_submit interaction with the field values. */
57
+ setModal(modal: ModalBuilder | ModalData): this;
58
+ toJSON(): ButtonData;
59
+ }
60
+ export declare class StringSelectMenuBuilder {
61
+ data: SelectData;
62
+ setCustomId(id: string): this;
63
+ setPlaceholder(text: string): this;
64
+ setMinValues(n: number): this;
65
+ setMaxValues(n: number): this;
66
+ setDisabled(disabled?: boolean): this;
67
+ addOptions(...options: SelectOption[]): this;
68
+ toJSON(): SelectData;
69
+ }
70
+ export declare class TextInputBuilder {
71
+ data: Partial<TextInputData>;
72
+ setCustomId(id: string): this;
73
+ setLabel(label: string): this;
74
+ setStyle(style: "short" | "paragraph"): this;
75
+ setPlaceholder(text: string): this;
76
+ setRequired(required?: boolean): this;
77
+ setMaxLength(n: number): this;
78
+ toJSON(): TextInputData;
79
+ }
80
+ export declare class ModalBuilder {
81
+ data: ModalData;
82
+ setCustomId(id: string): this;
83
+ setTitle(title: string): this;
84
+ /** Add text inputs to the modal (up to 5). */
85
+ addComponents(...inputs: (TextInputBuilder | TextInputData)[]): this;
86
+ toJSON(): ModalData;
87
+ }
88
+ export declare class ActionRowBuilder {
89
+ data: ActionRowData;
90
+ /** Add up to 5 buttons, or a single select menu. */
91
+ addComponents(...components: (ButtonBuilder | StringSelectMenuBuilder | ComponentData)[]): this;
92
+ toJSON(): ActionRowData;
93
+ }
94
+ export type ComponentResolvable = ActionRowBuilder | ActionRowData;
95
+ export declare function resolveComponent(c: ComponentResolvable): ActionRowData;
@@ -0,0 +1,73 @@
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
+ /** Attach a modal — clicking the button opens it instantly; submit fires a
16
+ * modal_submit interaction with the field values. */
17
+ setModal(modal) {
18
+ this.data.modal = modal instanceof ModalBuilder ? modal.toJSON() : modal;
19
+ if (!this.data.custom_id)
20
+ this.data.custom_id = this.data.modal.custom_id;
21
+ return this;
22
+ }
23
+ toJSON() { return this.data; }
24
+ }
25
+ export class StringSelectMenuBuilder {
26
+ data = { type: "select", custom_id: "", options: [] };
27
+ setCustomId(id) { this.data.custom_id = id; return this; }
28
+ setPlaceholder(text) { this.data.placeholder = text; return this; }
29
+ setMinValues(n) { this.data.min_values = n; return this; }
30
+ setMaxValues(n) { this.data.max_values = n; return this; }
31
+ setDisabled(disabled = true) { this.data.disabled = disabled; return this; }
32
+ addOptions(...options) {
33
+ this.data.options.push(...options);
34
+ return this;
35
+ }
36
+ toJSON() { return this.data; }
37
+ }
38
+ export class TextInputBuilder {
39
+ data = { style: "short", required: true };
40
+ setCustomId(id) { this.data.custom_id = id; return this; }
41
+ setLabel(label) { this.data.label = label; return this; }
42
+ setStyle(style) { this.data.style = style; return this; }
43
+ setPlaceholder(text) { this.data.placeholder = text; return this; }
44
+ setRequired(required = true) { this.data.required = required; return this; }
45
+ setMaxLength(n) { this.data.max_length = n; return this; }
46
+ toJSON() { return this.data; }
47
+ }
48
+ export class ModalBuilder {
49
+ data = { custom_id: "", title: "", fields: [] };
50
+ setCustomId(id) { this.data.custom_id = id; return this; }
51
+ setTitle(title) { this.data.title = title; return this; }
52
+ /** Add text inputs to the modal (up to 5). */
53
+ addComponents(...inputs) {
54
+ for (const i of inputs)
55
+ this.data.fields.push(i instanceof TextInputBuilder ? i.toJSON() : i);
56
+ return this;
57
+ }
58
+ toJSON() { return this.data; }
59
+ }
60
+ export class ActionRowBuilder {
61
+ data = { type: "action_row", components: [] };
62
+ /** Add up to 5 buttons, or a single select menu. */
63
+ addComponents(...components) {
64
+ for (const c of components) {
65
+ this.data.components.push(c instanceof ButtonBuilder || c instanceof StringSelectMenuBuilder ? c.toJSON() : c);
66
+ }
67
+ return this;
68
+ }
69
+ toJSON() { return this.data; }
70
+ }
71
+ export function resolveComponent(c) {
72
+ return c instanceof ActionRowBuilder ? c.toJSON() : c;
73
+ }
@@ -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, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, resolveComponent, type ButtonStyle, type ButtonData, type SelectData, type SelectOption, type ComponentData, type ModalData, type TextInputData, 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, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, 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,47 @@ export declare class Role {
197
205
  }
198
206
  export declare class Interaction {
199
207
  private client;
208
+ /** "command" (slash), "component" (button/select), or "modal_submit". */
209
+ type: "command" | "component" | "modal_submit";
200
210
  commandName: string;
201
211
  commandId: string;
212
+ /** The component/modal customId that triggered this. */
213
+ customId: string;
214
+ /** The message the component is on. */
215
+ messageId: string | null;
216
+ /** Selected values (select-menu interactions). */
217
+ values: string[];
218
+ /** Submitted field values keyed by input customId (modal submits). */
219
+ fields: Record<string, string>;
202
220
  spaceId: string;
203
221
  channelId: string;
204
222
  userId: string;
205
223
  args: Record<string, unknown>;
206
224
  constructor(client: ClientContext, raw: {
207
- command: string;
208
- command_id: string;
225
+ type?: string;
226
+ command?: string;
227
+ command_id?: string;
228
+ custom_id?: string;
229
+ message_id?: string;
209
230
  space_id: string;
210
231
  channel_id: string;
211
232
  user_id: string;
212
- args: Record<string, unknown>;
233
+ args?: Record<string, unknown>;
234
+ data?: {
235
+ values?: string[];
236
+ fields?: Record<string, string>;
237
+ };
213
238
  });
239
+ /** True if this is a slash command. */
240
+ isCommand(): boolean;
241
+ /** True if this is a button click (component with no selected values). */
242
+ isButton(): boolean;
243
+ /** True if this is a select-menu choice. */
244
+ isSelectMenu(): boolean;
245
+ /** True if this is a submitted modal. */
246
+ isModalSubmit(): boolean;
247
+ /** Get a submitted modal field value by its input customId. */
248
+ field(customId: string): string | undefined;
214
249
  /** Respond to the command by posting a message in its channel. */
215
- reply(content: string): Promise<Message>;
250
+ reply(content: MessageContent): Promise<Message>;
216
251
  }
@@ -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,50 @@ export class Role {
363
376
  }
364
377
  export class Interaction {
365
378
  client;
379
+ /** "command" (slash), "component" (button/select), or "modal_submit". */
380
+ type;
366
381
  commandName;
367
382
  commandId;
383
+ /** The component/modal customId that triggered this. */
384
+ customId;
385
+ /** The message the component is on. */
386
+ messageId;
387
+ /** Selected values (select-menu interactions). */
388
+ values;
389
+ /** Submitted field values keyed by input customId (modal submits). */
390
+ fields;
368
391
  spaceId;
369
392
  channelId;
370
393
  userId;
371
394
  args;
372
395
  constructor(client, raw) {
373
396
  this.client = client;
374
- this.commandName = raw.command;
375
- this.commandId = raw.command_id;
397
+ this.type = raw.type === "component" ? "component" : raw.type === "modal_submit" ? "modal_submit" : "command";
398
+ this.commandName = raw.command ?? "";
399
+ this.commandId = raw.command_id ?? "";
400
+ this.customId = raw.custom_id ?? "";
401
+ this.messageId = raw.message_id ?? null;
402
+ this.values = Array.isArray(raw.data?.values) ? raw.data.values : [];
403
+ this.fields = raw.data?.fields && typeof raw.data.fields === "object" ? raw.data.fields : {};
376
404
  this.spaceId = raw.space_id;
377
405
  this.channelId = raw.channel_id;
378
406
  this.userId = raw.user_id;
379
407
  this.args = raw.args ?? {};
380
408
  }
409
+ /** True if this is a slash command. */
410
+ isCommand() { return this.type === "command"; }
411
+ /** True if this is a button click (component with no selected values). */
412
+ isButton() { return this.type === "component" && this.values.length === 0; }
413
+ /** True if this is a select-menu choice. */
414
+ isSelectMenu() { return this.type === "component" && this.values.length > 0; }
415
+ /** True if this is a submitted modal. */
416
+ isModalSubmit() { return this.type === "modal_submit"; }
417
+ /** Get a submitted modal field value by its input customId. */
418
+ field(customId) { return this.fields[customId]; }
381
419
  /** Respond to the command by posting a message in its channel. */
382
420
  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 ?? "" });
421
+ const p = toPayload(content);
422
+ const { message } = await this.client.rest.post(`/channels/${this.channelId}/messages`, p);
423
+ return new Message(this.client, { ...message, content: p.content ?? "", author_id: this.client.user?.id ?? "" });
385
424
  }
386
425
  }
package/dist/types.d.ts CHANGED
@@ -146,12 +146,19 @@ 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" | "modal_submit";
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>;
158
+ data?: {
159
+ values?: string[];
160
+ fields?: Record<string, string>;
161
+ };
155
162
  };
156
163
  export interface ReactionEvent {
157
164
  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.6.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",