lunibee 0.1.6 → 0.1.7

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 (55) hide show
  1. package/README.md +3 -3
  2. package/dist/builders/commands.d.ts +37 -0
  3. package/dist/builders/components.d.ts +19 -12
  4. package/dist/builders/embed.d.ts +3 -1
  5. package/dist/builders/index.d.ts +1 -1
  6. package/dist/builders/index.js +146 -19
  7. package/dist/builders/index.js.map +8 -7
  8. package/dist/collection/index.d.ts +30 -0
  9. package/dist/collection/index.js +78 -5
  10. package/dist/collection/index.js.map +4 -4
  11. package/dist/core/events.d.ts +2 -0
  12. package/dist/core/index.d.ts +38 -0
  13. package/dist/core/index.js +754 -175
  14. package/dist/core/index.js.map +24 -22
  15. package/dist/core/permissions.d.ts +112 -53
  16. package/dist/formatters/index.d.ts +43 -3
  17. package/dist/formatters/index.js +64 -1
  18. package/dist/formatters/index.js.map +3 -3
  19. package/dist/handlers/index.js.map +2 -2
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1762 -291
  22. package/dist/index.js.map +43 -38
  23. package/dist/managers/emoji.d.ts +38 -0
  24. package/dist/managers/guild.d.ts +112 -1
  25. package/dist/managers/index.d.ts +31 -1
  26. package/dist/managers/index.js +803 -77
  27. package/dist/managers/index.js.map +21 -20
  28. package/dist/rest/index.d.ts +101 -11
  29. package/dist/rest/index.js +301 -45
  30. package/dist/rest/index.js.map +8 -6
  31. package/dist/rest/redis.d.ts +37 -0
  32. package/dist/rest/routes.d.ts +22 -0
  33. package/dist/rest/store.d.ts +38 -0
  34. package/dist/rest/webhook.d.ts +2 -2
  35. package/dist/sharding/cluster.d.ts +61 -0
  36. package/dist/sharding/index.d.ts +8 -0
  37. package/dist/sharding/index.js +286 -18
  38. package/dist/sharding/index.js.map +8 -7
  39. package/dist/structures/audit-log.d.ts +26 -0
  40. package/dist/structures/base.d.ts +32 -0
  41. package/dist/structures/index.d.ts +29 -1
  42. package/dist/structures/index.js +1022 -86
  43. package/dist/structures/index.js.map +14 -10
  44. package/dist/structures/resources.d.ts +192 -4
  45. package/dist/types/index.d.ts +76 -0
  46. package/dist/types/index.js.map +2 -2
  47. package/dist/utils/index.js.map +2 -2
  48. package/dist/voice/index.d.ts +88 -0
  49. package/dist/voice/index.js +204 -5
  50. package/dist/voice/index.js.map +3 -3
  51. package/dist/ws/index.d.ts +40 -1
  52. package/dist/ws/index.js +102 -18
  53. package/dist/ws/index.js.map +4 -4
  54. package/package.json +16 -16
  55. package/dist/structures/permissions.d.ts +0 -73
@@ -88,6 +88,22 @@ var GatewayOpcodes = {
88
88
  Hello: 10,
89
89
  HeartbeatAck: 11
90
90
  };
