district.js 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 koya (District)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -42,8 +42,31 @@ await msg.edit("hello, world!");
42
42
  await msg.react("👍");
43
43
  await msg.delete();
44
44
 
45
- // Read history
45
+ // Read history (returns a Collection)
46
46
  const recent = await client.channel(channelId).fetchMessages(50);
47
+ const commands = recent.filter((m) => m.content?.startsWith("!"));
48
+ ```
49
+
50
+ ## Users, spaces & permissions
51
+
52
+ ```js
53
+ const user = await message.fetchAuthor(); // User
54
+ const space = await client.spaces.fetch(spaceId); // Space (app must be installed)
55
+
56
+ const members = await space.members(); // Collection<SpaceMember>
57
+ const roles = await space.roles(); // Collection<Role>
58
+ const named = members.filter((m) => m.nickname !== null);
59
+ const canBan = roles.some((r) => r.permissions.has("BanMembers"));
60
+
61
+ // Permissions
62
+ import { PermissionsBitField } from "district.js";
63
+ const perms = new PermissionsBitField(["SendMessages", "AddReactions"]);
64
+ perms.has(PermissionsBitField.Flags.SendMessages); // true
65
+ perms.toString(); // "32770" — use as the install-link permissions integer
66
+
67
+ // Markdown
68
+ import { Formatters } from "district.js";
69
+ channel.send(Formatters.bold("Heads up!") + " " + Formatters.code("v0.2.0"));
47
70
  ```
48
71
 
49
72
  ## Slash commands
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, type ClientContext } from "./structures.js";
4
+ import { ClientUser, Message, TextChannel, Interaction, User, Space, type ClientContext } 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. */
@@ -28,8 +28,18 @@ export declare class Client extends EventEmitter implements ClientContext {
28
28
  /** A handle to a channel, for `.send()` / `.fetchMessages()`. */
29
29
  channel(id: string): TextChannel;
30
30
  readonly channels: {
31
+ /** Fetch a channel's info (kind, name, space). */
32
+ fetch: (id: string) => Promise<TextChannel>;
33
+ /** Send a message to a channel. */
31
34
  send: (channelId: string, content: string) => Promise<Message>;
32
- fetch: (channelId: string, limit?: number) => Promise<Message[]>;
35
+ };
36
+ /** Fetch users by id. */
37
+ readonly users: {
38
+ fetch: (id: string) => Promise<User>;
39
+ };
40
+ /** Fetch spaces by id. */
41
+ readonly spaces: {
42
+ fetch: (id: string) => Promise<Space>;
33
43
  };
34
44
  /** Validate the token, load the bot's identity, and emit `ready`. */
35
45
  login(): Promise<ClientUser>;
package/dist/client.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { EventEmitter } from "node:events";
4
4
  import http from "node:http";
5
5
  import { REST } from "./rest.js";
6
- import { ClientUser, Message, TextChannel, Interaction } from "./structures.js";
6
+ import { ClientUser, Message, TextChannel, Interaction, User, Space } from "./structures.js";
7
7
  import { verifyAndParse } from "./webhook.js";
8
8
  export class Client extends EventEmitter {
9
9
  rest;
@@ -36,8 +36,18 @@ export class Client extends EventEmitter {
36
36
  return new TextChannel(this, id);
37
37
  }
38
38
  channels = {
39
+ /** Fetch a channel's info (kind, name, space). */
40
+ fetch: (id) => new TextChannel(this, id).fetch(),
41
+ /** Send a message to a channel. */
39
42
  send: (channelId, content) => new TextChannel(this, channelId).send(content),
40
- fetch: (channelId, limit) => new TextChannel(this, channelId).fetchMessages(limit),
43
+ };
44
+ /** Fetch users by id. */
45
+ users = {
46
+ fetch: (id) => this.rest.get(`/users/${id}`).then((r) => new User(r.user)),
47
+ };
48
+ /** Fetch spaces by id. */
49
+ spaces = {
50
+ fetch: (id) => this.rest.get(`/spaces/${id}`).then((r) => new Space(this, r.space)),
41
51
  };
42
52
  /** Validate the token, load the bot's identity, and emit `ready`. */
43
53
  async login() {
@@ -0,0 +1,23 @@
1
+ export declare class Collection<K, V> extends Map<K, V> {
2
+ /** The first value (or first N as an array). */
3
+ first(): V | undefined;
4
+ first(n: number): V[];
5
+ /** The last value. */
6
+ last(): V | undefined;
7
+ /** The first value matching a predicate. */
8
+ find(fn: (value: V, key: K) => boolean): V | undefined;
9
+ /** A new Collection of the values matching a predicate. */
10
+ filter(fn: (value: V, key: K) => boolean): Collection<K, V>;
11
+ /** Map each value to a new array. */
12
+ map<T>(fn: (value: V, key: K) => T): T[];
13
+ /** True if any value matches. */
14
+ some(fn: (value: V, key: K) => boolean): boolean;
15
+ /** True if every value matches. */
16
+ every(fn: (value: V, key: K) => boolean): boolean;
17
+ /** Reduce over the values. */
18
+ reduce<T>(fn: (acc: T, value: V, key: K) => T, initial: T): T;
19
+ /** All values as an array. */
20
+ toArray(): V[];
21
+ /** A random value. */
22
+ random(): V | undefined;
23
+ }
@@ -0,0 +1,65 @@
1
+ // A Map with array-style helpers — district.js's workhorse return type, modelled
2
+ // on discord.js's Collection.
3
+ export class Collection extends Map {
4
+ first(n) {
5
+ if (n === undefined)
6
+ return this.values().next().value;
7
+ return [...this.values()].slice(0, n);
8
+ }
9
+ /** The last value. */
10
+ last() {
11
+ return [...this.values()].at(-1);
12
+ }
13
+ /** The first value matching a predicate. */
14
+ find(fn) {
15
+ for (const [k, v] of this)
16
+ if (fn(v, k))
17
+ return v;
18
+ return undefined;
19
+ }
20
+ /** A new Collection of the values matching a predicate. */
21
+ filter(fn) {
22
+ const out = new Collection();
23
+ for (const [k, v] of this)
24
+ if (fn(v, k))
25
+ out.set(k, v);
26
+ return out;
27
+ }
28
+ /** Map each value to a new array. */
29
+ map(fn) {
30
+ const out = [];
31
+ for (const [k, v] of this)
32
+ out.push(fn(v, k));
33
+ return out;
34
+ }
35
+ /** True if any value matches. */
36
+ some(fn) {
37
+ for (const [k, v] of this)
38
+ if (fn(v, k))
39
+ return true;
40
+ return false;
41
+ }
42
+ /** True if every value matches. */
43
+ every(fn) {
44
+ for (const [k, v] of this)
45
+ if (!fn(v, k))
46
+ return false;
47
+ return true;
48
+ }
49
+ /** Reduce over the values. */
50
+ reduce(fn, initial) {
51
+ let acc = initial;
52
+ for (const [k, v] of this)
53
+ acc = fn(acc, v, k);
54
+ return acc;
55
+ }
56
+ /** All values as an array. */
57
+ toArray() {
58
+ return [...this.values()];
59
+ }
60
+ /** A random value. */
61
+ random() {
62
+ const arr = this.toArray();
63
+ return arr[Math.floor(Math.random() * arr.length)];
64
+ }
65
+ }
@@ -0,0 +1,9 @@
1
+ export declare const Formatters: {
2
+ readonly bold: (text: string) => string;
3
+ readonly italic: (text: string) => string;
4
+ readonly strikethrough: (text: string) => string;
5
+ readonly code: (text: string) => string;
6
+ readonly codeBlock: (text: string, lang?: string) => string;
7
+ readonly blockQuote: (text: string) => string;
8
+ readonly link: (label: string, url: string) => string;
9
+ };
@@ -0,0 +1,11 @@
1
+ // Markdown helpers for message content. District renders standard markdown, so
2
+ // these produce text that renders as formatted in the app.
3
+ export const Formatters = {
4
+ bold: (text) => `**${text}**`,
5
+ italic: (text) => `*${text}*`,
6
+ strikethrough: (text) => `~~${text}~~`,
7
+ code: (text) => `\`${text}\``,
8
+ codeBlock: (text, lang = "") => `\`\`\`${lang}\n${text}\n\`\`\``,
9
+ blockQuote: (text) => text.split("\n").map((l) => `> ${l}`).join("\n"),
10
+ link: (label, url) => `[${label}](${url})`,
11
+ };
package/dist/index.d.ts CHANGED
@@ -1,5 +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, type ClientContext } from "./structures.js";
3
+ export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, type ClientContext, } from "./structures.js";
4
+ export { Collection } from "./collection.js";
5
+ export { PermissionsBitField, type PermissionResolvable, type PermissionFlagName } from "./permissions.js";
6
+ export { Formatters } from "./formatters.js";
4
7
  export { verifySignature, parseEvent, verifyAndParse, type EventHeaders } from "./webhook.js";
