discord-self-lite 0.1.8 → 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.8",
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
  /**
@@ -26,6 +28,9 @@ class RestManager {
26
28
 
27
29
  // Circuit breaker state for heavily rate-limited routes
28
30
  this.circuitBreakers = new Map(); // routeKey -> { failures, lastFailure, openUntil }
31
+
32
+ // Suspended routes - temporarily disabled endpoints
33
+ this.suspendedRoutes = new Map(); // routeKey -> { suspendedUntil, reason }
29
34
  }
30
35
 
31
36
  /**
@@ -44,45 +49,53 @@ class RestManager {
44
49
  const circuitBreaker = this.circuitBreakers.get(routeKey);
45
50
  if (circuitBreaker && circuitBreaker.openUntil > Date.now()) {
46
51
  const remainingTime = circuitBreaker.openUntil - Date.now();
47
- throw new Error(
48
- `Circuit breaker open for ${routeKey}. Retry in ${remainingTime}ms`,
52
+ console.log(
53
+ `🚫 Request dropped: Circuit breaker open for ${routeKey} (${Math.round(remainingTime / 1000)}s remaining)`,
49
54
  );
55
+ return Promise.resolve(null);
50
56
  }
51
57
 
52
- if (this.isRateLimited(endpoint, method)) {
53
- const rateLimit = this.rateLimits.get(routeKey);
54
- const globalLimited =
55
- this.globalRateLimit && Date.now() < this.globalRateLimit.reset;
56
-
57
- // Calculate delay with minimum 2s buffer and more conservative timing
58
- const baseDelay = globalLimited
59
- ? this.globalRateLimit.reset - Date.now()
60
- : rateLimit
61
- ? rateLimit.reset - Date.now()
62
- : 1000;
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
+ }
63
67
 
64
- // Add buffer time and ensure minimum delay
65
- const delay = Math.max(baseDelay + 2000, 3000); // Minimum 3s delay
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
+ });
66
75
 
67
76
  console.log(
68
- `🚫 Pre-emptive rate limit wait: ${delay}ms for ${routeKey} (${endpoint})`,
77
+ `⏸️ Suspending route ${routeKey} for 2 minutes (pre-emptive)`,
69
78
  );
70
- await this.sleep(delay);
71
79
 
72
- // Double-check after waiting - if still limited, reject immediately
73
- if (this.isRateLimited(endpoint, method)) {
74
- throw new Error(`Endpoint still rate limited after wait: ${routeKey}`);
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
+ );
75
86
  }
87
+
88
+ console.log(`🚫 Request dropped: Route suspended for ${routeKey}`);
89
+ return Promise.resolve(null);
76
90
  }
77
91
 
78
92
  return new Promise((resolve, reject) => {
79
93
  // Prevent queue from growing too large during rate limit storms
80
94
  if (this.requestQueue.length >= 50) {
81
- reject(
82
- new Error(
83
- `Request queue full (${this.requestQueue.length} requests). Rate limiting too aggressive.`,
84
- ),
95
+ console.log(
96
+ `🚫 Request dropped: Queue full (${this.requestQueue.length} requests)`,
85
97
  );
98
+ resolve(null);
86
99
  return;
87
100
  }
88
101
 
@@ -119,6 +132,26 @@ class RestManager {
119
132
 
120
133
  this.processingQueue = true;
121
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
+
122
155
  while (this.requestQueue.length > 0) {
123
156
  // Double-check global rate limit for each request
124
157
  if (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
@@ -132,34 +165,52 @@ class RestManager {
132
165
  const { endpoint, options, resolve, reject } = request;
133
166
 
134
167
  try {
135
- // Check endpoint-specific rate limit
136
168
  const routeKey = this.getRouteKey(endpoint, options.method || "GET");
137
- const rateLimit = this.rateLimits.get(routeKey);
138
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);
139
182
  if (
140
183
  rateLimit &&
141
184
  rateLimit.remaining <= 0 &&
142
185
  Date.now() < rateLimit.reset
143
186
  ) {
144
- 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
+
145
194
  console.log(
146
- `⏳ Route rate limit for ${routeKey} (${endpoint}), waiting ${delay}ms`,
195
+ `⏸️ Suspending route ${routeKey} for 2 minutes (queue processing)`,
147
196
  );
148
- await this.sleep(delay);
149
197
 
150
- // After waiting, check if we still have remaining requests
151
- const updatedRateLimit = this.rateLimits.get(routeKey);
152
- if (updatedRateLimit && updatedRateLimit.remaining <= 0) {
153
- // Still rate limited, put request back at front of queue
154
- this.requestQueue.unshift(request);
155
- 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
+ );
156
204
  }
205
+
206
+ resolve(null);
207
+ continue;
157
208
  }
158
209
 
159
210
  const result = await this.makeRequest(endpoint, options);
160
211
  resolve(result);
161
212
 
162
- // Increased delay between requests to be more conservative with rate limits
213
+ // Delay between requests to be conservative with rate limits
163
214
  await this.sleep(500);
164
215
  } catch (error) {
165
216
  reject(error);
@@ -199,22 +250,51 @@ class RestManager {
199
250
  // Handle rate limiting
200
251
  if (response.status === 429) {
201
252
  const retryAfter =
202
- parseInt(response.headers.get("retry-after")) * 1000 + 1000;
253
+ parseInt(response.headers.get("retry-after")) * 1000 || 2000;
203
254
  const isGlobal =
204
255
  response.headers.get("x-ratelimit-global") === "true";
205
256
  const routeKey = this.getRouteKey(endpoint, options.method || "GET");
206
257
 
207
- // Exponential backoff: increase delay based on attempt number
208
- const backoffDelay = retryAfter * Math.pow(1.5, attempt);
209
-
210
258
  console.log(
211
259
  `🚫 Rate limited! ${
212
260
  isGlobal ? "Global" : `Route (${routeKey})`
213
- } limit for ${endpoint}, retry after ${backoffDelay}ms (attempt ${attempt + 1})`,
261
+ } limit for ${endpoint}`,
214
262
  );
215
263
 
216
- // Circuit breaker: track consecutive failures
217
- if (!isGlobal) {
264
+ if (isGlobal) {
265
+ this.globalRateLimit = { reset: Date.now() + retryAfter + 2000 };
266
+ console.log(
267
+ `⏳ Global rate limit set, waiting ${retryAfter + 2000}ms`,
268
+ );
269
+ } else {
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
290
+ this.rateLimits.set(routeKey, {
291
+ remaining: 0,
292
+ reset: Date.now() + suspensionTime,
293
+ limit: this.rateLimits.get(routeKey)?.limit || 5,
294
+ updatedAt: Date.now(),
295
+ });
296
+
297
+ // Open circuit breaker after 2 consecutive 429s
218
298
  const breaker = this.circuitBreakers.get(routeKey) || {
219
299
  failures: 0,
220
300
  lastFailure: 0,
@@ -223,55 +303,53 @@ class RestManager {
223
303
  breaker.failures++;
224
304
  breaker.lastFailure = Date.now();
225
305
 
226
- // Open circuit breaker after 3 consecutive failures within 30 seconds
227
- if (
228
- breaker.failures >= 3 &&
229
- Date.now() - breaker.lastFailure < 30000
230
- ) {
231
- breaker.openUntil = Date.now() + 60000; // Open for 60 seconds
232
- console.log(
233
- `🔌 Circuit breaker OPENED for ${routeKey} - too many rate limits`,
234
- );
306
+ if (breaker.failures >= 2) {
307
+ breaker.openUntil = Date.now() + suspensionTime;
308
+ console.log(`🔌 Circuit breaker OPENED for ${routeKey}`);
235
309
  }
236
310
 
237
311
  this.circuitBreakers.set(routeKey, breaker);
312
+
313
+ // Don't retry, return null immediately
314
+ return null;
238
315
  }
239
316
 
317
+ // For global rate limits, wait and retry
240
318
  if (isGlobal) {
241
- this.globalRateLimit = { reset: Date.now() + backoffDelay };
242
- } else {
243
- // Update route-specific rate limit for 429 responses
244
- this.rateLimits.set(routeKey, {
245
- remaining: 0,
246
- reset: Date.now() + backoffDelay,
247
- limit: this.rateLimits.get(routeKey)?.limit || 5, // Default limit if unknown
248
- updatedAt: Date.now(),
249
- });
319
+ await this.sleep(retryAfter + 2000);
320
+ attempt++;
321
+ continue;
250
322
  }
251
323
 
252
- await this.sleep(backoffDelay);
253
- attempt++;
254
- continue;
324
+ // For route rate limits, don't retry
325
+ return null;
255
326
  }
256
327
 
257
328
  if (!response.ok) {
258
329
  const error = await response
259
330
  .json()
260
331
  .catch(() => ({ message: "Unknown error" }));
261
- throw new DiscordAPIError(error.message, response.status, endpoint);
332
+ throw new DiscordAPIError(
333
+ error.message,
334
+ response.status,
335
+ endpoint,
336
+ error,
337
+ );
262
338
  }
263
339
 
264
340
  // Handle 204 No Content (successful but no body)
265
341
  if (response.status === 204) {
266
- // Reset circuit breaker on successful response
342
+ // Reset circuit breaker and suspension on successful response
267
343
  const routeKey = this.getRouteKey(endpoint, options.method || "GET");
268
344
  this.circuitBreakers.delete(routeKey);
345
+ this.suspendedRoutes.delete(routeKey);
269
346
  return null;
270
347
  }
271
348
 
272
- // Reset circuit breaker on successful response
349
+ // Reset circuit breaker and suspension on successful response
273
350
  const routeKey = this.getRouteKey(endpoint, options.method || "GET");
274
351
  this.circuitBreakers.delete(routeKey);
352
+ this.suspendedRoutes.delete(routeKey);
275
353
 
276
354
  return await response.json();
277
355
  } catch (error) {
@@ -351,6 +429,13 @@ class RestManager {
351
429
  this.circuitBreakers.delete(key);
352
430
  }
353
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
+ }
354
439
  }
355
440
 
356
441
  /**
@@ -363,6 +448,24 @@ class RestManager {
363
448
  return new Promise((resolve) => setTimeout(resolve, ms));
364
449
  }
365
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;
467
+ }
468
+
366
469
  /**
367
470
  * Check if a route is currently rate limited
368
471
  * @param {string} endpoint - The API endpoint
@@ -428,6 +531,81 @@ class RestManager {
428
531
  return await sendMessage(this, channelId, payload);
429
532
  }
430
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
+
431
609
  /**
432
610
  * React to a message
433
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) {