lunibee 0.1.5 → 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 (60) hide show
  1. package/README.md +13 -24
  2. package/dist/builders/commands.d.ts +37 -0
  3. package/dist/builders/components.d.ts +127 -12
  4. package/dist/builders/embed.d.ts +3 -1
  5. package/dist/builders/index.d.ts +2 -2
  6. package/dist/builders/index.js +319 -16
  7. package/dist/builders/index.js.map +8 -7
  8. package/dist/collection/index.d.ts +36 -0
  9. package/dist/collection/index.js +110 -6
  10. package/dist/collection/index.js.map +4 -4
  11. package/dist/core/events.d.ts +69 -3
  12. package/dist/core/index.d.ts +158 -90
  13. package/dist/core/index.js +1543 -1169
  14. package/dist/core/index.js.map +24 -23
  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 +5 -4
  21. package/dist/index.js +3882 -1588
  22. package/dist/index.js.map +43 -35
  23. package/dist/managers/emoji.d.ts +38 -0
  24. package/dist/managers/guild.d.ts +141 -0
  25. package/dist/managers/index.d.ts +86 -75
  26. package/dist/managers/index.js +1084 -67
  27. package/dist/managers/index.js.map +21 -16
  28. package/dist/managers/message.d.ts +7 -12
  29. package/dist/managers/user.d.ts +20 -0
  30. package/dist/rest/index.d.ts +173 -1
  31. package/dist/rest/index.js +343 -47
  32. package/dist/rest/index.js.map +8 -6
  33. package/dist/rest/redis.d.ts +37 -0
  34. package/dist/rest/routes.d.ts +26 -1
  35. package/dist/rest/store.d.ts +38 -0
  36. package/dist/rest/webhook.d.ts +2 -2
  37. package/dist/sharding/cluster.d.ts +61 -0
  38. package/dist/sharding/index.d.ts +8 -0
  39. package/dist/sharding/index.js +289 -18
  40. package/dist/sharding/index.js.map +8 -7
  41. package/dist/structures/audit-log.d.ts +26 -0
  42. package/dist/structures/base.d.ts +131 -48
  43. package/dist/structures/embed.d.ts +65 -0
  44. package/dist/structures/index.d.ts +74 -26
  45. package/dist/structures/index.js +1528 -59
  46. package/dist/structures/index.js.map +14 -8
  47. package/dist/structures/interactions.d.ts +72 -17
  48. package/dist/structures/resources.d.ts +199 -7
  49. package/dist/types/index.d.ts +605 -84
  50. package/dist/types/index.js +71 -1
  51. package/dist/types/index.js.map +3 -3
  52. package/dist/utils/index.js.map +2 -2
  53. package/dist/voice/index.d.ts +88 -0
  54. package/dist/voice/index.js +204 -5
  55. package/dist/voice/index.js.map +3 -3
  56. package/dist/ws/index.d.ts +40 -1
  57. package/dist/ws/index.js +105 -18
  58. package/dist/ws/index.js.map +4 -4
  59. package/package.json +16 -16
  60. 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,7 +395,14 @@ 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
  }
403
+ this.#emit("RAW", { event: event ?? "UNKNOWN", data });
404
+ if (this.#closed)
405
+ return;
360
406
  this.#emit(event ?? "dispatch", data);
361
407
  }
362
408
  #handleHello(data) {
@@ -430,12 +476,12 @@ class Gateway {
430
476
  #sendHeartbeat() {
431
477
  this.#heartbeatACK = false;
432
478
  this.#heartbeatSentAt = Date.now();
433
- if (!this.send({
479
+ if (!this.#dispatch({
434
480
  op: GatewayOpcodes.Heartbeat,
435
481
  d: this.#sequence,
436
482
  s: null,
437
483
  t: null
438
- })) {
484
+ }, true)) {
439
485
  this.#emitError(new GatewayError("Unable to send Gateway heartbeat because the WebSocket is not open."));
440
486
  return;
441
487
  }
@@ -468,14 +514,23 @@ class Gateway {
468
514
  if (action === "identify") {
469
515
  this.#sequence = null;
470
516
  this.#sessionId = undefined;
517
+ this.#resumeURL = undefined;
471
518
  }
472
519
  this.#setState("RECONNECT" /* Reconnect */);
473
520
  this.#scheduleReconnect();
474
521
  }