5
- export type { ClientOptions, RawMessage, RawSelfUser, DistrictEvent, ReactionEvent, DeletedMessage, } from "./types.js";
8
+ export type { ClientOptions, RawMessage, RawSelfUser, RawUser, RawSpace, RawChannel, RawRole, RawMember, DistrictEvent, ReactionEvent, DeletedMessage, } from "./types.js";
package/dist/index.js CHANGED
@@ -6,5 +6,8 @@
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 } from "./structures.js";
9
+ export { ClientUser, Message, TextChannel, Interaction, User, Space, SpaceMember, Role, } from "./structures.js";
10
+ export { Collection } from "./collection.js";
11
+ export { PermissionsBitField } from "./permissions.js";
12
+ export { Formatters } from "./formatters.js";
10
13
  export { verifySignature, parseEvent, verifyAndParse } from "./webhook.js";
@@ -0,0 +1,71 @@
1
+ export type PermissionResolvable = bigint | number | string | PermissionFlagName | Array<bigint | number | string>;
2
+ declare const FLAGS: {
3
+ readonly ViewChannel: bigint;
4
+ readonly SendMessages: bigint;
5
+ readonly ManageMessages: bigint;
6
+ readonly ManageChannels: bigint;
7
+ readonly ManageRoles: bigint;
8
+ readonly ManageSpace: bigint;
9
+ readonly KickMembers: bigint;
10
+ readonly BanMembers: bigint;
11
+ readonly ManageNicknames: bigint;
12
+ readonly MentionEveryone: bigint;
13
+ readonly Connect: bigint;
14
+ readonly Speak: bigint;
15
+ readonly MuteMembers: bigint;
16
+ readonly MoveMembers: bigint;
17
+ readonly PrioritySpeaker: bigint;
18
+ readonly AddReactions: bigint;
19
+ readonly EmbedLinks: bigint;
20
+ readonly AttachFiles: bigint;
21
+ readonly PinMessages: bigint;
22
+ readonly CreateInvites: bigint;
23
+ readonly Administrator: bigint;
24
+ readonly ManageThreads: bigint;
25
+ readonly ManageWebhooks: bigint;
26
+ };
27
+ export type PermissionFlagName = keyof typeof FLAGS;
28
+ export declare class PermissionsBitField {
29
+ /** All permission flags, by name. */
30
+ static readonly Flags: {
31
+ readonly ViewChannel: bigint;
32
+ readonly SendMessages: bigint;
33
+ readonly ManageMessages: bigint;
34
+ readonly ManageChannels: bigint;
35
+ readonly ManageRoles: bigint;
36
+ readonly ManageSpace: bigint;
37
+ readonly KickMembers: bigint;
38
+ readonly BanMembers: bigint;
39
+ readonly ManageNicknames: bigint;
40
+ readonly MentionEveryone: bigint;
41
+ readonly Connect: bigint;
42
+ readonly Speak: bigint;
43
+ readonly MuteMembers: bigint;
44
+ readonly MoveMembers: bigint;
45
+ readonly PrioritySpeaker: bigint;
46
+ readonly AddReactions: bigint;
47
+ readonly EmbedLinks: bigint;
48
+ readonly AttachFiles: bigint;
49
+ readonly PinMessages: bigint;
50
+ readonly CreateInvites: bigint;
51
+ readonly Administrator: bigint;
52
+ readonly ManageThreads: bigint;
53
+ readonly ManageWebhooks: bigint;
54
+ };
55
+ bitfield: bigint;
56
+ constructor(permissions?: PermissionResolvable);
57
+ /** Resolve any permission input to a single bigint. */
58
+ static resolve(perm: PermissionResolvable): bigint;
59
+ /** True if the bitfield includes a permission (Administrator grants all). */
60
+ has(perm: PermissionResolvable): boolean;
61
+ /** Add permissions (returns this). */
62
+ add(...perms: PermissionResolvable[]): this;
63
+ /** Remove permissions (returns this). */
64
+ remove(...perms: PermissionResolvable[]): this;
65
+ /** The flag names contained in this bitfield. */
66
+ toArray(): PermissionFlagName[];
67
+ /** The integer value, as a string (safe for large bitfields). */
68
+ toString(): string;
69
+ valueOf(): bigint;
70
+ }
71
+ export {};
@@ -0,0 +1,80 @@
1
+ // A wrapper over District's permission bitfield (mirrors permission_flags), for
2
+ // building and checking the permissions integer used by install links and roles.
3
+ const FLAGS = {
4
+ ViewChannel: 1n << 0n,
5
+ SendMessages: 1n << 1n,
6
+ ManageMessages: 1n << 2n,
7
+ ManageChannels: 1n << 3n,
8
+ ManageRoles: 1n << 4n,
9
+ ManageSpace: 1n << 5n,
10
+ KickMembers: 1n << 6n,
11
+ BanMembers: 1n << 7n,
12
+ ManageNicknames: 1n << 8n,
13
+ MentionEveryone: 1n << 9n,
14
+ Connect: 1n << 10n,
15
+ Speak: 1n << 11n,
16
+ MuteMembers: 1n << 12n,
17
+ MoveMembers: 1n << 13n,
18
+ PrioritySpeaker: 1n << 14n,
19
+ AddReactions: 1n << 15n,
20
+ EmbedLinks: 1n << 16n,
21
+ AttachFiles: 1n << 17n,
22
+ PinMessages: 1n << 18n,
23
+ CreateInvites: 1n << 19n,
24
+ Administrator: 1n << 20n,
25
+ ManageThreads: 1n << 21n,
26
+ ManageWebhooks: 1n << 22n,
27
+ };
28
+ export class PermissionsBitField {
29
+ /** All permission flags, by name. */
30
+ static Flags = FLAGS;
31
+ bitfield;
32
+ constructor(permissions = 0n) {
33
+ this.bitfield = PermissionsBitField.resolve(permissions);
34
+ }
35
+ /** Resolve any permission input to a single bigint. */
36
+ static resolve(perm) {
37
+ if (typeof perm === "bigint")
38
+ return perm;
39
+ if (typeof perm === "number")
40
+ return BigInt(perm);
41
+ if (typeof perm === "string")
42
+ return perm in FLAGS ? FLAGS[perm] : BigInt(perm);
43
+ if (Array.isArray(perm))
44
+ return perm.reduce((acc, p) => acc | PermissionsBitField.resolve(p), 0n);
45
+ return 0n;
46
+ }
47
+ /** True if the bitfield includes a permission (Administrator grants all). */
48
+ has(perm) {
49
+ if ((this.bitfield & FLAGS.Administrator) === FLAGS.Administrator && perm !== "Administrator")
50
+ return true;
51
+ const bit = PermissionsBitField.resolve(perm);
52
+ return (this.bitfield & bit) === bit;
53
+ }
54
+ /** Add permissions (returns this). */
55
+ add(...perms) {
56
+ for (const p of perms)
57
+ this.bitfield |= PermissionsBitField.resolve(p);
58
+ return this;
59
+ }
60
+ /** Remove permissions (returns this). */
61
+ remove(...perms) {
62
+ for (const p of perms)
63
+ this.bitfield &= ~PermissionsBitField.resolve(p);
64
+ return this;
65
+ }
66
+ /** The flag names contained in this bitfield. */
67
+ toArray() {
68
+ return Object.keys(FLAGS).filter((name) => {
69
+ const bit = FLAGS[name];
70
+ return (this.bitfield & bit) === bit;
71
+ });
72
+ }
73
+ /** The integer value, as a string (safe for large bitfields). */
74
+ toString() {
75
+ return this.bitfield.toString();
76
+ }
77
+ valueOf() {
78
+ return this.bitfield;
79
+ }
80
+ }
@@ -1,5 +1,7 @@
1
1
  import type { REST } from "./rest.js";
