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 +1 -1
- package/src/classes/Channel.js +43 -2
- package/src/classes/Client.js +29 -2
- package/src/classes/ClientUser.js +71 -0
- package/src/classes/DiscordAPIError.js +3 -1
- package/src/classes/Message.js +84 -0
- package/src/classes/User.js +24 -52
- package/src/connection/rest/RestManager.js +304 -23
- package/src/connection/rest/methods/createDM.js +13 -0
- package/src/connection/rest/methods/fetchUser.js +12 -0
- package/src/connection/ws/WebSocket.js +3 -4
package/package.json
CHANGED
package/src/classes/Channel.js
CHANGED
|
@@ -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<
|
|
54
|
+
* @returns {Promise<Message>} The sent message object
|
|
54
55
|
*/
|
|
55
56
|
async send(payload) {
|
|
56
|
-
|
|
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
|
package/src/classes/Client.js
CHANGED
|
@@ -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<
|
|
94
|
+
* @returns {Promise<Message>} The sent message object
|
|
93
95
|
*/
|
|
94
96
|
async sendMessage(channelId, content) {
|
|
95
|
-
|
|
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
|
|
package/src/classes/Message.js
CHANGED
|
@@ -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;
|
package/src/classes/User.js
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
* @
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
*
|
|
56
|
-
* @
|
|
57
|
-
* @returns {void}
|
|
37
|
+
* Get existing DM channel or create one if needed
|
|
38
|
+
* @returns {Promise<Channel>} The DM channel instance
|
|
58
39
|
*/
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
70
|
-
* @param {object}
|
|
71
|
-
* @
|
|
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
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
+
`⏸️ Suspending route ${routeKey} for 2 minutes (queue processing)`,
|
|
95
196
|
);
|
|
96
|
-
await this.sleep(delay);
|
|
97
197
|
|
|
98
|
-
//
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
//
|
|
111
|
-
await this.sleep(
|
|
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
|
|
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}
|
|
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
|
-
//
|
|
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() +
|
|
168
|
-
limit: this.rateLimits.get(routeKey)?.limit || 5,
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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(
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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) {
|