475
522
  #closeAction(code) {
476
- 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))
477
532
  return "stop";
478
- if ([4007, 4009].includes(code))
533
+ if (code === GatewayCloseCodes.InvalidSeq || code === GatewayCloseCodes.SessionTimedOut)
479
534
  return "identify";
480
535
  return this.#sessionId && this.#sequence !== null ? "resume" : "identify";
481
536
  }
@@ -488,7 +543,7 @@ class Gateway {
488
543
  return;
489
544
  }
490
545
  const delay = Math.min(this.#options.reconnectMaxDelay, this.#options.reconnectBaseDelay * 2 ** this.#attempt++);
491
- const jitter = Math.random() * Math.min(250, Math.max(1, delay * 0.25));
546
+ const jitter = Math.random() * Math.max(1, delay * 0.25);
492
547
  this.#reconnectTimer = setTimeout(() => {
493
548
  this.#reconnectTimer = undefined;
494
549
  this.#open(this.#resumeURL ?? "wss://gateway.discord.gg/?v=10&encoding=json");
@@ -558,6 +613,37 @@ class Gateway {
558
613
  this.state = next;
559
614
  this.#emit("stateChange", { previous, next });
560
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
+ }
561
647
  }
562
648
 
563
649
  // packages/sharding/src/bus.ts
@@ -633,8 +719,155 @@ class ShardBus {
633
719
  }
634
720
  }
635
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
+ }
636
867
 
637
868
  // packages/sharding/src/index.ts
869
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
870
+
638
871
  class ShardManager {
639
872
  shards = new Map;
640
873
  get shardCount() {
@@ -643,6 +876,8 @@ class ShardManager {
643
876
  #options;
644
877
  #resolved = false;
645
878
  #destroyed = false;
879
+ #auto;
880
+ #autoScaleTimer;
646
881
  constructor(options) {
647
882
  if (!options.token?.trim())
648
883
  throw new TypeError("A Discord bot token is required.");
@@ -651,6 +886,9 @@ class ShardManager {
651
886
  const count = options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
652
887
  if (!Number.isInteger(count) || count < 1)
653
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";
654
892
  this.#options = { ...options };
655
893
  if (options.shardCount !== "auto")
656
894
  this.#initialize(count);
@@ -670,24 +908,56 @@ class ShardManager {
670
908
  return data.shards;
671
909
  }
672
910
  async connect() {
911
+ if (this.#autoScaleTimer) {
912
+ clearInterval(this.#autoScaleTimer);
913
+ this.#autoScaleTimer = undefined;
914
+ }
673
915
  await this.#ensureInitialized();
674
916
  for (const [id, shard] of this.shards) {
675
917
  try {
676
918
  await shard.connect();
677
919
  } catch (error) {
678
920
  this.destroy();
679
- throw new Error(`Failed to connect shard ${id}.`, { cause: error });
921
+ throw new Error(`Failed to connect shard ${id}.`, {
922
+ cause: error
923
+ });
680
924
  }
681
925
  if (id + 1 < this.shardCount && this.#options.spawnDelay && this.#options.spawnDelay > 0)
682
- 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);
932
+ }
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);
683
944
  }
684
945
  }
946
+ async reshard(count) {
947
+ this.#options.shardCount = count;
948
+ this.destroy();
949
+ await this.connect();
950
+ }
685
951
  destroy() {
686
952
  for (const shard of this.shards.values())
687
953
  shard.close();
688
954
  this.shards.clear();
689
955
  this.#resolved = false;
690
956
  this.#destroyed = true;
957
+ if (this.#autoScaleTimer) {
958
+ clearInterval(this.#autoScaleTimer);
959
+ this.#autoScaleTimer = undefined;
960
+ }
691
961
  }
692
962
  getShardIdForGuild(guildId) {
693
963
  const id = BigInt(guildId);
@@ -725,8 +995,9 @@ class ShardManager {
725
995
  }
726
996
  }
727
997
  export {
998
+ ClusterManager,
728
999
  ShardBus,
729
1000
  ShardManager
730
1001
  };
731
1002
 
732
- //# debugId=3D6EA7786F9CDC6964756E2164756E21
1003
+ //# debugId=F2AF359ADCA0121464756E2164756E21