lunibee 0.2.3 → 0.2.4

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.
Files changed (61) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +225 -225
  3. package/dist/builders/attachment.d.ts +8 -0
  4. package/dist/builders/commands.d.ts +122 -65
  5. package/dist/builders/components.d.ts +10 -31
  6. package/dist/builders/embed.d.ts +66 -33
  7. package/dist/builders/index.js +97 -131
  8. package/dist/builders/index.js.map +8 -8
  9. package/dist/collection/cache.d.ts +13 -6
  10. package/dist/collection/index.d.ts +90 -17
  11. package/dist/collection/index.js +442 -266
  12. package/dist/collection/index.js.map +4 -4
  13. package/dist/core/index.d.ts +8 -0
  14. package/dist/core/index.js +607 -352
  15. package/dist/core/index.js.map +47 -47
  16. package/dist/core/permissions.d.ts +7 -3
  17. package/dist/formatters/index.js +9 -5
  18. package/dist/formatters/index.js.map +3 -3
  19. package/dist/handlers/index.js +5 -10
  20. package/dist/handlers/index.js.map +3 -3
  21. package/dist/index.js +769 -587
  22. package/dist/index.js.map +65 -65
  23. package/dist/managers/base.d.ts +22 -14
  24. package/dist/managers/index.d.ts +2 -1
  25. package/dist/managers/index.js +445 -276
  26. package/dist/managers/index.js.map +26 -26
  27. package/dist/managers/message.d.ts +7 -3
  28. package/dist/rest/errors.d.ts +14 -7
  29. package/dist/rest/index.d.ts +24 -12
  30. package/dist/rest/index.js +54 -17
  31. package/dist/rest/index.js.map +14 -14
  32. package/dist/rest/redis.d.ts +1 -1
  33. package/dist/rest/route.d.ts +8 -4
  34. package/dist/rest/routes.d.ts +102 -48
  35. package/dist/rest/transport.d.ts +10 -5
  36. package/dist/rest/webhook.d.ts +9 -5
  37. package/dist/sharding/bus.d.ts +34 -17
  38. package/dist/sharding/index.d.ts +34 -17
  39. package/dist/sharding/index.js +64 -32
  40. package/dist/sharding/index.js.map +17 -17
  41. package/dist/structures/base.d.ts +19 -10
  42. package/dist/structures/channels.d.ts +28 -14
  43. package/dist/structures/index.js +9 -59
  44. package/dist/structures/index.js.map +19 -18
  45. package/dist/structures/interactions.d.ts +96 -48
  46. package/dist/structures/resources.d.ts +88 -44
  47. package/dist/types/gateway.d.ts +1 -1
  48. package/dist/types/index.js +20 -22
  49. package/dist/types/index.js.map +4 -4
  50. package/dist/utils/index.js.map +2 -2
  51. package/dist/voice/index.d.ts +12 -4
  52. package/dist/voice/index.js +14 -18
  53. package/dist/voice/index.js.map +3 -3
  54. package/dist/ws/decoder.d.ts +6 -2
  55. package/dist/ws/index.d.ts +25 -13
  56. package/dist/ws/index.js +63 -31
  57. package/dist/ws/index.js.map +14 -14
  58. package/dist/ws/protocol.d.ts +10 -2
  59. package/dist/ws/session.d.ts +14 -4
  60. package/dist/ws/state.d.ts +22 -11
  61. package/package.json +15 -14
@@ -7,7 +7,8 @@ export interface MessageCacheOptions {
7
7
  /** Sliding TTL in ms. Omit for no expiry. */
8
8
  ttl?: number;
9
9
  }
10
- import { REST } from "@lunibee/rest";
10
+ import { REST, type RESTFileAttachment } from "@lunibee/rest";
11
+ import type { AllowedMentions } from "@lunibee/types";
11
12
  import { Message, type ResourceContext } from "@lunibee/structures";
12
13
  /** A file to upload with a message. */
