district.js 0.2.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
+ }
@@ -0,0 +1,15 @@
1
+ export declare const ChannelType: {
2
+ readonly Text: "text";
3
+ readonly Voice: "voice";
4
+ readonly Stage: "stage";
5
+ readonly Event: "event";
6
+ readonly Lobby: "lobby";
7
+ readonly Announcement: "announcement";
8
+ readonly Rules: "rules";
9
+ readonly Calendar: "calendar";
10
+ readonly Media: "media";
11
+ readonly DM: "dm";
12
+ readonly Overview: "overview";
13
+ readonly AuditLog: "audit-log";
14
+ };
15
+ export type ChannelTypeValue = (typeof ChannelType)[keyof typeof ChannelType];
package/dist/enums.js ADDED
@@ -0,0 +1,15 @@
1
+ // District channel kinds (mirrors the channels.kind check constraint).
2
+ export const ChannelType = {
3
+ Text: "text",
4
+ Voice: "voice",
5
+ Stage: "stage",
6
+ Event: "event",
7
+ Lobby: "lobby",
8
+ Announcement: "announcement",
9
+ Rules: "rules",
10
+ Calendar: "calendar",
11
+ Media: "media",
12
+ DM: "dm",
13
+ Overview: "overview",
14
+ AuditLog: "audit-log",
15
+ };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
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, 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";
9
+ export { ChannelType, type ChannelTypeValue } from "./enums.js";
7
10
  export { verifySignature, parseEvent, verifyAndParse, type EventHeaders } from "./webhook.js";
8
- export type { ClientOptions, RawMessage, RawSelfUser, RawUser, RawSpace, RawChannel, RawRole, RawMember, DistrictEvent, ReactionEvent, DeletedMessage, } from "./types.js";
11
+ export type { ClientOptions, RawMessage, RawSelfUser, RawUser, RawSpace, RawChannel, RawRole, RawMember, RawAttachment, RawBan, RawInvite, RawWebhook, DistrictEvent, ReactionEvent, DeletedMessage, } from "./types.js";
package/dist/index.js CHANGED
@@ -6,8 +6,11 @@
6
6
  // client.listen(3000);
7
7
  export { Client } from "./client.js";
8
8
  export { REST, DistrictAPIError, DEFAULT_API_BASE } from "./rest.js";
9
- export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, } from "./structures.js";
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";
15
+ export { ChannelType } from "./enums.js";
13
16
  export { verifySignature, parseEvent, verifyAndParse } from "./webhook.js";
@@ -1,7 +1,15 @@
1
1
  import type { REST } from "./rest.js";
