discord-self-lite 0.1.10 → 0.1.12

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.10",
3
+ "version": "0.1.12",
4
4
  "description": "Lightweight Discord selfbot library",
5
5
  "keywords": [
6
6
  "discord",
@@ -55,6 +55,7 @@ class Channel {
55
55
  */
56
56
  async send(payload) {
57
57
  const data = await this.rest.sendMessage(this.id, payload);
58
+ if (!data) return null;
58
59
  return new Message(this.client, data);
59
60
  }
60
61
 
@@ -134,7 +135,7 @@ class Channel {
134
135
  * @returns {Guild|null} The guild instance or null if not a guild channel
135
136
  */
136
137
  getGuild() {
137
- const guildId = this.data.guild_id;
138
+ const guildId = this.guildId;
138
139
  if (!guildId) return null;
139
140
  return this.client.getGuild(guildId);
140
141
  }
@@ -144,7 +145,7 @@ class Channel {
144
145
  * @returns {Promise<Guild|null>} The guild instance or null if not a guild channel
145
146
  */
146
147
  async fetchGuild() {
147
- const guildId = this.data.guild_id;
148
+ const guildId = this.guildId;
148
149
  if (!guildId) return null;
149
150
  return await this.client.fetchGuild(guildId);
150
151
  }
@@ -213,7 +214,7 @@ class Channel {
213
214
  if (this.isDM() || this.isGroupDM()) {
214
215
  return `https://discord.com/channels/@me/${this.id}`;
215
216
  }
216
- const guildId = this.data.guild_id;
217
+ const guildId = this.guildId;
217
218
  return `https://discord.com/channels/${guildId}/${this.id}`;
218
219
  }
219
220
 
@@ -234,8 +235,8 @@ class Channel {
234
235
  }
235
236
 
236
237
  // Apply channel permission overwrites
237
- if (this.data.permission_overwrites) {
238
- const overwrites = this.data.permission_overwrites;
238
+ if (this.permissionOverwrites) {
239
+ const overwrites = this.permissionOverwrites;
239
240
 
240
241
  // Role overwrites (deny takes precedence over allow)
241
242
  const memberRoles = member.roles || [];
@@ -244,7 +245,7 @@ class Channel {
244
245
  // Role overwrite
245
246
  if (
246
247
  memberRoles.includes(overwrite.id) ||
247
- overwrite.id === this.data.guild_id
248
+ overwrite.id === this.guildId
248
249
  ) {
249
250
  permissions &= ~toBigInt(overwrite.deny || 0);
250
251
  permissions |= toBigInt(overwrite.allow || 0);
@@ -49,7 +49,27 @@ class Client extends EventEmitter {
49
49
  async login(token) {
50
50
  this.token = token;
51
51
  this.rest = new RestManager(this.token, this.options.apiVersion || 9);
52
- await this.connect();
52
+
53
+ return new Promise((resolve, reject) => {
54
+ const onReady = () => {
55
+ this.removeListener("error", onError);
56
+ resolve();
57
+ };
58
+
59
+ const onError = (msg) => {
60
+ this.removeListener("ready", onReady);
61
+ reject(new Error(msg));
62
+ };
63
+
64
+ this.once("ready", onReady);
65
+ this.once("error", onError);
66
+
67
+ this.connect().catch((err) => {
68
+ this.removeListener("ready", onReady);
69
+ this.removeListener("error", onError);
70
+ reject(err);
71
+ });
72
+ });
53
73
  }
54
74
 
55
75
  /**
@@ -105,10 +105,10 @@ class Guild {
105
105
  * @returns {string|null} The icon URL or null if no icon
106
106
  */
107
107
  getIconURL(options = {}) {
108
- if (!this.data.icon) return null;
108
+ if (!this.icon) return null;
109
109
  const size = options.size || 512;
110
110
  const format = options.format || "png";
111
- return `https://cdn.discordapp.com/icons/${this.id}/${this.data.icon}.${format}?size=${size}`;
111
+ return `https://cdn.discordapp.com/icons/${this.id}/${this.icon}.${format}?size=${size}`;
112
112
  }
113
113
 
114
114
  /**
@@ -119,10 +119,10 @@ class Guild {
119
119
  * @returns {string|null} The banner URL or null if no banner
120
120
  */
121
121
  getBannerURL(options = {}) {
122
- if (!this.data.banner) return null;
122
+ if (!this.banner) return null;
123
123
  const size = options.size || 512;
124
124
  const format = options.format || "png";
125
- return `https://cdn.discordapp.com/banners/${this.id}/${this.data.banner}.${format}?size=${size}`;
125
+ return `https://cdn.discordapp.com/banners/${this.id}/${this.banner}.${format}?size=${size}`;
126
126
  }
127
127
 
128
128
  /**
@@ -1,6 +1,21 @@
1
- const WebSocketError = require("./WebSocketError");
2
1
  const User = require("./User");
3
2
 
3
+ function camelCaseKeys(obj) {
4
+ if (Array.isArray(obj)) {
5
+ return obj.map((item) => camelCaseKeys(item));
6
+ } else if (obj !== null && typeof obj === "object") {
7
+ const newObj = {};
8
+ for (const [key, value] of Object.entries(obj)) {
9
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
10
+ letter.toUpperCase(),
11
+ );
12
+ newObj[camelKey] = camelCaseKeys(value);
13
+ }
14
+ return newObj;
15
+ }
16
+ return obj;
17
+ }
18
+
4
19
  /**
5
20
  * Represents a Discord message
6
21
  */
@@ -32,12 +47,13 @@ class Message {
32
47
  }
33
48
  }
34
49
 
35
- this.components = this.components || [];
50
+ this.components = camelCaseKeys(this.components || []);
36
51
  this.attachments = this.attachments || [];
37
- this.embeds = this.embeds || [];
52
+ this.embeds = camelCaseKeys(this.embeds || []);
38
53
  this.mentions = this.mentions || [];
39
54
  this.mentionRoles = this.mentionRoles || [];
40
55
  this.reactions = this.reactions || [];
56
+ this.url = `https://discord.com/channels/${this.guildId ? this.guildId : "@me"}/${this.channelId}/${this.id}`;
41
57
  }
42
58
 
43
59
  /**
@@ -65,9 +81,9 @@ class Message {
65
81
  this[camelKey] = value;
66
82
  }
67
83
 
68
- this.components = this.components || [];
84
+ this.components = camelCaseKeys(this.components || []);
69
85
  this.attachments = this.attachments || [];
70
- this.embeds = this.embeds || [];
86
+ this.embeds = camelCaseKeys(this.embeds || []);
71
87
  this.mentions = this.mentions || [];
72
88
  this.mentionRoles = this.mentionRoles || [];
73
89
  this.reactions = this.reactions || [];
@@ -117,11 +133,11 @@ class Message {
117
133
 
118
134
  // Get all buttons from components
119
135
  const buttons = [];
120
- if (this.data.components && Array.isArray(this.data.components)) {
121
- for (const row of this.data.components) {
136
+ if (this.components && Array.isArray(this.components)) {
137
+ for (const row of this.components) {
122
138
  if (row.components && Array.isArray(row.components)) {
123
139
  for (const component of row.components) {
124
- if (component.type === 2 && component.custom_id) {
140
+ if (component.type === 2 && component.customId) {
125
141
  // Type 2 is button
126
142
  buttons.push(component);
127
143
  }
@@ -137,7 +153,7 @@ class Message {
137
153
  // Handle different input types
138
154
  if (input === null) {
139
155
  // No input: click first button
140
- customId = buttons[0].custom_id;
156
+ customId = buttons[0].customId;
141
157
  } else if (typeof input === "number") {
142
158
  // Integer input: click button at that index
143
159
  if (input < 0 || input >= buttons.length) {
@@ -147,21 +163,21 @@ class Message {
147
163
  } button(s) (0-${buttons.length - 1})`,
148
164
  );
149
165
  }
150
- customId = buttons[input].custom_id;
166
+ customId = buttons[input].customId;
151
167
  } else if (typeof input === "string") {
152
- // String input: use as custom_id directly (preserving original behavior)
168
+ // String input: use as customId directly
153
169
  customId = input;
154
170
  } else {
155
171
  throw new Error("Invalid input type. Expected null, number, or string.");
156
172
  }
157
173
 
158
174
  // Get required data for button interaction
159
- const applicationId = this.data.application_id || this.author.id;
160
- const messageFlags = this.data.flags || 0;
175
+ const applicationId = this.applicationId || this.author.id;
176
+ const messageFlags = this.flags || 0;
161
177
 
162
178
  if (!this.client.sessionId) {
163
- throw new WebSocketError(
164
- "No session ID available - client not properly connected",
179
+ throw new Error(
180
+ "No session ID available - client not properly connected (yet).",
165
181
  );
166
182
  }
167
183
 
@@ -197,7 +213,7 @@ class Message {
197
213
  * Returns the referenced `Message` instance or `null` if not available.
198
214
  */
199
215
  async fetchReference() {
200
- const ref = this.messageReference || this.data?.message_reference;
216
+ const ref = this.data?.message_reference;
201
217
  if (!ref) return null;
202
218
 
203
219
  const messageId = ref.messageId || ref.message_id;
@@ -207,13 +223,15 @@ class Message {
207
223
  let channel;
208
224
  try {
209
225
  channel = await this.client.resolveChannel(channelId);
210
- } catch {
226
+ } catch (error) {
227
+ console.error(error);
211
228
  return null;
212
229
  }
213
230
 
214
231
  try {
215
232
  return await channel.fetchMessage(messageId);
216
- } catch {
233
+ } catch (error) {
234
+ console.error(error);
217
235
  return null;
218
236
  }
219
237
  }
@@ -74,7 +74,7 @@ class RestManager {
74
74
  });
75
75
 
76
76
  console.log(
77
- `⏸️ Suspending route ${routeKey} for 2 minutes (pre-emptive)`,
77
+ `⏸️ Suspending route ${routeKey} for 2 minutes (pre-emptive)`,
78
78
  );
79
79
 
80
80
  // Clear queued requests for this route
@@ -192,7 +192,7 @@ class RestManager {
192
192
  });
193
193
 
194
194
  console.log(
195
- `⏸️ Suspending route ${routeKey} for 2 minutes (queue processing)`,
195
+ `⏸️ Suspending route ${routeKey} for 2 minutes (queue processing)`,
196
196
  );
197
197
 
198
198
  // Clear remaining queued requests for this route
@@ -267,15 +267,14 @@ class RestManager {
267
267
  `⏳ Global rate limit set, waiting ${retryAfter + 2000}ms`,
268
268
  );
269
269
  } else {
270
- // Immediately suspend the route for 2 minutes on first 429
271
- const suspensionTime = 2 * 60 * 1000;
270
+ const suspensionTime = retryAfter + 500;
272
271
  this.suspendedRoutes.set(routeKey, {
273
272
  suspendedUntil: Date.now() + suspensionTime,
274
273
  reason: `429 response on attempt ${attempt + 1}`,
275
274
  });
276
275
 
277
276
  console.log(
278
- `⏸️ Suspending route ${routeKey} for 2 minutes (429 response)`,
277
+ `⏸️ Suspending route ${routeKey} for ${suspensionTime}ms (429 response)`,
279
278
  );
280
279
 
281
280
  // Clear queue for this route
@@ -353,6 +352,15 @@ class RestManager {
353
352
 
354
353
  return await response.json();
355
354
  } catch (error) {
355
+ // Do not retry on Missing Permissions (50013)
356
+ if (
357
+ error.name === "DiscordAPIError" &&
358
+ error.fullError &&
359
+ error.fullError.code === 50013
360
+ ) {
361
+ throw error;
362
+ }
363
+
356
364
  if (attempt === maxRetries - 1) {
357
365
  throw error;
358
366
  }
@@ -672,7 +680,19 @@ class RestManager {
672
680
  * @returns {Promise<object>} Message data
673
681
  */
674
682
  async fetchMessage(channelId, messageId) {
675
- return await this.request(`/channels/${channelId}/messages/${messageId}`);
683
+ const messages = await this.request(
684
+ `/channels/${channelId}/messages?limit=1&around=${messageId}`,
685
+ );
686
+ if (Array.isArray(messages)) {
687
+ const message = messages.find((m) => m.id === messageId);
688
+ if (message) return message;
689
+ }
690
+ throw new DiscordAPIError(
691
+ "Unknown Message",
692
+ 404,
693
+ `/channels/${channelId}/messages/${messageId}`,
694
+ { message: "Unknown Message", code: 10008 },
695
+ );
676
696
  }
677
697
 
678
698
  /**
@@ -4,8 +4,6 @@ const Message = require("../../classes/Message");
4
4
  const Guild = require("../../classes/Guild");
5
5
  const Channel = require("../../classes/Channel");
6
6
  const ClientUser = require("../../classes/ClientUser");
7
- const WebSocketError = require("../../classes/WebSocketError");
8
-
9
7
  class DiscordWebSocket extends EventEmitter {
10
8
  constructor(client, token, options = {}) {
11
9
  super();
@@ -31,7 +29,7 @@ class DiscordWebSocket extends EventEmitter {
31
29
  this.ws = new WebSocket(this.gatewayUrl);
32
30
 
33
31
  this.ws.on("open", () => {
34
- console.log("WebSocket connected");
32
+ this.client.emit("debug", "WebSocket connected");
35
33
  this.client.emit("connected");
36
34
  });
37
35
 
@@ -40,23 +38,47 @@ class DiscordWebSocket extends EventEmitter {
40
38
  });
41
39
 
42
40
  this.ws.on("close", (code, reason) => {
43
- console.log(`WebSocket closed: ${code} - ${reason}`);
41
+ this.client.emit("debug", `WebSocket closed: ${code} - ${reason}`);
44
42
  this.ready = false;
45
43
  if (this.heartbeatInterval) {
46
44
  clearInterval(this.heartbeatInterval);
47
45
  this.heartbeatInterval = null;
48
46
  }
49
47
  this.client.emit("disconnected", code, reason);
48
+ const terminalCodes = [4004, 4010, 4011, 4012, 4013, 4014];
49
+
50
+ if (terminalCodes.includes(code)) {
51
+ if (code === 4004) {
52
+ try {
53
+ if (this.ws) this.ws.terminate();
54
+ } catch {
55
+ /* ignore */
56
+ }
57
+ this.ws = null;
58
+ this.client.emit(
59
+ "error",
60
+ `Authentication failed (close code ${code}). Please check your token and try again.`,
61
+ );
62
+ return;
63
+ }
64
+
65
+ this.client.emit(
66
+ "debug",
67
+ `Terminal error received (${code}). Connection will not be restarted. (Reason: ${reason})`,
68
+ );
69
+ return;
70
+ }
71
+
50
72
  if (code !== 1000) {
51
- // Not a clean close
52
73
  this.reconnect();
53
74
  }
54
75
  });
55
76
 
56
77
  this.ws.on("error", (error) => {
57
- const wsError = new WebSocketError(`WebSocket error: ${error.message}`);
58
- console.error(wsError);
59
- this.client.emit("error", wsError);
78
+ this.client.emit(
79
+ "error",
80
+ `WebSocket error: ${error && error.message ? error.message : String(error)}`,
81
+ );
60
82
  });
61
83
  }
62
84
 
@@ -87,7 +109,7 @@ class DiscordWebSocket extends EventEmitter {
87
109
  this.handleDispatch(message);
88
110
  break;
89
111
  default:
90
- console.log("Unhandled op:", message.op);
112
+ this.client.emit("debug", `Unhandled op: ${message.op}`);
91
113
  }
92
114
  }
93
115
 
@@ -185,9 +207,9 @@ class DiscordWebSocket extends EventEmitter {
185
207
  const member = new GuildMember(this.client, memberData, guild);
186
208
  guild._members.set(this.client.user.id, member);
187
209
  } catch (err) {
188
- console.warn(
189
- `Failed to fetch member for guild ${guild.id}:`,
190
- err.message,
210
+ this.client.emit(
211
+ "debug",
212
+ `Failed to fetch member for guild ${guild.id}: ${err && err.message ? err.message : String(err)}`,
191
213
  );
192
214
  }
193
215
  }
@@ -197,16 +219,14 @@ class DiscordWebSocket extends EventEmitter {
197
219
 
198
220
  reconnect() {
199
221
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
200
- const err = new WebSocketError("Max reconnect attempts reached");
201
- console.error(err);
202
- this.client.emit("error", err);
222
+ this.client.emit("error", "Max reconnect attempts reached");
203
223
  this.client.emit("maxReconnects");
204
224
  return;
205
225
  }
206
226
 
207
227
  this.reconnectAttempts++;
208
228
  const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); // Exponential backoff
209
- console.log(`Reconnecting in ${delay}ms...`);
229
+ this.client.emit("debug", `Reconnecting in ${delay}ms...`);
210
230
  setTimeout(() => {
211
231
  this.connect();
212
232
  }, delay);
@@ -1,18 +0,0 @@
1
- /**
2
- * Represents an error related to the WebSocket connection
3
- * @extends Error
4
- */
5
- class WebSocketError extends Error {
6
- /**
7
- * Create a new WebSocketError
8
- * @param {string} message - The error message
9
- * @param {number} [code] - The WebSocket close code
10
- */
11
- constructor(message, code) {
12
- super(message);
13
- this.name = "WebSocketError";
14
- this.code = code;
15
- }
16
- }
17
-
18
- module.exports = WebSocketError;