13
14
  export interface MessageFile {
@@ -26,12 +27,15 @@ export type MessageEditOptions = MessageCreateOptions;
26
27
  * data as UTF-8. `allowedMentions` is the client's default, applied only when
27
28
  * the payload has no `allowed_mentions` of its own.
28
29
  */
29
- export declare function toRequest(options: MessageCreateOptions, allowedMentions?: unknown): unknown;
30
+ export declare function toRequest(options: MessageCreateOptions, allowedMentions?: AllowedMentions): Omit<MessageCreateOptions, "files"> | {
31
+ body: Omit<MessageCreateOptions, "files">;
32
+ files: RESTFileAttachment[];
33
+ };
30
34
  export declare class MessageManager {
31
35
  #private;
32
36
  /** Cached messages; always empty unless a message cache is configured. */
33
37
  readonly cache: Collection<string, Message>;
34
- constructor(rest: REST, context: ResourceContext, channelId: string, cache?: MessageCacheOptions, allowedMentions?: unknown);
38
+ constructor(rest: REST, context: ResourceContext, channelId: string, cache?: MessageCacheOptions, allowedMentions?: AllowedMentions);
35
39
  resolve(messageId: string): Promise<Message>;
36
40
  send(options: MessageCreateOptions): Promise<Message>;
37
41
  fetch(messageId: string): Promise<Message>;
@@ -28,13 +28,20 @@ export type RESTErrorKind =
28
28
  export declare function redactPath(path: string): string;
29
29
  /** Error thrown when Discord rejects a REST request. */
30
30
  export declare class RESTError extends Error {
31
- /** Failure category. */ readonly kind: RESTErrorKind;
32
- /** HTTP status returned by Discord. */ readonly status: number;
33
- /** Discord API error code, when provided. */ readonly code?: number;
34
- /** Raw Discord validation/error payload. */ readonly errors?: unknown;
35
- /** HTTP method used for the failed request. */ readonly method?: string;
36
- /** API path used for the failed request. */ readonly path?: string;
37
- /** What to check next, for errors with a common cause (missing access or permissions). */ readonly hint?: string;
31
+ /** Failure category. */
32
+ readonly kind: RESTErrorKind;
33
+ /** HTTP status returned by Discord. */
34
+ readonly status: number;
35
+ /** Discord API error code, when provided. */
36
+ readonly code?: number;
37
+ /** Raw Discord validation/error payload. */
38
+ readonly errors?: unknown;
39
+ /** HTTP method used for the failed request. */
40
+ readonly method?: string;
41
+ /** API path used for the failed request. */
42
+ readonly path?: string;
43
+ /** What to check next, for errors with a common cause (missing access or permissions). */
44
+ readonly hint?: string;
38
45
  /** Creates a REST error with request context. @param message Error message. @param status HTTP status. @param code Discord error code. @param errors Raw error payload. @param options Request context and cause. */
39
46
  constructor(message: string, status: number, code?: number, errors?: unknown, options?: {
40
47
  method?: string;
@@ -16,11 +16,16 @@ export interface RESTFileAttachment {
16
16
  * `reason`, `auth`) additively without changing that positional default.
17
17
  */
18
18
  export interface RESTRequestOptions {
19
- /** Abort signal used to cancel the request and any queued wait. */ signal?: AbortSignal;
20
- /** Query-string appended to the path (`URLSearchParams`, a record, or a raw string). */ query?: RESTQuery;
21
- /** Extra request headers merged in (library headers such as `Authorization` win). */ headers?: Record<string, string>;
22
- /** Audit-log reason, sent as the `X-Audit-Log-Reason` header. */ reason?: string;
23
- /** Set `false` to omit the `Authorization` header for this request. Defaults to `true`. */ auth?: boolean;
19
+ /** Abort signal used to cancel the request and any queued wait. */
20
+ signal?: AbortSignal;
21
+ /** Query-string appended to the path (`URLSearchParams`, a record, or a raw string). */
22
+ query?: RESTQuery;
23
+ /** Extra request headers merged in (library headers such as `Authorization` win). */
24
+ headers?: Record<string, string>;
25
+ /** Audit-log reason, sent as the `X-Audit-Log-Reason` header. */
26
+ reason?: string;
27
+ /** Set `false` to omit the `Authorization` header for this request. Defaults to `true`. */
28
+ auth?: boolean;
24
29
  }
25
30
  /**
26
31
  * Discord.js-familiar request payload. Superset of {@link RESTRequestOptions} that also
@@ -28,8 +33,10 @@ export interface RESTRequestOptions {
28
33
  * additive `method(path, { body })` overloads without breaking the positional signature.
29
34
  */
30
35
  export interface RequestData extends RESTRequestOptions {
31
- /** Request body. Encoded as JSON unless `files` are present (then multipart). */ body?: unknown;
32
- /** File attachments; presence upgrades the request to `multipart/form-data`. */ files?: RESTFileAttachment[];
36
+ /** Request body. Encoded as JSON unless `files` are present (then multipart). */
37
+ body?: unknown;
38
+ /** File attachments; presence upgrades the request to `multipart/form-data`. */
39
+ files?: RESTFileAttachment[];
33
40
  }
34
41
  /** Context passed to `RESTHooks.onRequest`. */
35
42
  export interface RESTRequestContext {
@@ -81,9 +88,12 @@ export interface RESTHooks {
81
88
  }
82
89
  /** Configures which REST failures may be retried. */
83
90
  export interface RetryPolicy {
84
- /** Maximum retry attempts. */ maxRetries: number;
85
- /** Determines whether a request may be retried. @param method HTTP method. @param status HTTP status, or zero for transport errors. @returns True when retrying is safe under the policy. */ shouldRetry(method: string, status: number): boolean;
86
- /** Calculates delay before the next attempt. @param attempt Zero-based retry number. @param retryAfter Server-provided delay, when available. @returns Delay in milliseconds. */ getDelay(attempt: number, retryAfter?: number): number;
91
+ /** Maximum retry attempts. */
92
+ maxRetries: number;
93
+ /** Determines whether a request may be retried. @param method HTTP method. @param status HTTP status, or zero for transport errors. @returns True when retrying is safe under the policy. */
94
+ shouldRetry(method: string, status: number): boolean;
95
+ /** Calculates delay before the next attempt. @param attempt Zero-based retry number. @param retryAfter Server-provided delay, when available. @returns Delay in milliseconds. */
96
+ getDelay(attempt: number, retryAfter?: number): number;
87
97
  }
88
98
  /** Creates the default conservative REST retry policy. @param maxRetries Maximum retries. @returns Retry policy that retries rate limits and idempotent transient failures. @throws {TypeError} If maxRetries is negative or not finite. */
89
99
  export declare function createRetryPolicy(maxRetries?: number): RetryPolicy;
@@ -126,7 +136,8 @@ export declare class REST {
126
136
  * @throws {RESTError} If Discord or transport rejects the request.
127
137
  */
128
138
  request<T>(method: string, path: string, body?: unknown, options?: RequestData): Promise<T>;
129
- /** Sends a GET request. @param path API path. @param options Request options (`query`, `headers`, `signal`, …). @returns Decoded response. @throws {RESTError} When the request fails. */ get<T>(path: string, options?: RequestData): Promise<T>;
139
+ /** Sends a GET request. @param path API path. @param options Request options (`query`, `headers`, `signal`, …). @returns Decoded response. @throws {RESTError} When the request fails. */
140
+ get<T>(path: string, options?: RequestData): Promise<T>;
130
141
  /** Sends a POST request. Accepts a raw body (`post(path, body)`) or a Discord.js-familiar `post(path, { body })`. @param path API path. @param body Optional JSON body, or a `RequestData` wrapper. @param options Request options. @returns Decoded response. @throws {RESTError} When the request fails. */
131
142
  post<T>(path: string, options: RequestData): Promise<T>;
132
143
  post<T>(path: string, body?: unknown, options?: RequestData): Promise<T>;
@@ -136,7 +147,8 @@ export declare class REST {
136
147
  /** Sends a PUT request. Accepts a raw body (`put(path, body)`) or a Discord.js-familiar `put(path, { body })`. @param path API path. @param body Optional JSON body, or a `RequestData` wrapper. @param options Request options. @returns Decoded response. @throws {RESTError} When the request fails. */
137
148
  put<T>(path: string, options: RequestData): Promise<T>;
138
149
  put<T>(path: string, body?: unknown, options?: RequestData): Promise<T>;
139
- /** Sends a DELETE request. @param path API path. @param options Request options (`reason`, `headers`, `signal`, …). @returns Decoded response. @throws {RESTError} When the request fails. */ delete<T>(path: string, options?: RequestData): Promise<T>;
150
+ /** Sends a DELETE request. @param path API path. @param options Request options (`reason`, `headers`, `signal`, …). @returns Decoded response. @throws {RESTError} When the request fails. */
151
+ delete<T>(path: string, options?: RequestData): Promise<T>;
140
152
  /**
141
153
  * Sends a multipart/form-data POST request for file uploads.
142
154
  * Attaches a JSON payload as `payload_json` and files as additional fields.
@@ -2,7 +2,7 @@
2
2
  // packages/rest/package.json
3
3
  var package_default = {
4
4
  name: "@lunibee/rest",
5
- version: "0.2.3",
5
+ version: "0.2.4",
6
6
  type: "module",
7
7
  module: "./src/index.ts",
8
8
  types: "./src/index.ts",
@@ -83,7 +83,7 @@ function kindFromStatus(status) {
83
83
  return "client";
84
84
  }
85
85
  function redactPath(path) {
86
- return path.replace(/^(\/webhooks\/[^/?]+\/)[^/?]+/, "$1:token").replace(/^(\/interactions\/[^/?]+\/)[^/?]+/, "$1:token");
86
+ return path.replace(/^(\/webhooks\/[^/?]+\/)[^/?]+/i, "$1:token").replace(/^(\/interactions\/[^/?]+\/)[^/?]+/i, "$1:token");
87
87
  }
88
88
  var HINTS = {
89
89
  50001: "The bot cannot see this resource: check that it is in the guild and has View Channel there.",
@@ -142,19 +142,23 @@ async function abortable(promise, signal, path) {
142
142
  }
143
143
 
144
144
  // packages/rest/src/route.ts
145
+ import { createHash } from "crypto";
145
146
  function normalizeRoutePath(path) {
146
147
  const queryIndex = path.indexOf("?");
147
148
  const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
148
- return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
149
+ return redactPath(routePath).replace(/\/\d+(?=\/|$)/g, "/:id");
149
150
  }
150
151
  function majorParameter(path) {
151
- const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/.exec(path);
152
+ const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/i.exec(path);
152
153
  if (!match)
153
154
  return "@none";
154
- if (match[1] === "webhooks" && match[3] !== undefined)
155
- return `${match[2]}:${match[3]}`;
155
+ if (match[1].toLowerCase() === "webhooks" && match[3] !== undefined)
156
+ return `${match[2]}:${tokenDigest(match[3])}`;
156
157
  return match[2];
157
158
  }
159
+ function tokenDigest(token) {
160
+ return createHash("sha256").update(token).digest("hex").slice(0, 16);
161
+ }
158
162
  function scopeBucket(hashOrRoute, major) {
159
163
  return `${hashOrRoute}|${major}`;
160
164
  }
@@ -396,6 +400,9 @@ var Routes = {
396
400
  user: () => "/users/@me",
397
401
  userById: (userId) => `/users/${snowflake(userId, "User ID")}`,
398
402
  guild: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}`,
403
+ currentUserGuild: (guildId) => `/users/@me/guilds/${snowflake(guildId, "Guild ID")}`,
404
+ sticker: (stickerId) => `/stickers/${snowflake(stickerId, "Sticker ID")}`,
405
+ messageReactionUser: (channelId, messageId, emoji, userId) => `${Routes.messageReactions(channelId, messageId, emoji)}/${snowflake(userId, "User ID")}`,
399
406
  guildPreview: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/preview`,
400
407
  guildActiveThreads: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/threads/active`,
401
408
  guildWebhooks: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/webhooks`,
@@ -657,7 +664,7 @@ class WebhookClient {
657
664
  #rest;
658
665
  constructor(options, restOptions) {
659
666
  if (options.url) {
660
- const match = options.url.match(/discord(?:app)?.com\/api\/webhooks\/(\d+)\/([A-Za-z0-9_-]+)/);
667
+ const match = options.url.match(/^https:\/\/(?:(?:ptb|canary)\.)?discord(?:app)?\.com\/api(?:\/v\d+)?\/webhooks\/(\d{1,20})\/([A-Za-z0-9_-]+)\/?(?:[?#].*)?$/);
661
668
  if (!match)
662
669
  throw new Error("Invalid Discord webhook URL format.");
663
670
  this.id = match[1];
@@ -672,13 +679,20 @@ class WebhookClient {
672
679
  }
673
680
  async send(options) {
674
681
  const payload = typeof options === "string" ? { content: options } : options;
675
- const { thread_id, ...body } = payload;
676
- const query = thread_id ? `?thread_id=${thread_id}&wait=true` : `?wait=true`;
682
+ const { thread_id, files, ...body } = payload;
683
+ const params = new URLSearchParams({ wait: "true" });
684
+ if (thread_id)
685
+ params.set("thread_id", String(thread_id));
686
+ const query = `?${params}`;
677
687
  const formattedEmbeds = body.embeds?.map((e) => ("toJSON" in e) ? e.toJSON() : e);
678
- return this.#rest.post(`${Routes.webhook(this.id, this.token)}${query}`, {
688
+ const formattedComponents = body.components?.map((c) => ("toJSON" in c) ? c.toJSON() : c);
689
+ const path = `${Routes.webhook(this.id, this.token)}${query}`;
690
+ const json = {
679
691
  ...body,
680
- embeds: formattedEmbeds
681
- });
692
+ embeds: formattedEmbeds,
693
+ components: formattedComponents
694
+ };
695
+ return files?.length ? this.#rest.postWithFiles(path, json, files) : this.#rest.post(path, json);
682
696
  }
683
697
  async editMessage(messageId, options) {
684
698
  const payload = typeof options === "string" ? { content: options } : options;
@@ -693,6 +707,22 @@ class WebhookClient {
693
707
  var LIBRARY_VERSION = package_default.version;
694
708
  var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
695
709
  var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${LIBRARY_VERSION})`;
710
+ function assertSafePath(path) {
711
+ const end = path.search(/[?#]/);
712
+ const pathname = end === -1 ? path : path.slice(0, end);
713
+ if (pathname.includes("\\"))
714
+ throw new TypeError("REST paths must not contain backslashes.");
715
+ for (const segment of pathname.split("/")) {
716
+ let decoded;
717
+ try {
718
+ decoded = decodeURIComponent(segment);
719
+ } catch {
720
+ throw new TypeError("REST path has malformed percent-encoding.");
721
+ }
722
+ if (decoded === "." || decoded === "..")
723
+ throw new TypeError("REST paths must not contain '.' or '..' segments.");
724
+ }
725
+ }
696
726
  var REQUEST_DATA_KEYS = new Set([
697
727
  "body",
698
728
  "files",
@@ -777,6 +807,7 @@ class REST {
777
807
  throw new TypeError("REST method is required.");
778
808
  if (!path.startsWith("/"))
779
809
  throw new TypeError("REST paths must start with '/'.");
810
+ assertSafePath(path);
780
811
  const route = createRouteKey(method, path);
781
812
  const files = options.files;
782
813
  if (files && files.length > 0 && !(body instanceof FormData))
@@ -864,13 +895,19 @@ class REST {
864
895
  #headers(body, options) {
865
896
  const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
866
897
  const headers = { ...options.headers };
898
+ const set = (name, value) => {
899
+ for (const key of Object.keys(headers))
900
+ if (key.toLowerCase() === name.toLowerCase())
901
+ delete headers[key];
902
+ headers[name] = value;
903
+ };
867
904
  if (options.auth !== false && this.#token)
868
- headers["Authorization"] = `Bot ${this.#token}`;
869
- headers["User-Agent"] = USER_AGENT;
905
+ set("Authorization", `Bot ${this.#token}`);
906
+ set("User-Agent", USER_AGENT);
870
907
  if (options.reason !== undefined)
871
- headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
908
+ set("X-Audit-Log-Reason", encodeURIComponent(options.reason));
872
909
  if (!isFormData)
873
- headers["Content-Type"] = "application/json";
910
+ set("Content-Type", "application/json");
874
911
  return headers;
875
912
  }
876
913
  #encodeBody(body) {
@@ -949,4 +986,4 @@ export {
949
986
  serializeQuery
950
987
  };
951
988
 
952
- //# debugId=0345D6D5C65D143C64756E2164756E21
989
+ //# debugId=51D994350F46554C64756E2164756E21