2
- import type { RawMessage, RawSelfUser, RawUser, RawSpace, RawChannel, RawRole, RawMember } from "./types.js";
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;
@@ -25,11 +33,13 @@ export declare class Message {
25
33
  createdAt: string | null;
26
34
  editedAt: string | null;
27
35
  parentId: string | null;
36
+ /** Media/files on the message (populated from fetchMessages / fetchAttachments). */
37
+ attachments: Attachment[];
28
38
  constructor(client: ClientContext, raw: RawMessage);
29
39
  /** True if this message was sent by the current bot. */
30
40
  get authoredByMe(): boolean;
31
- /** Reply to this message (threads it via replyTo). */
32
- 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>;
33
43
  /** Edit this message (only the bot's own messages). */
34
44
  edit(content: string): Promise<this>;
35
45
  /** Delete this message. */
@@ -42,6 +52,8 @@ export declare class Message {
42
52
  fetchAuthor(): Promise<User>;
43
53
  /** A handle to the channel this message is in. */
44
54
  get channel(): TextChannel;
55
+ /** Fetch this message's attachments (useful for messages from events). */
56
+ fetchAttachments(): Promise<Attachment[]>;
45
57
  }
46
58
  export declare class TextChannel {
47
59
  private client;
@@ -57,10 +69,16 @@ export declare class TextChannel {
57
69
  fetch(): Promise<this>;
58
70
  /** @internal populate info fields from a raw channel row. */
59
71
  applyInfo(raw: RawChannel): this;
60
- /** Send a message to this channel. */
61
- send(content: string): Promise<Message>;
72
+ /** Send a message to this channel. Accepts text or { content, embeds }. */
73
+ send(content: MessageContent): Promise<Message>;
62
74
  /** Fetch recent messages (oldest → newest), up to 100. */
63
75
  fetchMessages(limit?: number): Promise<Collection<string, Message>>;
76
+ /** The channel's webhooks. */
77
+ webhooks(): Promise<Collection<string, Webhook>>;
78
+ /** Create a webhook. The returned Webhook has a one-time url + token you can send() with. */
79
+ createWebhook(name: string, options?: {
80
+ avatarUrl?: string;
81
+ }): Promise<Webhook>;
64
82
  }
65
83
  /** A District user's public profile. */
66
84
  export declare class User {
@@ -94,6 +112,76 @@ export declare class Space {
94
112
  roles(): Promise<Collection<string, Role>>;
95
113
  /** The space owner's profile. */
96
114
  fetchOwner(): Promise<User>;
115
+ /** Banned users. */
116
+ bans(): Promise<Collection<string, Ban>>;
117
+ /** Ban a user (needs the Ban Members permission). Removes their membership. */
118
+ ban(userId: string, options?: {
119
+ reason?: string;
120
+ until?: string | Date;
121
+ }): Promise<void>;
122
+ /** Lift a user's ban. */
123
+ unban(userId: string): Promise<void>;
124
+ /** Active invites. */
125
+ invites(): Promise<Collection<string, Invite>>;
126
+ /** Create an invite (needs Create Invites). `expiresIn` is seconds. */
127
+ createInvite(options?: {
128
+ maxUses?: number;
129
+ expiresIn?: number;
130
+ }): Promise<Invite>;
131
+ }
132
+ /** A file or media on a message. */
133
+ export declare class Attachment {
134
+ id: string;
135
+ kind: string;
136
+ url: string;
137
+ thumbUrl: string | null;
138
+ filename: string | null;
139
+ mimeType: string | null;
140
+ size: number | null;
141
+ width: number | null;
142
+ height: number | null;
143
+ spoiler: boolean;
144
+ constructor(raw: RawAttachment);
145
+ get isImage(): boolean;
146
+ get isVideo(): boolean;
147
+ }
148
+ /** A space ban. */
149
+ export declare class Ban {
150
+ userId: string;
151
+ reason: string | null;
152
+ until: string | null;
153
+ by: string;
154
+ constructor(raw: RawBan);
155
+ /** True if the ban is permanent (no expiry). */
156
+ get permanent(): boolean;
157
+ }
158
+ /** A space invite. */
159
+ export declare class Invite {
160
+ id: string;
161
+ code: string;
162
+ url: string;
163
+ maxUses: number | null;
164
+ useCount: number;
165
+ expiresAt: string | null;
166
+ constructor(raw: RawInvite);
167
+ }
168
+ /** A channel webhook. `url`/`token` are only present on the one you just created. */
169
+ export declare class Webhook {
170
+ private client;
171
+ id: string;
172
+ name: string;
173
+ channelId: string;
174
+ avatarUrl: string | null;
175
+ url: string | null;
176
+ token: string | null;
177
+ constructor(client: ClientContext, raw: RawWebhook);
178
+ /** Post via this webhook. Only works on a webhook you just created (has a url). */
179
+ send(content: string, options?: {
180
+ username?: string;
181
+ avatarUrl?: string;
182
+ }): Promise<void>;
183
+ /** Delete this webhook. */
184
+ delete(): Promise<void>;
97
185
  }
98
186
  /** A user's membership in a space. */
99
187
  export declare class SpaceMember {
@@ -117,20 +205,33 @@ export declare class Role {
117
205
  }
118
206
  export declare class Interaction {
119
207
  private client;
208
+ /** "command" (slash command) or "component" (button click). */
209
+ type: "command" | "component";
120
210
  commandName: string;
121
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;
122
216
  spaceId: string;
123
217
  channelId: string;
124
218
  userId: string;
125
219
  args: Record<string, unknown>;
126
220
  constructor(client: ClientContext, raw: {
127
- command: string;
128
- command_id: string;
221
+ type?: string;
222
+ command?: string;
223
+ command_id?: string;
224
+ custom_id?: string;
225
+ message_id?: string;
129
226
  space_id: string;
130
227
  channel_id: string;
131
228
  user_id: string;
132
- args: Record<string, unknown>;
229
+ args?: Record<string, unknown>;
133
230
  });
231
+ /** True if this is a button click. */
232
+ isButton(): boolean;
233
+ /** True if this is a slash command. */
234
+ isCommand(): boolean;
134
235
  /** Respond to the command by posting a message in its channel. */
135
- reply(content: string): Promise<Message>;
236
+ reply(content: MessageContent): Promise<Message>;
136
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;
@@ -25,6 +36,8 @@ export class Message {
25
36
  createdAt;
26
37
  editedAt;
27
38
  parentId;
39
+ /** Media/files on the message (populated from fetchMessages / fetchAttachments). */
40
+ attachments;
28
41
  constructor(client, raw) {
29
42
  this.client = client;
30
43
  this.id = raw.id;
@@ -34,13 +47,15 @@ export class Message {
34
47
  this.createdAt = raw.created_at ?? null;
35
48
  this.editedAt = raw.edited_at ?? null;
36
49
  this.parentId = raw.parent_id ?? null;
50
+ this.attachments = (raw.attachments ?? []).map((a) => new Attachment(a));
37
51
  }
38
52
  /** True if this message was sent by the current bot. */
39
53
  get authoredByMe() { return !!this.client.user && this.authorId === this.client.user.id; }
40
- /** Reply to this message (threads it via replyTo). */
54
+ /** Reply to this message (threads it via replyTo). Accepts text or { content, embeds }. */
41
55
  async reply(content) {
42
- const { message } = await this.client.rest.post(`/channels/${this.channelId}/messages`, { content, replyTo: this.id });
43
- 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 ?? "" });
44
59
  }
45
60
  /** Edit this message (only the bot's own messages). */
46
61
  async edit(content) {
@@ -69,6 +84,11 @@ export class Message {
69
84
  get channel() {
70
85
  return new TextChannel(this.client, this.channelId);
71
86
  }
87
+ /** Fetch this message's attachments (useful for messages from events). */
88
+ async fetchAttachments() {
89
+ const { attachments } = await this.client.rest.get(`/channels/${this.channelId}/messages/${this.id}/attachments`);
90
+ return (attachments ?? []).map((a) => new Attachment(a));
91
+ }
72
92
  }
73
93
  export class TextChannel {
74
94
  client;
@@ -97,10 +117,11 @@ export class TextChannel {
97
117
  this.position = raw.position ?? null;
98
118
  return this;
99
119
  }
100
- /** Send a message to this channel. */
120
+ /** Send a message to this channel. Accepts text or { content, embeds }. */
101
121
  async send(content) {
102
- const { message } = await this.client.rest.post(`/channels/${this.id}/messages`, { content });
103
- 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 ?? "" });
104
125
  }
105
126
  /** Fetch recent messages (oldest → newest), up to 100. */
106
127
  async fetchMessages(limit = 50) {
@@ -110,6 +131,19 @@ export class TextChannel {
110
131
  out.set(m.id, new Message(this.client, m));
111
132
  return out;
112
133
  }
134
+ /** The channel's webhooks. */
135
+ async webhooks() {
136
+ const { webhooks } = await this.client.rest.get(`/channels/${this.id}/webhooks`);
137
+ const out = new Collection();
138
+ for (const w of (webhooks ?? []))
139
+ out.set(w.id, new Webhook(this.client, w));
140
+ return out;
141
+ }
142
+ /** Create a webhook. The returned Webhook has a one-time url + token you can send() with. */
143
+ async createWebhook(name, options) {
144
+ const { webhook } = await this.client.rest.post(`/channels/${this.id}/webhooks`, { name, avatarUrl: options?.avatarUrl });
145
+ return new Webhook(this.client, webhook);
146
+ }
113
147
  }
114
148
  /** A District user's public profile. */
115
149
  export class User {
@@ -182,6 +216,128 @@ export class Space {
182
216
  const { user } = await this.client.rest.get(`/users/${this.ownerId}`);
183
217
  return new User(user);
184
218
  }
219
+ /** Banned users. */
220
+ async bans() {
221
+ const { bans } = await this.client.rest.get(`/spaces/${this.id}/bans`);
222
+ const out = new Collection();
223
+ for (const b of (bans ?? []))
224
+ out.set(b.user_id, new Ban(b));
225
+ return out;
226
+ }
227
+ /** Ban a user (needs the Ban Members permission). Removes their membership. */
228
+ async ban(userId, options) {
229
+ const until = options?.until ? new Date(options.until).toISOString() : undefined;
230
+ await this.client.rest.put(`/spaces/${this.id}/bans/${userId}`, { reason: options?.reason, until });
231
+ }
232
+ /** Lift a user's ban. */
233
+ async unban(userId) {
234
+ await this.client.rest.delete(`/spaces/${this.id}/bans/${userId}`);
235
+ }
236
+ /** Active invites. */
237
+ async invites() {
238
+ const { invites } = await this.client.rest.get(`/spaces/${this.id}/invites`);
239
+ const out = new Collection();
240
+ for (const i of (invites ?? []))
241
+ out.set(i.code, new Invite(i));
242
+ return out;
243
+ }
244
+ /** Create an invite (needs Create Invites). `expiresIn` is seconds. */
245
+ async createInvite(options) {
246
+ const { invite } = await this.client.rest.post(`/spaces/${this.id}/invites`, { maxUses: options?.maxUses, expiresIn: options?.expiresIn });
247
+ return new Invite(invite);
248
+ }
249
+ }
250
+ /** A file or media on a message. */
251
+ export class Attachment {
252
+ id;
253
+ kind;
254
+ url;
255
+ thumbUrl;
256
+ filename;
257
+ mimeType;
258
+ size;
259
+ width;
260
+ height;
261
+ spoiler;
262
+ constructor(raw) {
263
+ this.id = raw.id;
264
+ this.kind = raw.kind;
265
+ this.url = raw.url;
266
+ this.thumbUrl = raw.thumb_url ?? null;
267
+ this.filename = raw.filename ?? null;
268
+ this.mimeType = raw.mime_type ?? null;
269
+ this.size = raw.size_bytes ?? null;
270
+ this.width = raw.width ?? null;
271
+ this.height = raw.height ?? null;
272
+ this.spoiler = raw.spoiler ?? false;
273
+ }
274
+ get isImage() { return this.kind === "image" || this.kind === "gifv"; }
275
+ get isVideo() { return this.kind === "video"; }
276
+ }
277
+ /** A space ban. */
278
+ export class Ban {
279
+ userId;
280
+ reason;
281
+ until;
282
+ by;
283
+ constructor(raw) {
284
+ this.userId = raw.user_id;
285
+ this.reason = raw.reason;
286
+ this.until = raw.until;
287
+ this.by = raw.by;
288
+ }
289
+ /** True if the ban is permanent (no expiry). */
290
+ get permanent() { return this.until === null; }
291
+ }
292
+ /** A space invite. */
293
+ export class Invite {
294
+ id;
295
+ code;
296
+ url;
297
+ maxUses;
298
+ useCount;
299
+ expiresAt;
300
+ constructor(raw) {
301
+ this.id = raw.id;
302
+ this.code = raw.code;
303
+ this.url = raw.url ?? `https://usedistrict.org/i/${raw.code}`;
304
+ this.maxUses = raw.max_uses;
305
+ this.useCount = raw.use_count ?? 0;
306
+ this.expiresAt = raw.expires_at;
307
+ }
308
+ }
309
+ /** A channel webhook. `url`/`token` are only present on the one you just created. */
310
+ export class Webhook {
311
+ client;
312
+ id;
313
+ name;
314
+ channelId;
315
+ avatarUrl;
316
+ url;
317
+ token;
318
+ constructor(client, raw) {
319
+ this.client = client;
320
+ this.id = raw.id;
321
+ this.name = raw.name;
322
+ this.channelId = raw.channel_id;
323
+ this.avatarUrl = raw.avatar_url ?? null;
324
+ this.url = raw.url ?? null;
325
+ this.token = raw.token ?? null;
326
+ }
327
+ /** Post via this webhook. Only works on a webhook you just created (has a url). */
328
+ async send(content, options) {
329
+ if (!this.url)
330
+ throw new Error("This webhook has no url/token — those are only returned when it's first created.");
331
+ await fetch(this.url, {
332
+ method: "POST",
333
+ headers: { "content-type": "application/json" },
334
+ body: JSON.stringify({ content, username: options?.username, avatar_url: options?.avatarUrl }),
335
+ });
336
+ }
337
+ /** Delete this webhook. */
338
+ async delete() {
339
+ await this.client.rest.delete(`/channels/${this.channelId}/webhooks/${this.id}`);
340
+ }
185
341
  }
186
342
  /** A user's membership in a space. */
187
343
  export class SpaceMember {
@@ -220,24 +376,38 @@ export class Role {
220
376
  }
221
377
  export class Interaction {
222
378
  client;
379
+ /** "command" (slash command) or "component" (button click). */
380
+ type;
223
381
  commandName;
224
382
  commandId;
383
+ /** The clicked button's customId (component interactions). */
384
+ customId;
385
+ /** The message the component is on (component interactions). */
386
+ messageId;
225
387
  spaceId;
226
388
  channelId;
227
389
  userId;
228
390
  args;
229
391
  constructor(client, raw) {
230
392
  this.client = client;
231
- this.commandName = raw.command;
232
- 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;
233
398
  this.spaceId = raw.space_id;
234
399
  this.channelId = raw.channel_id;
235
400
  this.userId = raw.user_id;
236
401
  this.args = raw.args ?? {};
237
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"; }
238
407
  /** Respond to the command by posting a message in its channel. */
239
408
  async reply(content) {
240
- const { message } = await this.client.rest.post(`/channels/${this.channelId}/messages`, { content });
241
- 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 ?? "" });
242
412
  }
243
413
  }
package/dist/types.d.ts CHANGED
@@ -19,6 +19,47 @@ export interface RawMessage {
19
19
  created_at?: string;
20
20
  edited_at?: string | null;
21
21
  parent_id?: string | null;
22
+ attachments?: RawAttachment[];
23
+ }
24
+ /** A message attachment (image/video/file/…). */
25
+ export interface RawAttachment {
26
+ id: string;
27
+ kind: string;
28
+ url: string;
29
+ thumb_url?: string | null;
30
+ filename?: string | null;
31
+ mime_type?: string | null;
32
+ size_bytes?: number | null;
33
+ width?: number | null;
34
+ height?: number | null;
35
+ spoiler?: boolean;
36
+ }
37
+ /** A space ban (`GET /spaces/:id/bans`). */
38
+ export interface RawBan {
39
+ user_id: string;
40
+ reason: string | null;
41
+ until: string | null;
42
+ by: string;
43
+ created_at?: string;
44
+ }
45
+ /** A space invite (`GET/POST /spaces/:id/invites`). */
46
+ export interface RawInvite {
47
+ id: string;
48
+ code: string;
49
+ url?: string;
50
+ max_uses: number | null;
51
+ use_count?: number;
52
+ expires_at: string | null;
53
+ revoked_at?: string | null;
54
+ }
55
+ /** A channel webhook (`GET/POST /channels/:id/webhooks`). */
56
+ export interface RawWebhook {
57
+ id: string;
58
+ name: string;
59
+ channel_id: string;
60
+ avatar_url?: string | null;
61
+ token?: string;
62
+ url?: string;
22
63
  }
23
64
  /** The bot's own identity (`GET /me`). */
24
65
  export interface RawSelfUser {
@@ -105,12 +146,15 @@ export type DistrictEvent = {
105
146
  emoji: string;
106
147
  } | {
107
148
  event: "interaction.create";
108
- command: string;
109
- command_id: string;
149
+ type?: "command" | "component";
150
+ command?: string;
151
+ command_id?: string;
152
+ custom_id?: string;
153
+ message_id?: string;
110
154
  space_id: string;
111
155
  channel_id: string;
112
156
  user_id: string;
113
- args: Record<string, unknown>;
157
+ args?: Record<string, unknown>;
114
158
  };
115
159
  export interface ReactionEvent {
116
160
  spaceId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "district.js",
3
- "version": "0.2.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",