91
+ var GatewayCloseCodes = {
92
+ UnknownError: 4000,
93
+ UnknownOpcode: 4001,
94
+ DecodeError: 4002,
95
+ NotAuthenticated: 4003,
96
+ AuthenticationFailed: 4004,
97
+ AlreadyAuthenticated: 4005,
98
+ InvalidSeq: 4007,
99
+ RateLimited: 4008,
100
+ SessionTimedOut: 4009,
101
+ InvalidShard: 4010,
102
+ ShardingRequired: 4011,
103
+ InvalidAPIVersion: 4012,
104
+ InvalidIntents: 4013,
105
+ DisallowedIntents: 4014
106
+ };
91
107
  class GatewayError extends Error {
92
108
  code;
93
109
  constructor(message, code, options) {
@@ -121,6 +137,8 @@ class Gateway {
121
137
  #rejectConnect;
122
138
  #listeners = new Map;
123
139
  #sendTimestamps = [];
140
+ ping = -1;
141
+ #decompressor;
124
142
  constructor(options) {
125
143
  if (!options.token?.trim())
126
144
  throw new TypeError("A Gateway token is required.");
@@ -141,7 +159,12 @@ class Gateway {
141
159
  browser: "Discord Android",
142
160
  device: "Discord Android"
143
161
  },
144
- presence: { status: "online", activities: [], afk: false, since: null },
162
+ presence: {
163
+ status: "online",
164
+ activities: [],
165
+ afk: false,
166
+ since: null
167
+ },
145
168
  ...options
146
169
  };
147
170
  if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
@@ -156,10 +179,13 @@ class Gateway {
156
179
  return this.#connectPromise;
157
180
  this.#closed = false;
158
181
  this.#setState("CONNECT" /* Connect */);
182
+ const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}&compress=zlib-stream` : url;
183
+ if (this.#options.compress)
184
+ this.#initDecompressor();
159
185
  this.#connectPromise = new Promise((resolve, reject) => {
160
186
  this.#resolveConnect = resolve;
161
187
  this.#rejectConnect = reject;
162
- this.#open(url);
188
+ this.#open(connectURL);
163
189
  });
164
190
  return this.#connectPromise;
165
191
  }
@@ -195,12 +221,15 @@ class Gateway {
195
221
  send(payload) {
196
222
  if (!payload || !Number.isInteger(payload.op))
197
223
  throw new TypeError("Gateway payload must contain an integer opcode.");
224
+ return this.#dispatch(payload, false);
225
+ }
226
+ #dispatch(payload, privileged) {
198
227
  if (this.#ws?.readyState !== WebSocket.OPEN)
199
228
  return false;
200
229
  const now = Date.now();
201
230
  while (this.#sendTimestamps.length && now - this.#sendTimestamps[0] >= 60000)
202
231
  this.#sendTimestamps.shift();
203
- if (this.#sendTimestamps.length >= 115) {
232
+ if (!privileged && this.#sendTimestamps.length >= 115) {
204
233
  this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
205
234
  return false;
206
235
  }
@@ -271,7 +300,11 @@ class Gateway {
271
300
  ws.addEventListener("message", (event) => {
272
301
  this.#lastMessageAt = Date.now();
273
302
  this.#zombieReported = false;
274
- this.#message(ws, String(event.data));
303
+ if (this.#options.compress && event.data instanceof ArrayBuffer) {
304
+ this.#decompress(new Uint8Array(event.data)).then((text) => this.#message(ws, text)).catch((err) => this.#emitError(err));
305
+ } else {
306
+ this.#message(ws, String(event.data));
307
+ }
275
308
  });
276
309
  ws.addEventListener("close", (event) => this.#close(ws, event.code));
277
310
  ws.addEventListener("error", () => this.#emitError(new GatewayError("Gateway WebSocket error")));
@@ -279,7 +312,7 @@ class Gateway {
279
312
  #startZombieDetection() {
280
313
  if (this.#zombieTimer)
281
314
  clearInterval(this.#zombieTimer);
282
- const interval = Math.max(1, Math.min(250, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
315
+ const interval = Math.max(1, Math.min(1000, this.#options.heartbeatAckTimeout, this.#options.zombieTimeout / 2));
283
316
  this.#zombieTimer = setInterval(() => {
284
317
  if (this.#closed || this.#ws?.readyState !== WebSocket.OPEN || this.#lastMessageAt === 0)
285
318
  return;
@@ -330,18 +363,24 @@ class Gateway {
330
363
  break;
331
364
  case GatewayOpcodes.HeartbeatAck:
332
365
  this.#heartbeatACK = true;
366
+ this.ping = Date.now() - this.#heartbeatSentAt;
333
367
  this.#clearHeartbeatAckTimer();
334
368
  this.#emit("heartbeatAck", payload.d);
335
369
  break;
336
370
  case GatewayOpcodes.Reconnect:
337
371
  ws.close(1001, "Server requested reconnect");
338
372
  break;
339
- case GatewayOpcodes.InvalidSession:
340
- this.#sessionId = undefined;
341
- this.#sequence = null;
342
- this.#emit("invalidSession", payload.d);
343
- ws.close(1000, "Invalid session");
373
+ case GatewayOpcodes.InvalidSession: {
374
+ const resumable = payload.d === true;
375
+ if (!resumable) {
376
+ this.#sessionId = undefined;
377
+ this.#sequence = null;
378
+ this.#resumeURL = undefined;
379
+ }
380
+ this.#emit("invalidSession", resumable);
381
+ ws.close(resumable ? GatewayCloseCodes.UnknownError : 1000, resumable ? "Invalid session (resumable)" : "Invalid session");
344
382
  break;
383
+ }
345
384
  }
346
385
  }
347
386
  #handleDispatch(event, data) {
@@ -356,6 +395,10 @@ class Gateway {
356
395
  this.#attempt = 0;
357
396
  this.#setState("READY" /* Ready */);
358
397
  this.#emit("ready", data);
398
+ } else if (event === "RESUMED") {
399
+ this.#attempt = 0;
400
+ this.#setState("READY" /* Ready */);
401
+ this.#emit("resumed", data);
359
402
  }
360
403
  this.#emit("RAW", { event: event ?? "UNKNOWN", data });
361
404
  if (this.#closed)
@@ -433,12 +476,12 @@ class Gateway {
433
476
  #sendHeartbeat() {
434
477
  this.#heartbeatACK = false;
435
478
  this.#heartbeatSentAt = Date.now();
436
- if (!this.send({
479
+ if (!this.#dispatch({
437
480
  op: GatewayOpcodes.Heartbeat,
438
481
  d: this.#sequence,
439
482
  s: null,
440
483
  t: null
441
- })) {
484
+ }, true)) {
442
485
  this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
443
486
  return;
444
487
  }
@@ -471,14 +514,23 @@ class Gateway {
471
514
  if (action === "identify") {
472
515
  this.#sequence = null;
473
516
  this.#sessionId = undefined;
517
+ this.#resumeURL = undefined;
474
518
  }
475
519
  this.#setState("RECONNECT" /* Reconnect */);
476
520
  this.#scheduleReconnect();
477
521
  }
478
522
  #closeAction(code) {
479
- if ([4004, 4010, 4011, 4012, 4013, 4014].includes(code))
523
+ const fatal = [
524
+ GatewayCloseCodes.AuthenticationFailed,
525
+ GatewayCloseCodes.InvalidShard,
526
+ GatewayCloseCodes.ShardingRequired,
527
+ GatewayCloseCodes.InvalidAPIVersion,
528
+ GatewayCloseCodes.InvalidIntents,
529
+ GatewayCloseCodes.DisallowedIntents
530
+ ];
531
+ if (fatal.includes(code))
480
532
  return "stop";
481
- if ([4007, 4009].includes(code))
533
+ if (code === GatewayCloseCodes.InvalidSeq || code === GatewayCloseCodes.SessionTimedOut)
482
534
  return "identify";
483
535
  return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
484
536
  }
@@ -491,7 +543,7 @@ class Gateway {
491
543
  return;
492
544
  }
493
545
  const delay = Math.min(this.#options.reconnectMaxDelay, this.#options.reconnectBaseDelay * 2 ** this.#attempt++);
494
- const jitter = Math.random() * Math.min(250, Math.max(1, delay * 0.25));
546
+ const jitter = Math.random() * Math.max(1, delay * 0.25);
495
547
  this.#reconnectTimer = setTimeout(() => {
496
548
  this.#reconnectTimer = undefined;
497
549
  this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
@@ -561,6 +613,37 @@ class Gateway {
561
613
  this.state = next;
562
614
  this.#emit("stateChange", { previous, next });
563
615
  }
616
+ #initDecompressor() {
617
+ const ds = new DecompressionStream("deflate-raw");
618
+ this.#decompressor = {
619
+ writer: ds.writable.getWriter(),
620
+ reader: ds.readable.getReader()
621
+ };
622
+ }
623
+ async#decompress(chunk) {
624
+ if (!this.#decompressor)
625
+ this.#initDecompressor();
626
+ const { writer, reader } = this.#decompressor;
627
+ await writer.write(chunk);
628
+ const parts = [];
629
+ while (true) {
630
+ const { done, value } = await Promise.race([
631
+ reader.read(),
632
+ new Promise((resolve) => setTimeout(() => resolve({ done: true, value: undefined }), 50))
633
+ ]);
634
+ if (done || !value)
635
+ break;
636
+ parts.push(value);
637
+ }
638
+ const total = parts.reduce((n, p) => n + p.length, 0);
639
+ const merged = new Uint8Array(total);
640
+ let offset = 0;
641
+ for (const p of parts) {
642
+ merged.set(p, offset);
643
+ offset += p.length;
644
+ }
645
+ return new TextDecoder().decode(merged);
646
+ }
564
647
  }
565
648
 
566
649
  // packages/sharding/src/bus.ts
@@ -636,8 +719,155 @@ class ShardBus {
636
719
  }
637
720
  }
638
721
  }
722
+ // packages/sharding/src/cluster.ts
723
+ import { fork } from "child_process";
724
+ import { cpus } from "os";
725
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
726
+
727
+ class ClusterManager {
728
+ clusters = new Map;
729
+ shardCount = 0;
730
+ #options;
731
+ #auto;
732
+ #autoScaleTimer;
733
+ #spawned = false;
734
+ constructor(options) {
735
+ if (!options.token?.trim())
736
+ throw new TypeError("A Discord bot token is required.");
737
+ if (!options.script?.trim())
738
+ throw new TypeError("A script path is required for clustering.");
739
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
740
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
741
+ }
742
+ this.#auto = options.shardCount === "auto";
743
+ this.#options = { ...options };
744
+ }
745
+ async fetchRecommendedShardCount() {
746
+ const response = await fetch("https://discord.com/api/v10/gateway/bot", {
747
+ headers: {
748
+ Authorization: `Bot ${this.#options.token}`,
749
+ "User-Agent": "Lunibee/0.1.0"
750
+ }
751
+ });
752
+ if (!response.ok) {
753
+ throw new Error(`Gateway discovery failed with status ${response.status}`);
754
+ }
755
+ const data = await response.json();
756
+ if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1) {
757
+ throw new Error("Gateway discovery returned an invalid shard count.");
758
+ }
759
+ return data.shards;
760
+ }
761
+ async spawn() {
762
+ if (this.#spawned)
763
+ return;
764
+ if (typeof process?.versions?.bun === "string") {
765
+ throw new Error("ClusterManager requires the Node.js runtime; Bun does not support child_process.fork() for this clustering mode.");
766
+ }
767
+ this.#spawned = true;
768
+ const count = this.#options.shardCount === "auto" || this.#options.shardCount === undefined ? await this.fetchRecommendedShardCount() : this.#options.shardCount;
769
+ this.shardCount = count;
770
+ const clusterCount = this.#options.clusterCount ?? cpus().length;
771
+ const chunks = Array.from({ length: clusterCount }, () => []);
772
+ for (let i = 0;i < count; i++) {
773
+ chunks[i % clusterCount].push(i);
774
+ }
775
+ for (let i = 0;i < clusterCount; i++) {
776
+ if (chunks[i].length === 0)
777
+ continue;
778
+ const child = fork(this.#options.script, [], {
779
+ env: {
780
+ ...process.env,
781
+ SHARD_LIST: chunks[i].join(","),
782
+ SHARD_COUNT: count.toString(),
783
+ CLUSTER_ID: i.toString()
784
+ }
785
+ });
786
+ this.clusters.set(i, {
787
+ id: i,
788
+ process: child,
789
+ shards: chunks[i]
790
+ });
791
+ await sleep(500);
792
+ }
793
+ if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
794
+ this.#autoScaleTimer = setInterval(() => {
795
+ this.checkAutoScale();
796
+ }, this.#options.autoScaleInterval);
797
+ }
798
+ }
799
+ async checkAutoScale() {
800
+ if (!this.#auto)
801
+ return;
802
+ try {
803
+ const recommended = await this.fetchRecommendedShardCount();
804
+ if (recommended !== this.shardCount) {
805
+ await this.respawn(recommended);
806
+ }
807
+ } catch (error) {
808
+ const normalized = error instanceof Error ? error : new Error(String(error));
809
+ this.#options.onAutoScaleError?.(normalized);
810
+ }
811
+ }
812
+ async respawn(newShardCount) {
813
+ this.#options.shardCount = newShardCount;
814
+ await this.shutdownAll();
815
+ this.#spawned = false;
816
+ await this.spawn();
817
+ }
818
+ async shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
819
+ if (this.#autoScaleTimer) {
820
+ clearInterval(this.#autoScaleTimer);
821
+ this.#autoScaleTimer = undefined;
822
+ }
823
+ await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
824
+ this.clusters.clear();
825
+ this.#spawned = false;
826
+ }
827
+ async#shutdownCluster(cluster, timeoutMs) {
828
+ const child = cluster.process;
829
+ if (child.exitCode !== null || child.signalCode !== null)
830
+ return;
831
+ await new Promise((resolve) => {
832
+ let timer;
833
+ const finish = () => {
834
+ if (timer)
835
+ clearTimeout(timer);
836
+ resolve();
837
+ };
838
+ child.once("exit", finish);
839
+ try {
840
+ child.kill("SIGTERM");
841
+ } catch {
842
+ child.off("exit", finish);
843
+ finish();
844
+ return;
845
+ }
846
+ timer = setTimeout(() => {
847
+ try {
848
+ child.kill("SIGKILL");
849
+ } catch {}
850
+ }, timeoutMs);
851
+ });
852
+ }
853
+ killAll() {
854
+ if (this.#autoScaleTimer) {
855
+ clearInterval(this.#autoScaleTimer);
856
+ this.#autoScaleTimer = undefined;
857
+ }
858
+ for (const cluster of this.clusters.values()) {
859
+ try {
860
+ cluster.process.kill();
861
+ } catch {}
862
+ }
863
+ this.clusters.clear();
864
+ this.#spawned = false;
865
+ }
866
+ }
639
867
 
