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 +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 +245 -67
- 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
|
/**
|
|
@@ -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
|
-
|
|
48
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
77
|
+
`⏸️ Suspending route ${routeKey} for 2 minutes (pre-emptive)`,
|
|
69
78
|
);
|
|
70
|
-
await this.sleep(delay);
|
|
71
79
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
+
`⏸️ Suspending route ${routeKey} for 2 minutes (queue processing)`,
|
|
147
196
|
);
|
|
148
|
-
await this.sleep(delay);
|
|
149
197
|
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
//
|
|
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
|
|
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}
|
|
261
|
+
} limit for ${endpoint}`,
|
|
214
262
|
);
|
|
215
263
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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(
|
|
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;
|
|
@@ -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) {
|