2
- import type { RawMessage, RawSelfUser } from "./types.js";
2
+ import type { RawMessage, RawSelfUser, RawUser, RawSpace, RawChannel, RawRole, RawMember } from "./types.js";
3
+ import { Collection } from "./collection.js";
4
+ import { PermissionsBitField } from "./permissions.js";
3
5
  /** Minimal client surface the structures need (avoids a circular import). */
4
6
  export interface ClientContext {
5
7
  rest: REST;
@@ -36,15 +38,82 @@ export declare class Message {
36
38
  react(emoji: string): Promise<void>;
37
39
  /** Remove the bot's reaction. */
38
40
  removeReaction(emoji: string): Promise<void>;
41
+ /** Fetch the author's full public profile. */
42
+ fetchAuthor(): Promise<User>;
43
+ /** A handle to the channel this message is in. */
44
+ get channel(): TextChannel;
39
45
  }
40
46
  export declare class TextChannel {
41
47
  private client;
42
48
  id: string;
49
+ /** Channel kind (text, voice, announcement, …) — populated after fetch(). */
50
+ kind: string | null;
51
+ name: string | null;
52
+ spaceId: string | null;
53
+ topic: string | null;
54
+ position: number | null;
43
55
  constructor(client: ClientContext, id: string);
56
+ /** Load this channel's info (kind, name, space). Returns itself. */
57
+ fetch(): Promise<this>;
58
+ /** @internal populate info fields from a raw channel row. */
59
+ applyInfo(raw: RawChannel): this;
44
60
  /** Send a message to this channel. */
45
61
  send(content: string): Promise<Message>;
46
62
  /** Fetch recent messages (oldest → newest), up to 100. */
47
- fetchMessages(limit?: number): Promise<Message[]>;
63
+ fetchMessages(limit?: number): Promise<Collection<string, Message>>;
64
+ }
65
+ /** A District user's public profile. */
66
+ export declare class User {
67
+ id: string;
68
+ handle: string;
69
+ displayName: string;
70
+ avatarUrl: string | null;
71
+ bannerUrl: string | null;
72
+ status: string | null;
73
+ bot: boolean;
74
+ constructor(raw: RawUser);
75
+ get username(): string;
76
+ }
77
+ /** A space (District's "server"/guild). Fetch related resources from it. */
78
+ export declare class Space {
79
+ private client;
80
+ id: string;
81
+ short: string;
82
+ name: string;
83
+ tagline: string | null;
84
+ description: string | null;
85
+ ownerId: string;
86
+ boostLevel: number;
87
+ memberCount: number | null;
88
+ constructor(client: ClientContext, raw: RawSpace);
89
+ /** The space's channels (must have the app installed). */
90
+ channels(): Promise<Collection<string, TextChannel>>;
91
+ /** The space's members. */
92
+ members(limit?: number): Promise<Collection<string, SpaceMember>>;
93
+ /** The space's roles. */
94
+ roles(): Promise<Collection<string, Role>>;
95
+ /** The space owner's profile. */
96
+ fetchOwner(): Promise<User>;
97
+ }
98
+ /** A user's membership in a space. */
99
+ export declare class SpaceMember {
100
+ private client;
101
+ spaceId: string;
102
+ userId: string;
103
+ nickname: string | null;
104
+ constructor(client: ClientContext, spaceId: string, raw: RawMember);
105
+ /** Fetch this member's full profile. */
106
+ user(): Promise<User>;
107
+ }
108
+ /** A space role, with a parsed permissions bitfield. */
109
+ export declare class Role {
110
+ id: string;
111
+ name: string;
112
+ color: string;
113
+ position: number;
114
+ hoist: boolean;
115
+ permissions: PermissionsBitField;
116
+ constructor(raw: RawRole);
48
117
  }
49
118
  export declare class Interaction {
50
119
  private client;
@@ -1,5 +1,7 @@
1
1
  // Rich objects returned by the client — each carries a back-reference so you can
2
2
  // act on it directly: message.reply(), channel.send(), interaction.reply().
3
+ import { Collection } from "./collection.js";
4
+ import { PermissionsBitField } from "./permissions.js";
3
5
  export class ClientUser {
4
6
  id;
5
7
  handle;
@@ -58,14 +60,43 @@ export class Message {
58
60
  async removeReaction(emoji) {
59
61
  await this.client.rest.delete(`/channels/${this.channelId}/messages/${this.id}/reactions/${encodeURIComponent(emoji)}`);
60
62
  }
63
+ /** Fetch the author's full public profile. */
64
+ async fetchAuthor() {
65
+ const { user } = await this.client.rest.get(`/users/${this.authorId}`);
66
+ return new User(user);
67
+ }
68
+ /** A handle to the channel this message is in. */
69
+ get channel() {
70
+ return new TextChannel(this.client, this.channelId);
71
+ }
61
72
  }
62
73
  export class TextChannel {
63
74
  client;
64
75
  id;
76
+ /** Channel kind (text, voice, announcement, …) — populated after fetch(). */
77
+ kind = null;
78
+ name = null;
79
+ spaceId = null;
80
+ topic = null;
81
+ position = null;
65
82
  constructor(client, id) {
66
83
  this.client = client;
67
84
  this.id = id;
68
85
  }
86
+ /** Load this channel's info (kind, name, space). Returns itself. */
87
+ async fetch() {
88
+ const { channel } = await this.client.rest.get(`/channels/${this.id}`);
89
+ return this.applyInfo(channel);
90
+ }
91
+ /** @internal populate info fields from a raw channel row. */
92
+ applyInfo(raw) {
93
+ this.kind = raw.kind ?? null;
94
+ this.name = raw.name ?? null;
95
+ this.spaceId = raw.space_id ?? null;
96
+ this.topic = raw.topic ?? null;
97
+ this.position = raw.position ?? null;
98
+ return this;
99
+ }
69
100
  /** Send a message to this channel. */
70
101
  async send(content) {
71
102
  const { message } = await this.client.rest.post(`/channels/${this.id}/messages`, { content });
@@ -74,7 +105,117 @@ export class TextChannel {
74
105
  /** Fetch recent messages (oldest → newest), up to 100. */
75
106
  async fetchMessages(limit = 50) {
76
107
  const { messages } = await this.client.rest.get(`/channels/${this.id}/messages?limit=${limit}`);
77
- return (messages ?? []).map((m) => new Message(this.client, m));
108
+ const out = new Collection();
109
+ for (const m of (messages ?? []))
110
+ out.set(m.id, new Message(this.client, m));
111
+ return out;
112
+ }
113
+ }
114
+ /** A District user's public profile. */
115
+ export class User {
116
+ id;
117
+ handle;
118
+ displayName;
119
+ avatarUrl;
120
+ bannerUrl;
121
+ status;
122
+ bot;
123
+ constructor(raw) {
124
+ this.id = raw.id;
125
+ this.handle = raw.handle;
126
+ this.displayName = raw.display_name;
127
+ this.avatarUrl = raw.avatar_url;
128
+ this.bannerUrl = raw.banner_url ?? null;
129
+ this.status = raw.status ?? null;
130
+ this.bot = raw.is_bot;
131
+ }
132
+ get username() { return this.handle; }
133
+ }
134
+ /** A space (District's "server"/guild). Fetch related resources from it. */
135
+ export class Space {
136
+ client;
137
+ id;
138
+ short;
139
+ name;
140
+ tagline;
141
+ description;
142
+ ownerId;
143
+ boostLevel;
144
+ memberCount;
145
+ constructor(client, raw) {
146
+ this.client = client;
147
+ this.id = raw.id;
148
+ this.short = raw.short;
149
+ this.name = raw.name;
150
+ this.tagline = raw.tagline;
151
+ this.description = raw.description;
152
+ this.ownerId = raw.owner_id;
153
+ this.boostLevel = raw.boost_level;
154
+ this.memberCount = raw.member_count ?? null;
155
+ }
156
+ /** The space's channels (must have the app installed). */
157
+ async channels() {
158
+ const { channels } = await this.client.rest.get(`/spaces/${this.id}/channels`);
159
+ const out = new Collection();
160
+ for (const c of (channels ?? []))
161
+ out.set(c.id, new TextChannel(this.client, c.id).applyInfo(c));
162
+ return out;
163
+ }
164
+ /** The space's members. */
165
+ async members(limit = 100) {
166
+ const { members } = await this.client.rest.get(`/spaces/${this.id}/members?limit=${limit}`);
167
+ const out = new Collection();
168
+ for (const m of (members ?? []))
169
+ out.set(m.user_id, new SpaceMember(this.client, this.id, m));
170
+ return out;
171
+ }
172
+ /** The space's roles. */
173
+ async roles() {
174
+ const { roles } = await this.client.rest.get(`/spaces/${this.id}/roles`);
175
+ const out = new Collection();
176
+ for (const r of (roles ?? []))
177
+ out.set(r.id, new Role(r));
178
+ return out;
179
+ }
180
+ /** The space owner's profile. */
181
+ async fetchOwner() {
182
+ const { user } = await this.client.rest.get(`/users/${this.ownerId}`);
183
+ return new User(user);
184
+ }
185
+ }
186
+ /** A user's membership in a space. */
187
+ export class SpaceMember {
188
+ client;
189
+ spaceId;
190
+ userId;
191
+ nickname;
192
+ constructor(client, spaceId, raw) {
193
+ this.client = client;
194
+ this.spaceId = spaceId;
195
+ this.userId = raw.user_id;
196
+ this.nickname = raw.nickname;
197
+ }
198
+ /** Fetch this member's full profile. */
199
+ async user() {
200
+ const { user } = await this.client.rest.get(`/users/${this.userId}`);
201
+ return new User(user);
202
+ }
203
+ }
204
+ /** A space role, with a parsed permissions bitfield. */
205
+ export class Role {
206
+ id;
207
+ name;
208
+ color;
209
+ position;
210
+ hoist;
211
+ permissions;
212
+ constructor(raw) {
213
+ this.id = raw.id;
214
+ this.name = raw.name;
215
+ this.color = raw.color;
216
+ this.position = raw.position;
217
+ this.hoist = raw.hoist;
218
+ this.permissions = new PermissionsBitField(raw.permissions);
78
219
  }
79
220
  }
80
221
  export class Interaction {
package/dist/types.d.ts CHANGED
@@ -28,6 +28,50 @@ export interface RawSelfUser {
28
28
  avatar_url: string | null;
29
29
  is_bot: boolean;
30
30
  }
31
+ /** A public user profile (`GET /users/:id`). */
32
+ export interface RawUser {
33
+ id: string;
34
+ handle: string;
35
+ display_name: string;
36
+ avatar_url: string | null;
37
+ banner_url?: string | null;
38
+ status?: string | null;
39
+ is_bot: boolean;
40
+ }
41
+ /** A space (`GET /spaces/:id`). */
42
+ export interface RawSpace {
43
+ id: string;
44
+ short: string;
45
+ name: string;
46
+ tagline: string | null;
47
+ description: string | null;
48
+ owner_id: string;
49
+ boost_level: number;
50
+ member_count?: number;
51
+ }
52
+ /** A channel (`GET /channels/:id`, `GET /spaces/:id/channels`). */
53
+ export interface RawChannel {
54
+ id: string;
55
+ kind: string;
56
+ space_id: string | null;
57
+ name: string | null;
58
+ topic?: string | null;
59
+ position?: number;
60
+ }
61
+ /** A space role (`GET /spaces/:id/roles`). */
62
+ export interface RawRole {
63
+ id: string;
64
+ name: string;
65
+ color: string;
66
+ permissions: string;
67
+ position: number;
68
+ hoist: boolean;
69
+ }
70
+ /** A space membership (`GET /spaces/:id/members`). */
71
+ export interface RawMember {
72
+ user_id: string;
73
+ nickname: string | null;
74
+ }
31
75
  export type DistrictEvent = {
32
76
  event: "message.create";
33
77
  space_id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "district.js",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -11,13 +11,23 @@
11
11
  "import": "./dist/index.js"
12
12
  }
13
13
  },
14
- "files": ["dist"],
15
- "engines": { "node": ">=18" },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
16
20
  "scripts": {
17
21
  "build": "tsc -p tsconfig.json",
18
22
  "typecheck": "tsc -p tsconfig.json --noEmit"
19
23
  },
20
- "keywords": ["district", "bot", "api", "chat", "sdk"],
24
+ "keywords": [
25
+ "district",
26
+ "bot",
27
+ "api",
28
+ "chat",
29
+ "sdk"
30
+ ],
21
31
  "license": "MIT",
22
32
  "devDependencies": {
23
33
  "@types/node": "^20.14.0",