discord-self-lite 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discord-self-lite",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Lightweight Discord selfbot library",
5
5
  "keywords": [
6
6
  "discord",
@@ -2,6 +2,7 @@
2
2
  * Represents a Discord channel
3
3
  */
4
4
  const Permissions = require("./Permissions");
5
+ const Message = require("./Message");
5
6
 
6
7
  /**
7
8
  * Safe BigInt conversion
@@ -50,10 +51,11 @@ class Channel {
50
51
  /**
51
52
  * Send a message to this channel
52
53
  * @param {string|object} payload - Message content or payload object
53
- * @returns {Promise<object>} The sent message data
54
+ * @returns {Promise<Message>} The sent message object
54
55
  */
55
56
  async send(payload) {
56
- return await this.rest.sendMessage(this.id, payload);
57
+ const data = await this.rest.sendMessage(this.id, payload);
58
+ return new Message(this.client, data);
57
59
  }
58
60
 
59
61
  /**
@@ -88,6 +90,45 @@ class Channel {
88
90
  return new Message(this.client, data);
89
91
  }
90
92
 
93
+ /**
94
+ * Fetch webhooks for this channel
95
+ * @returns {Promise<Array>} Array of webhook objects
96
+ */
97
+ async fetchWebhooks() {
98
+ const webhooks = await this.rest.fetchWebhooks(this.id);
99
+ if (!Array.isArray(webhooks)) return webhooks;
100
+
101
+ return webhooks.map((webhook) => {
102
+ const url = webhook.token
103
+ ? `https://discord.com/api/webhooks/${webhook.id}/${webhook.token}`
104
+ : null;
105
+
106
+ return {
107
+ ...webhook,
108
+ url,
109
+ };
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Create a webhook in this channel
115
+ * @param {string} name - Webhook name
116
+ * @param {object} [options={}] - Webhook options
117
+ * @param {string} [options.avatar] - Webhook avatar payload
118
+ * @returns {Promise<object>} Created webhook object with `.url`
119
+ */
120
+ async createWebhook(name, options = {}) {
121
+ const webhook = await this.rest.createWebhook(this.id, name, options);
122
+ if (!webhook) return webhook;
123
+
124
+ return {
125
+ ...webhook,
126
+ url: webhook.token
127
+ ? `https://discord.com/api/webhooks/${webhook.id}/${webhook.token}`
128
+ : null,
129
+ };
130
+ }
131
+
91
132
  /**
92
133
  * Get the guild this channel belongs to
93
134
  * @returns {Guild|null} The guild instance or null if not a guild channel
@@ -4,6 +4,7 @@ const RestManager = require("../connection/rest/RestManager");
4
4
  const Guild = require("./Guild");
5
5
  const Channel = require("./Channel");
6
6
  const Message = require("./Message");
7
+ const User = require("./User");
7
8
 
8
9
  /**
9
10
  * The main client for connecting to Discord
@@ -35,6 +36,7 @@ class Client extends EventEmitter {
35
36
  this.rest = null;
36
37
  this.guilds = new Map(); // Cache for Guild instances
37
38
  this.channels = new Map(); // Cache for Channel instances
39
+ this.users = new Map(); // Cache for User instances
38
40
  this.sessionId = null; // Will be set from WebSocket READY event
39
41
  this.user = null; // Will be set from WebSocket READY event
40
42
  }
@@ -89,10 +91,11 @@ class Client extends EventEmitter {
89
91
  * Send a message to a channel
90
92
  * @param {string} channelId - The channel ID to send to
91
93
  * @param {string|object} content - Message content or payload
92
- * @returns {Promise<object>} The sent message data
94
+ * @returns {Promise<Message>} The sent message object
93
95
  */
94
96
  async sendMessage(channelId, content) {
95
- return await this.rest.sendMessage(channelId, content);
97
+ const data = await this.rest.sendMessage(channelId, content);
98
+ return new Message(this, data);
96
99
  }
97
100
 
98
101
  /**
@@ -119,6 +122,18 @@ class Client extends EventEmitter {
119
122
  return this.guilds.get(id);
120
123
  }
121
124
 
125
+ /**
126
+ * Get user from cache, or create lightweight instance if not cached
127
+ * @param {string} id - The user ID
128
+ * @returns {User} The User instance
129
+ */
130
+ getUser(id) {
131
+ if (!this.users.has(id)) {
132
+ this.users.set(id, new User(this, { id }));
133
+ }
134
+ return this.users.get(id);
135
+ }
136
+
122
137
  /**
123
138
  * Fetch guild data from API and update cache
124
139
  * @param {string} id - The guild ID
@@ -147,6 +162,18 @@ class Client extends EventEmitter {
147
162
  return guild;
148
163
  }
149
164
 
165
+ /**
166
+ * Fetch user data from API and update cache
167
+ * @param {string} id - The user ID
168
+ * @returns {Promise<User>} The User instance with fresh data
169
+ */
170
+ async fetchUser(id) {
171
+ const data = await this.rest.fetchUser(id);
172
+ const user = new User(this, data);
173
+ this.users.set(id, user);
174
+ return user;
175
+ }
176
+
150
177
  /**
151
178
  * Fetch channel data from API and update cache
152
179
  * @param {string} id - The channel ID
@@ -0,0 +1,71 @@
1
+ const User = require("./User");
2
+
3
+ /**
4
+ * Represents the authenticated client user
5
+ */
6
+ class ClientUser extends User {
7
+ /**
8
+ * Set the user's presence/status
9
+ * @param {object} presence - Presence data
10
+ * @param {string} [presence.status] - Status: 'online', 'idle', 'dnd', 'invisible'
11
+ * @param {Array} [presence.activities] - Array of activity objects
12
+ * @param {boolean} [presence.afk] - Whether the user is AFK
13
+ * @param {number} [presence.since] - Unix timestamp of when the status was set
14
+ * @returns {void}
15
+ */
16
+ setPresence(presence) {
17
+ if (!this.client.ws || !this.client.ws.ready) {
18
+ throw new Error("Client is not connected");
19
+ }
20
+
21
+ const presenceData = {
22
+ status: presence.status || "online",
23
+ since: presence.since || 0,
24
+ activities: presence.activities || [],
25
+ afk: presence.afk || false,
26
+ };
27
+
28
+ // Send presence update via WebSocket
29
+ this.client.ws.send({
30
+ op: 3,
31
+ d: presenceData,
32
+ });
33
+
34
+ // Update client's stored presence
35
+ this.client.options.presence = presenceData;
36
+ }
37
+
38
+ /**
39
+ * Set the user's status (convenience method)
40
+ * @param {string} status - Status: 'online', 'idle', 'dnd', 'invisible'
41
+ * @returns {void}
42
+ */
43
+ setStatus(status) {
44
+ this.setPresence({
45
+ status: status,
46
+ since: this.client.options.presence.since,
47
+ activities: this.client.options.presence.activities,
48
+ afk: this.client.options.presence.afk,
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Set the user's activity
54
+ * @param {object} activity - Activity data
55
+ * @param {string} activity.name - Activity name
56
+ * @param {string} [activity.type=0] - Activity type
57
+ * @param {string} [activity.url] - Stream URL (for type 1)
58
+ * @returns {void}
59
+ */
60
+ setActivity(activity) {
61
+ const activities = activity ? [activity] : [];
62
+ this.setPresence({
63
+ status: this.client.options.presence.status,
64
+ since: this.client.options.presence.since,
65
+ activities: activities,
66
+ afk: this.client.options.presence.afk,
67
+ });
68
+ }
69
+ }
70
+
71
+ module.exports = ClientUser;
@@ -8,12 +8,14 @@ class DiscordAPIError extends Error {
8
8
  * @param {string} message - The error message.
9
9
  * @param {number} status - The HTTP status code of the response.
10
10
  * @param {string} path - The path of the API endpoint that was requested.
11
+ * @param {object|null} [fullError=null] - Full Discord API error payload.
11
12
  */
12
- constructor(message, status, path) {
13
+ constructor(message, status, path, fullError = null) {
13
14
  super(message);
14
15
  this.name = "DiscordAPIError";
15
16
  this.status = status;
16
17
  this.path = path;
18
+ this.fullError = fullError;
17
19
  }
18
20
  }
19
21
 
@@ -1,4 +1,5 @@
1
1
  const WebSocketError = require("./WebSocketError");
2
+ const User = require("./User");
2
3
 
3
4
  /**
4
5
  * Represents a Discord message
@@ -20,6 +21,17 @@ class Message {
20
21
  this[camelKey] = value;
21
22
  }
22
23
 
24
+ // Wrap author as User instance and cache it
25
+ if (this.author && typeof this.author === "object") {
26
+ const authorId = this.author.id;
27
+ if (!this.client.users.has(authorId)) {
28
+ this.author = new User(this.client, this.author);
29
+ this.client.users.set(authorId, this.author);
30
+ } else {
31
+ this.author = this.client.users.get(authorId);
32
+ }
33
+ }
34
+
23
35
  this.components = this.components || [];
24
36
  this.attachments = this.attachments || [];
25
37
  this.embeds = this.embeds || [];
@@ -36,6 +48,33 @@ class Message {
36
48
  return this.client.getChannel(this.channelId);
37
49
  }
38
50
 
51
+ async edit(payload) {
52
+ const updatedData = await this.client.rest.editMessage(
53
+ this.channelId,
54
+ this.id,
55
+ payload,
56
+ );
57
+
58
+ // Update internal raw data
59
+ this.data = updatedData;
60
+
61
+ for (const [key, value] of Object.entries(updatedData)) {
62
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
63
+ letter.toUpperCase(),
64
+ );
65
+ this[camelKey] = value;
66
+ }
67
+
68
+ this.components = this.components || [];
69
+ this.attachments = this.attachments || [];
70
+ this.embeds = this.embeds || [];
71
+ this.mentions = this.mentions || [];
72
+ this.mentionRoles = this.mentionRoles || [];
73
+ this.reactions = this.reactions || [];
74
+
75
+ return this;
76
+ }
77
+
39
78
  /**
40
79
  * Get the guild this message was sent in
41
80
  * @returns {Guild|null} The guild instance, or null if message was sent in DM
@@ -135,6 +174,51 @@ class Message {
135
174
  messageFlags,
136
175
  );
137
176
  }
177
+
178
+ /**
179
+ * Backwards-compatible alias for message reference data
180
+ */
181
+
182
+ get reference() {
183
+ // If the referenced message is embedded in the payload, return a Message instance.
184
+ const embedded =
185
+ this.data?.referenced_message || this.referencedMessage || null;
186
+ if (embedded && typeof embedded === "object") {
187
+ return new Message(this.client, embedded);
188
+ }
189
+
190
+ // Otherwise, no embedded message is available synchronously.
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Fetch the referenced message asynchronously when only IDs are present.
196
+ * Returns the referenced `Message` instance or `null` if not available.
197
+ */
198
+ async fetchReference() {
199
+ const ref = this.messageReference || this.data?.message_reference;
200
+ if (!ref) return null;
201
+
202
+ const messageId = ref.messageId || ref.message_id;
203
+ const channelId = ref.channelId || ref.channel_id || this.channelId;
204
+ if (!messageId || !channelId) return null;
205
+
206
+ // Try to get channel from cache or fetch from API
207
+ let channel = this.client.getChannel(channelId);
208
+ if (!channel) {
209
+ try {
210
+ channel = await this.client.fetchChannel(channelId);
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ try {
217
+ return await channel.fetchMessage(messageId);
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
138
222
  }
139
223
 
140
224
  module.exports = Message;
@@ -10,6 +10,7 @@ class User {
10
10
  constructor(client, data) {
11
11
  this.client = client;
12
12
  this.data = data;
13
+ this._dmChannelId = null;
13
14
 
14
15
  // Copy all user properties
15
16
  for (const [key, value] of Object.entries(data)) {
@@ -19,68 +20,39 @@ class User {
19
20
  this[camelKey] = value;
20
21
  }
21
22
  }
22
-
23
23
  /**
24
- * Set the user's presence/status
25
- * @param {object} presence - Presence data
26
- * @param {string} [presence.status] - Status: 'online', 'idle', 'dnd', 'invisible'
27
- * @param {Array} [presence.activities] - Array of activity objects
28
- * @param {boolean} [presence.afk] - Whether the user is AFK
29
- * @param {number} [presence.since] - Unix timestamp of when the status was set
30
- * @returns {void}
24
+ * Create a DM channel with this user
25
+ * @returns {Promise<Channel>} The DM channel instance
31
26
  */
32
- setPresence(presence) {
33
- if (!this.client.ws || !this.client.ws.ready) {
34
- throw new Error("Client is not connected");
35
- }
36
-
37
- const presenceData = {
38
- status: presence.status || "online",
39
- since: presence.since || 0,
40
- activities: presence.activities || [],
41
- afk: presence.afk || false,
42
- };
43
-
44
- // Send presence update via WebSocket
45
- this.client.ws.send({
46
- op: 3,
47
- d: presenceData,
48
- });
49
-
50
- // Update client's stored presence
51
- this.client.options.presence = presenceData;
27
+ async createDM() {
28
+ const data = await this.client.rest.createDM(this.id);
29
+ const Channel = require("./Channel");
30
+ const channel = new Channel(this.client, this.client.rest, data);
31
+ this.client.channels.set(channel.id, channel);
32
+ this._dmChannelId = channel.id;
33
+ return channel;
52
34
  }
53
35
 
54
36
  /**
55
- * Set the user's status (convenience method)
56
- * @param {string} status - Status: 'online', 'idle', 'dnd', 'invisible'
57
- * @returns {void}
37
+ * Get existing DM channel or create one if needed
38
+ * @returns {Promise<Channel>} The DM channel instance
58
39
  */
59
- setStatus(status) {
60
- this.setPresence({
61
- status: status,
62
- since: this.client.options.presence.since,
63
- activities: this.client.options.presence.activities,
64
- afk: this.client.options.presence.afk,
65
- });
40
+ async getDMChannel() {
41
+ if (this._dmChannelId && this.client.channels.has(this._dmChannelId)) {
42
+ return this.client.channels.get(this._dmChannelId);
43
+ }
44
+
45
+ return await this.createDM();
66
46
  }
67
47
 
68
48
  /**
69
- * Set the user's activity
70
- * @param {object} activity - Activity data
71
- * @param {string} activity.name - Activity name
72
- * @param {string} [activity.type=0] - Activity type (0=Playing, 1=Streaming, 2=Listening, 3=Watching, 5=Competing)
73
- * @param {string} [activity.url] - Stream URL (for type 1)
74
- * @returns {void}
49
+ * Send a DM to this user
50
+ * @param {string|object} payload - Message content or payload object
51
+ * @returns {Promise<Message>} The sent message
75
52
  */
76
- setActivity(activity) {
77
- const activities = activity ? [activity] : [];
78
- this.setPresence({
79
- status: this.client.options.presence.status,
80
- since: this.client.options.presence.since,
81
- activities: activities,
82
- afk: this.client.options.presence.afk,
83
- });
53
+ async send(payload) {
54
+ const dmChannel = await this.getDMChannel();
55
+ return await dmChannel.send(payload);
84
56
  }
85
57
  }
86
58
 
@@ -3,6 +3,8 @@ const react = require("./methods/react");
3
3
  const clickButton = require("./methods/clickButton");
4
4
  const fetchGuildMembers = require("./methods/fetchGuildMembers");
5
5
  const fetchGuildMember = require("./methods/fetchGuildMember");
6
+ const fetchUser = require("./methods/fetchUser");
7
+ const createDM = require("./methods/createDM");
6
8
  const DiscordAPIError = require("../../classes/DiscordAPIError");
7
9
 
8
10
  /**
@@ -23,6 +25,12 @@ class RestManager {
23
25
  this.globalRateLimit = null; // { reset }
24
26
  this.requestQueue = [];
25
27
  this.processingQueue = false;
28
+
29
+ // Circuit breaker state for heavily rate-limited routes
30
+ this.circuitBreakers = new Map(); // routeKey -> { failures, lastFailure, openUntil }
31
+
32
+ // Suspended routes - temporarily disabled endpoints
33
+ this.suspendedRoutes = new Map(); // routeKey -> { suspendedUntil, reason }
26
34
  }
27
35
 
28
36
  /**
@@ -33,7 +41,64 @@ class RestManager {
33
41
  * @throws {Error} If the request fails
34
42
  */
35
43
  async request(endpoint, options = {}) {
44
+ // Check rate limits before queuing the request with more aggressive logic
45
+ const method = options.method || "GET";
46
+ const routeKey = this.getRouteKey(endpoint, method);
47
+
48
+ // Check circuit breaker - reject immediately if route is temporarily disabled
49
+ const circuitBreaker = this.circuitBreakers.get(routeKey);
50
+ if (circuitBreaker && circuitBreaker.openUntil > Date.now()) {
51
+ const remainingTime = circuitBreaker.openUntil - Date.now();
52
+ console.log(
53
+ `🚫 Request dropped: Circuit breaker open for ${routeKey} (${Math.round(remainingTime / 1000)}s remaining)`,
54
+ );
55
+ return Promise.resolve(null);
56
+ }
57
+
58
+ // Check if route is suspended - silently reject to avoid spam
59
+ const suspendedRoute = this.suspendedRoutes.get(routeKey);
60
+ if (suspendedRoute && suspendedRoute.suspendedUntil > Date.now()) {
61
+ const remainingTime = suspendedRoute.suspendedUntil - Date.now();
62
+ console.log(
63
+ `🚫 Request dropped: Route ${routeKey} is suspended for ${Math.round(remainingTime / 1000)}s`,
64
+ );
65
+ return Promise.resolve(null);
66
+ }
67
+
68
+ // Check rate limits - if rate limited, suspend immediately instead of waiting
69
+ if (this.isRateLimited(endpoint, method)) {
70
+ const suspensionTime = 2 * 60 * 1000; // 2 minutes
71
+ this.suspendedRoutes.set(routeKey, {
72
+ suspendedUntil: Date.now() + suspensionTime,
73
+ reason: "Rate limited - pre-emptive suspension",
74
+ });
75
+
76
+ console.log(
77
+ `⏸️ Suspending route ${routeKey} for 2 minutes (pre-emptive)`,
78
+ );
79
+
80
+ // Clear queued requests for this route
81
+ const clearedCount = this.clearQueuedRequestsForRoute(routeKey);
82
+ if (clearedCount > 0) {
83
+ console.log(
84
+ `🗑️ Cleared ${clearedCount} queued requests for suspended route ${routeKey}`,
85
+ );
86
+ }
87
+
88
+ console.log(`🚫 Request dropped: Route suspended for ${routeKey}`);
89
+ return Promise.resolve(null);
90
+ }
91
+
36
92
  return new Promise((resolve, reject) => {
93
+ // Prevent queue from growing too large during rate limit storms
94
+ if (this.requestQueue.length >= 50) {
95
+ console.log(
96
+ `🚫 Request dropped: Queue full (${this.requestQueue.length} requests)`,
97
+ );
98
+ resolve(null);
99
+ return;
100
+ }
101
+
37
102
  this.requestQueue.push({
38
103
  endpoint,
39
104
  options,
@@ -67,6 +132,26 @@ class RestManager {
67
132
 
68
133
  this.processingQueue = true;
69
134
 
135
+ // Filter out suspended routes before processing any requests
136
+ this.requestQueue = this.requestQueue.filter((request) => {
137
+ const routeKey = this.getRouteKey(
138
+ request.endpoint,
139
+ request.options.method || "GET",
140
+ );
141
+ const suspendedRoute = this.suspendedRoutes.get(routeKey);
142
+
143
+ if (suspendedRoute && suspendedRoute.suspendedUntil > Date.now()) {
144
+ // Silently resolve suspended requests
145
+ console.log(
146
+ `🚫 Queued request dropped: Route ${routeKey} is suspended`,
147
+ );
148
+ request.resolve(null);
149
+ return false; // Remove from queue
150
+ }
151
+
152
+ return true; // Keep in queue
153
+ });
154
+
70
155
  while (this.requestQueue.length > 0) {
71
156
  // Double-check global rate limit for each request
72
157
  if (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
@@ -80,35 +165,53 @@ class RestManager {
80
165
  const { endpoint, options, resolve, reject } = request;
81
166
 
82
167
  try {
83
- // Check endpoint-specific rate limit
84
168
  const routeKey = this.getRouteKey(endpoint, options.method || "GET");
85
- const rateLimit = this.rateLimits.get(routeKey);
86
169
 
170
+ // Check suspension status before processing
171
+ const suspendedRoute = this.suspendedRoutes.get(routeKey);
172
+ if (suspendedRoute && suspendedRoute.suspendedUntil > Date.now()) {
173
+ console.log(
174
+ `🚫 Processing request dropped: Route ${routeKey} is suspended`,
175
+ );
176
+ resolve(null);
177
+ continue;
178
+ }
179
+
180
+ // Check rate limit before processing - suspend immediately if exhausted
181
+ const rateLimit = this.rateLimits.get(routeKey);
87
182
  if (
88
183
  rateLimit &&
89
184
  rateLimit.remaining <= 0 &&
90
185
  Date.now() < rateLimit.reset
91
186
  ) {
92
- const delay = rateLimit.reset - Date.now();
187
+ // Suspend instead of waiting
188
+ const suspensionTime = 2 * 60 * 1000; // 2 minutes
189
+ this.suspendedRoutes.set(routeKey, {
190
+ suspendedUntil: Date.now() + suspensionTime,
191
+ reason: "Rate limit exhausted during queue processing",
192
+ });
193
+
93
194
  console.log(
94
- `⏳ Route rate limit for ${routeKey} (${endpoint}), waiting ${delay}ms`,
195
+ `⏸️ Suspending route ${routeKey} for 2 minutes (queue processing)`,
95
196
  );
96
- await this.sleep(delay);
97
197
 
98
- // After waiting, check if we still have remaining requests
99
- const updatedRateLimit = this.rateLimits.get(routeKey);
100
- if (updatedRateLimit && updatedRateLimit.remaining <= 0) {
101
- // Still rate limited, put request back at front of queue
102
- this.requestQueue.unshift(request);
103
- continue;
198
+ // Clear remaining queued requests for this route
199
+ const clearedCount = this.clearQueuedRequestsForRoute(routeKey);
200
+ if (clearedCount > 0) {
201
+ console.log(
202
+ `🗑️ Cleared ${clearedCount} additional queued requests for ${routeKey}`,
203
+ );
104
204
  }
205
+
206
+ resolve(null);
207
+ continue;
105
208
  }
106
209
 
107
210
  const result = await this.makeRequest(endpoint, options);
108
211
  resolve(result);
109
212
 
110
- // Small delay between requests to be nice to Discord
111
- await this.sleep(100);
213
+ // Delay between requests to be conservative with rate limits
214
+ await this.sleep(500);
112
215
  } catch (error) {
113
216
  reject(error);
114
217
  }
@@ -147,7 +250,7 @@ class RestManager {
147
250
  // Handle rate limiting
148
251
  if (response.status === 429) {
149
252
  const retryAfter =
150
- parseInt(response.headers.get("retry-after")) * 1000 + 1000;
253
+ parseInt(response.headers.get("retry-after")) * 1000 || 2000;
151
254
  const isGlobal =
152
255
  response.headers.get("x-ratelimit-global") === "true";
153
256
  const routeKey = this.getRouteKey(endpoint, options.method || "GET");
@@ -155,38 +258,99 @@ class RestManager {
155
258
  console.log(
156
259
  `🚫 Rate limited! ${
157
260
  isGlobal ? "Global" : `Route (${routeKey})`
158
- } limit for ${endpoint}, retry after ${retryAfter}ms`,
261
+ } limit for ${endpoint}`,
159
262
  );
160
263
 
161
264
  if (isGlobal) {
162
- this.globalRateLimit = { reset: Date.now() + retryAfter };
265
+ this.globalRateLimit = { reset: Date.now() + retryAfter + 2000 };
266
+ console.log(
267
+ `⏳ Global rate limit set, waiting ${retryAfter + 2000}ms`,
268
+ );
163
269
  } else {
164
- // Update route-specific rate limit for 429 responses
270
+ // Immediately suspend the route for 2 minutes on first 429
271
+ const suspensionTime = 2 * 60 * 1000;
272
+ this.suspendedRoutes.set(routeKey, {
273
+ suspendedUntil: Date.now() + suspensionTime,
274
+ reason: `429 response on attempt ${attempt + 1}`,
275
+ });
276
+
277
+ console.log(
278
+ `⏸️ Suspending route ${routeKey} for 2 minutes (429 response)`,
279
+ );
280
+
281
+ // Clear queue for this route
282
+ const clearedCount = this.clearQueuedRequestsForRoute(routeKey);
283
+ if (clearedCount > 0) {
284
+ console.log(
285
+ `�️ Cleared ${clearedCount} queued requests for ${routeKey}`,
286
+ );
287
+ }
288
+
289
+ // Update rate limit state
165
290
  this.rateLimits.set(routeKey, {
166
291
  remaining: 0,
167
- reset: Date.now() + retryAfter,
168
- limit: this.rateLimits.get(routeKey)?.limit || 5, // Default limit if unknown
292
+ reset: Date.now() + suspensionTime,
293
+ limit: this.rateLimits.get(routeKey)?.limit || 5,
169
294
  updatedAt: Date.now(),
170
295
  });
296
+
297
+ // Open circuit breaker after 2 consecutive 429s
298
+ const breaker = this.circuitBreakers.get(routeKey) || {
299
+ failures: 0,
300
+ lastFailure: 0,
301
+ openUntil: 0,
302
+ };
303
+ breaker.failures++;
304
+ breaker.lastFailure = Date.now();
305
+
306
+ if (breaker.failures >= 2) {
307
+ breaker.openUntil = Date.now() + suspensionTime;
308
+ console.log(`🔌 Circuit breaker OPENED for ${routeKey}`);
309
+ }
310
+
311
+ this.circuitBreakers.set(routeKey, breaker);
312
+
313
+ // Don't retry, return null immediately
314
+ return null;
171
315
  }
172
316
 
173
- await this.sleep(retryAfter);
174
- attempt++;
175
- continue;
317
+ // For global rate limits, wait and retry
318
+ if (isGlobal) {
319
+ await this.sleep(retryAfter + 2000);
320
+ attempt++;
321
+ continue;
322
+ }
323
+
324
+ // For route rate limits, don't retry
325
+ return null;
176
326
  }
177
327
 
178
328
  if (!response.ok) {
179
329
  const error = await response
180
330
  .json()
181
331
  .catch(() => ({ message: "Unknown error" }));
182
- throw new DiscordAPIError(error.message, response.status, endpoint);
332
+ throw new DiscordAPIError(
333
+ error.message,
334
+ response.status,
335
+ endpoint,
336
+ error,
337
+ );
183
338
  }
184
339
 
185
340
  // Handle 204 No Content (successful but no body)
186
341
  if (response.status === 204) {
342
+ // Reset circuit breaker and suspension on successful response
343
+ const routeKey = this.getRouteKey(endpoint, options.method || "GET");
344
+ this.circuitBreakers.delete(routeKey);
345
+ this.suspendedRoutes.delete(routeKey);
187
346
  return null;
188
347
  }
189
348
 
349
+ // Reset circuit breaker and suspension on successful response
350
+ const routeKey = this.getRouteKey(endpoint, options.method || "GET");
351
+ this.circuitBreakers.delete(routeKey);
352
+ this.suspendedRoutes.delete(routeKey);
353
+
190
354
  return await response.json();
191
355
  } catch (error) {
192
356
  if (attempt === maxRetries - 1) {
@@ -258,6 +422,48 @@ class RestManager {
258
422
  this.rateLimits.delete(key);
259
423
  }
260
424
  }
425
+
426
+ // Clean up expired circuit breakers
427
+ for (const [key, breaker] of this.circuitBreakers.entries()) {
428
+ if (now > breaker.openUntil) {
429
+ this.circuitBreakers.delete(key);
430
+ }
431
+ }
432
+
433
+ // Clean up expired route suspensions
434
+ for (const [key, suspension] of this.suspendedRoutes.entries()) {
435
+ if (now > suspension.suspendedUntil) {
436
+ this.suspendedRoutes.delete(key);
437
+ }
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Sleep for a specified number of milliseconds
443
+ * @private
444
+ * @param {number} ms - Milliseconds to sleep
445
+ * @returns {Promise<void>}
446
+ */
447
+ sleep(ms) {
448
+ return new Promise((resolve) => setTimeout(resolve, ms));
449
+ }
450
+
451
+ /**
452
+ * Clear queued requests for a specific route
453
+ * @private
454
+ * @param {string} routeKey - The route key to clear requests for
455
+ * @returns {number} Number of requests cleared
456
+ */
457
+ clearQueuedRequestsForRoute(routeKey) {
458
+ const initialLength = this.requestQueue.length;
459
+ this.requestQueue = this.requestQueue.filter((request) => {
460
+ const requestRouteKey = this.getRouteKey(
461
+ request.endpoint,
462
+ request.options.method || "GET",
463
+ );
464
+ return requestRouteKey !== routeKey;
465
+ });
466
+ return initialLength - this.requestQueue.length;
261
467
  }
262
468
 
263
469
  /**
@@ -325,6 +531,81 @@ class RestManager {
325
531
  return await sendMessage(this, channelId, payload);
326
532
  }
327
533
 
534
+ /**
535
+ * Fetch a user by ID
536
+ * @param {string} userId
537
+ * @returns {Promise<object>} User data
538
+ */
539
+ async fetchUser(userId) {
540
+ return await fetchUser(this, userId);
541
+ }
542
+
543
+ /**
544
+ * Create a DM channel with a user
545
+ * @param {string} recipientId
546
+ * @returns {Promise<object>} Channel data
547
+ */
548
+ async createDM(recipientId) {
549
+ return await createDM(this, recipientId);
550
+ }
551
+
552
+ /**
553
+ * Fetch webhooks for a channel
554
+ * @param {string} channelId - The channel ID
555
+ * @returns {Promise<Array>} Webhook data array
556
+ */
557
+ async fetchWebhooks(channelId) {
558
+ return await this.request(`/channels/${channelId}/webhooks`);
559
+ }
560
+
561
+ /**
562
+ * Create a webhook in a channel
563
+ * @param {string} channelId - The channel ID
564
+ * @param {string} name - Webhook name
565
+ * @param {object} [options={}] - Webhook options
566
+ * @param {string} [options.avatar] - Webhook avatar payload
567
+ * @param {string} [options.reason] - Audit log reason
568
+ * @returns {Promise<object>} Created webhook data
569
+ */
570
+ async createWebhook(channelId, name, options = {}) {
571
+ const headers = {};
572
+ if (options.reason) {
573
+ headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
574
+ }
575
+
576
+ const body = { name };
577
+ const avatar = options.avatar || options.avatarURL;
578
+ if (avatar) {
579
+ body.avatar = avatar;
580
+ }
581
+
582
+ return await this.request(`/channels/${channelId}/webhooks`, {
583
+ method: "POST",
584
+ headers,
585
+ body: JSON.stringify(body),
586
+ });
587
+ }
588
+
589
+ /**
590
+ * Edit a message in a channel
591
+ * @param {string} channelId - The channel ID
592
+ * @param {string} messageId - The message ID
593
+ * @param {string|object} payload - New message content or payload object
594
+ * @returns {Promise<object>} The updated message data
595
+ */
596
+ async editMessage(channelId, messageId, payload) {
597
+ const updatedData = await this.request(
598
+ `/channels/${channelId}/messages/${messageId}`,
599
+ {
600
+ method: "PATCH",
601
+ body: JSON.stringify(
602
+ typeof payload === "string" ? { content: payload } : payload,
603
+ ),
604
+ },
605
+ );
606
+ return updatedData;
607
+ }
608
+
328
609
  /**
329
610
  * React to a message
330
611
  * @param {string} channelId - The channel ID
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Create a DM channel with a recipient
3
+ * @param {RestManager} rest
4
+ * @param {string} recipientId
5
+ */
6
+ async function createDM(rest, recipientId) {
7
+ return await rest.request(`/users/@me/channels`, {
8
+ method: "POST",
9
+ body: JSON.stringify({ recipient_id: recipientId }),
10
+ });
11
+ }
12
+
13
+ module.exports = createDM;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Fetch a user by ID
3
+ * @param {RestManager} rest
4
+ * @param {string} userId
5
+ */
6
+ async function fetchUser(rest, userId) {
7
+ return await rest.request(`/users/${userId}`, {
8
+ method: "GET",
9
+ });
10
+ }
11
+
12
+ module.exports = fetchUser;
@@ -3,7 +3,7 @@ const { EventEmitter } = require("events");
3
3
  const Message = require("../../classes/Message");
4
4
  const Guild = require("../../classes/Guild");
5
5
  const Channel = require("../../classes/Channel");
6
- const User = require("../../classes/User");
6
+ const ClientUser = require("../../classes/ClientUser");
7
7
  const WebSocketError = require("../../classes/WebSocketError");
8
8
 
9
9
  class DiscordWebSocket extends EventEmitter {
@@ -81,8 +81,7 @@ class DiscordWebSocket extends EventEmitter {
81
81
  this.startHeartbeat(message.d.heartbeat_interval);
82
82
  this.sendIdentify();
83
83
  break;
84
- case 11: // Heartbeat ACK
85
- // Heartbeat acknowledged
84
+ case 11: // Heartbeat acknowledged
86
85
  break;
87
86
  case 0: // Dispatch
88
87
  this.handleDispatch(message);
@@ -130,7 +129,7 @@ class DiscordWebSocket extends EventEmitter {
130
129
  case "READY":
131
130
  this.sessionId = message.d.session_id;
132
131
  this.client.sessionId = message.d.session_id;
133
- this.client.user = new User(this.client, message.d.user);
132
+ this.client.user = new ClientUser(this.client, message.d.user);
134
133
 
135
134
  // Process guilds from READY payload
136
135
  if (message.d.guilds) {