640
868
  // packages/sharding/src/index.ts
869
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
870
+
641
871
  class ShardManager {
642
872
  shards = new Map;
643
873
  get shardCount() {
@@ -646,6 +876,8 @@ class ShardManager {
646
876
  #options;
647
877
  #resolved = false;
648
878
  #destroyed = false;
879
+ #auto;
880
+ #autoScaleTimer;
649
881
  constructor(options) {
650
882
  if (!options.token?.trim())
651
883
  throw new TypeError("A Discord bot token is required.");
@@ -654,6 +886,9 @@ class ShardManager {
654
886
  const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
655
887
  if (!Number.isInteger(count) || count < 1)
656
888
  throw new RangeError("Shard count must be a positive integer.");
889
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
890
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
891
+ this.#auto = options.shardCount === "auto";
657
892
  this.#options = { ...options };
658
893
  if (options.shardCount !== "auto")
659
894
  this.#initialize(count);
@@ -673,24 +908,56 @@ class ShardManager {
673
908
  return data.shards;
674
909
  }
675
910
  async connect() {
911
+ if (this.#autoScaleTimer) {
912
+ clearInterval(this.#autoScaleTimer);
913
+ this.#autoScaleTimer = undefined;
914
+ }
676
915
  await this.#ensureInitialized();
677
916
  for (const [id, shard] of this.shards) {
678
917
  try {
679
918
  await shard.connect();
680
919
  } catch (error) {
681
920
  this.destroy();
682
- throw new Error(`Failed to connect shard ${id}.`, { cause: error });
921
+ throw new Error(`Failed to connect shard ${id}.`, {
922
+ cause: error
923
+ });
683
924
  }
684
925
  if (id + 1 < this.shardCount && this.#options.spawnDelay && this.#options.spawnDelay > 0)
685
- await Bun.sleep(this.#options.spawnDelay);
926
+ await sleep2(this.#options.spawnDelay);
927
+ }
928
+ if (this.#options.autoScaleInterval && !this.#autoScaleTimer) {
929
+ this.#autoScaleTimer = setInterval(() => {
930
+ this.checkAutoScale();
931
+ }, this.#options.autoScaleInterval);
686
932
  }
687
933
  }
934
+ async checkAutoScale() {
935
+ if (!this.#auto)
936
+ return;
937
+ try {
938
+ const recommended = await this.fetchRecommendedShardCount();
939
+ if (recommended !== this.shardCount) {
940
+ await this.reshard(recommended);
941
+ }
942
+ } catch (error) {
943
+ this.#options.onAutoScaleError?.(error);
944
+ }
945
+ }
946
+ async reshard(count) {
947
+ this.#options.shardCount = count;
948
+ this.destroy();
949
+ await this.connect();
950
+ }
688
951
  destroy() {
689
952
  for (const shard of this.shards.values())
690
953
  shard.close();
691
954
  this.shards.clear();
692
955
  this.#resolved = false;
693
956
  this.#destroyed = true;
957
+ if (this.#autoScaleTimer) {
958
+ clearInterval(this.#autoScaleTimer);
959
+ this.#autoScaleTimer = undefined;
960
+ }
694
961
  }
695
962
  getShardIdForGuild(guildId) {
696
963
  const id = BigInt(guildId);
@@ -728,8 +995,9 @@ class ShardManager {
728
995
  }
729
996
  }
730
997
  export {
998
+ ClusterManager,
731
999
  ShardBus,
732
1000
  ShardManager
733
1001
  };
734
1002
 
735
- //# debugId=155A3557E58FD4CF64756E2164756E21
1003
+ //# debugId=F2AF359ADCA0121464756E2164756E21