beniocord.js 2.0.7 → 2.0.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/Client.js CHANGED
@@ -23,6 +23,7 @@ let global = {
23
23
  token: "",
24
24
  apiUrl: "https://api-bots.beniocord.site"
25
25
  };
26
+
26
27
  /**
27
28
  * @fires Client#ready
28
29
  * @fires Client#messageCreate
@@ -40,14 +41,17 @@ class Client extends EventEmitter {
40
41
  throw new ClientError("Valid token is required", "INVALID_TOKEN");
41
42
  }
42
43
 
43
- // this.token = token.trim();
44
+ // Global configuration
44
45
  global.token = token.trim();
45
- // this.apiUrl = "https://api-bots.beniocord.site";
46
+
47
+ // Client state
46
48
  this.socket = null;
47
49
  this.user = null;
48
50
  this.isConnected = false;
49
51
  this.isReady = false;
52
+ this.status = 'online';
50
53
 
54
+ // Configuration options
51
55
  this.config = {
52
56
  connectionTimeout: 15000,
53
57
  requestTimeout: 10000,
@@ -56,18 +60,21 @@ class Client extends EventEmitter {
56
60
  };
57
61
 
58
62
  this.retryCount = 0;
63
+ this.heartbeatInterval = null;
59
64
 
65
+ // Cache system
60
66
  this.cache = {
61
67
  users: new Map(),
62
68
  channels: new Map(),
63
69
  messages: new Map(),
64
70
  emojis: new Map(),
65
71
  stickers: new Map(),
66
- presence: new Map(),
67
72
  };
68
73
 
74
+ // Track sent messages to avoid duplicates
69
75
  this._sentMessages = new Set();
70
76
 
77
+ // Setup axios instance
71
78
  this._axios = axios.create({
72
79
  baseURL: global.apiUrl,
73
80
  timeout: this.config.requestTimeout,
@@ -84,59 +91,22 @@ class Client extends EventEmitter {
84
91
  error => this._handleAxiosError(error)
85
92
  );
86
93
 
94
+ // Clean up sent messages cache periodically
87
95
  setInterval(() => {
88
96
  if (this._sentMessages.size > 1000) {
89
97
  this._sentMessages.clear();
90
98
  }
91
99
  }, 30 * 60 * 1000);
92
-
93
100
  }
94
101
 
95
- _handleAxiosError(error) {
96
- if (error.response) {
97
- const { status, data } = error.response;
98
- const errorCode = data?.error || 'UNKNOWN_ERROR';
99
- const errorMessage = data?.message || error.message;
100
-
101
- switch (status) {
102
- case 401:
103
- throw new ClientError(
104
- errorMessage || "Invalid or expired token",
105
- errorCode || "UNAUTHORIZED"
106
- );
107
- case 403:
108
- throw new ClientError(
109
- errorMessage || "Token lacks necessary permissions",
110
- errorCode || "FORBIDDEN"
111
- );
112
- case 404:
113
- throw new ClientError(
114
- errorMessage || "Resource not found",
115
- errorCode || "NOT_FOUND"
116
- );
117
- case 429:
118
- throw new ClientError(
119
- errorMessage || "Rate limit exceeded",
120
- errorCode || "RATE_LIMITED"
121
- );
122
- default:
123
- throw new ClientError(
124
- errorMessage || "API request failed",
125
- errorCode
126
- );
127
- }
128
- } else if (error.code === 'ECONNABORTED') {
129
- throw new ClientError("Request timeout", "TIMEOUT");
130
- } else if (error.code === 'ECONNREFUSED') {
131
- throw new ClientError("Cannot connect to API server", "CONNECTION_REFUSED");
132
- } else {
133
- throw new ClientError(
134
- error.message || "Network error",
135
- error.code || "NETWORK_ERROR"
136
- );
137
- }
138
- }
102
+ // ============================================================================
103
+ // PUBLIC API METHODS - Authentication & Connection
104
+ // ============================================================================
139
105
 
106
+ /**
107
+ * Validates the bot token with the API
108
+ * @returns {Promise<Object>} Validation response
109
+ */
140
110
  async validateToken() {
141
111
  try {
142
112
  const response = await this._axios.get('/api/auth/verify');
@@ -149,6 +119,10 @@ class Client extends EventEmitter {
149
119
  }
150
120
  }
151
121
 
122
+ /**
123
+ * Logs in the bot and establishes connection
124
+ * @returns {Promise<User>} The bot user object
125
+ */
152
126
  async login() {
153
127
  try {
154
128
  await this.validateToken();
@@ -176,162 +150,21 @@ class Client extends EventEmitter {
176
150
  }
177
151
  }
178
152
 
179
- async _connectSocket() {
180
- return new Promise((resolve, reject) => {
181
- const connectionTimeout = setTimeout(() => {
182
- if (this.socket) {
183
- this.socket.disconnect();
184
- }
185
- reject(new ClientError(
186
- `Connection timeout - failed to connect within ${this.config.connectionTimeout}ms`,
187
- "CONNECTION_TIMEOUT"
188
- ));
189
- }, this.config.connectionTimeout);
190
-
191
- this.socket = io(global.apiUrl, {
192
- auth: { token: global.token },
193
- extraHeaders: { 'Origin': global.apiUrl },
194
- timeout: 5000,
195
- reconnection: true,
196
- reconnectionDelay: this.config.reconnectionDelay,
197
- reconnectionAttempts: this.config.maxRetries,
198
- transports: ['websocket', 'polling']
199
- });
200
-
201
- this.socket.on("connect", () => {
202
- clearTimeout(connectionTimeout);
203
- this.isConnected = true;
204
- this.retryCount = 0;
205
- this._setupSocketHandlers();
206
- this._startHeartbeat();
207
- resolve();
208
- });
209
-
210
- this.socket.on("disconnect", (reason) => {
211
- this.isConnected = false;
212
- this._stopHeartbeat(); // Para o heartbeat
213
- this.emit("disconnect", reason);
214
-
215
- if (reason === "io server disconnect") {
216
- this.emit("error", new ClientError(
217
- "Disconnected by server - token may be invalid or revoked",
218
- "SERVER_DISCONNECT"
219
- ));
220
- }
221
- });
222
-
223
- this.socket.on("connect_error", (error) => {
224
- clearTimeout(connectionTimeout);
225
- this.retryCount++;
226
-
227
- let errorCode = "CONNECTION_ERROR";
228
- let errorMessage = "Failed to connect to server";
229
-
230
- const errorStr = error.message.toLowerCase();
231
-
232
- if (errorStr.includes("401") || errorStr.includes("unauthorized")) {
233
- errorCode = "UNAUTHORIZED";
234
- errorMessage = "Invalid token";
235
- } else if (errorStr.includes("403") || errorStr.includes("forbidden")) {
236
- errorCode = "FORBIDDEN";
237
- errorMessage = "Token expired or revoked";
238
- } else if (errorStr.includes("timeout")) {
239
- errorCode = "TIMEOUT";
240
- errorMessage = "Connection timeout";
241
- }
242
-
243
- const clientError = new ClientError(errorMessage, errorCode);
244
- this.emit("error", clientError);
245
-
246
- if (this.retryCount >= this.config.maxRetries) {
247
- reject(clientError);
248
- }
249
- });
250
-
251
- this.socket.on("reconnect", (attemptNumber) => {
252
- this.isConnected = true;
253
- this._startHeartbeat();
254
- this.emit("reconnect", attemptNumber);
255
- });
256
-
257
- this.socket.on("reconnect_error", (error) => {
258
- this.emit("reconnectError", error);
259
- });
260
-
261
- this.socket.on("reconnect_failed", () => {
262
- this._stopHeartbeat();
263
- this.emit("error", new ClientError(
264
- "Failed to reconnect after maximum attempts",
265
- "RECONNECT_FAILED"
266
- ));
267
- });
268
- });
269
- }
270
-
271
- async _joinAllChannelRooms() {
272
- try {
273
- const channels = await this.fetchChannels();
274
-
275
- for (const channel of channels) {
276
- if (this.socket && this.socket.connected) {
277
- this.socket.emit('channel:join', { channelId: channel.id });
278
- }
279
- }
280
- } catch (error) {
281
- console.error('Erro ao entrar nas rooms dos canais:', error);
282
- }
283
- }
284
-
285
- /**
286
- * Inicia o sistema de heartbeat
287
- * @private
288
- */
289
- _startHeartbeat() {
290
- // Para qualquer heartbeat existente
291
- this._stopHeartbeat();
292
-
293
- // Intervalo de 30 segundos (mesmo do frontend)
294
- const HEARTBEAT_INTERVAL = 30000;
295
-
296
- this.heartbeatInterval = setInterval(() => {
297
- if (this.socket && this.isConnected) {
298
- // Envia heartbeat simples para bots
299
- // Bots não precisam de isPageVisible/isAppFocused
300
- this.socket.emit("presence:heartbeat", {
301
- status: this.status || "online",
302
- clientType: "bot" // Identifica como bot
303
- });
304
- }
305
- }, HEARTBEAT_INTERVAL);
306
-
307
- // Envia primeiro heartbeat imediatamente
308
- if (this.socket && this.isConnected) {
309
- this.socket.emit("presence:heartbeat", {
310
- status: this.status || "online",
311
- clientType: "bot"
312
- });
313
- }
314
- }
315
-
316
153
  /**
317
- * Para o sistema de heartbeat
318
- * @private
154
+ * Checks if the client is ready and connected
155
+ * @returns {boolean} True if ready and connected
319
156
  */
320
- _stopHeartbeat() {
321
- if (this.heartbeatInterval) {
322
- clearInterval(this.heartbeatInterval);
323
- this.heartbeatInterval = null;
324
- }
157
+ ready() {
158
+ return this.isReady && this.isConnected && this.socket && this.socket.connected;
325
159
  }
326
160
 
327
161
  /**
328
- * Desconecta o cliente e limpa recursos
162
+ * Disconnects the bot from the server
329
163
  */
330
164
  disconnect() {
331
165
  this._stopHeartbeat();
332
166
 
333
167
  if (this.socket) {
334
- // Notifica o servidor antes de desconectar
335
168
  if (this.isConnected) {
336
169
  this.socket.emit("presence:update", {
337
170
  isPageVisible: false,
@@ -358,369 +191,306 @@ class Client extends EventEmitter {
358
191
  this.isReady = false;
359
192
  }
360
193
 
194
+ // ============================================================================
195
+ // PUBLIC API METHODS - User & Bot Status
196
+ // ============================================================================
361
197
 
362
- _setupSocketHandlers() {
363
- this._removeSocketHandlers();
198
+ /**
199
+ * Sets the bot's status
200
+ * @param {string} status - Status: "online", "away", "dnd", "offline"
201
+ */
202
+ async setStatus(status) {
203
+ const validStatuses = ["online", "offline", "away", "dnd"];
364
204
 
365
- this.socket.on('message:new', async (data) => {
366
- try {
367
- if (this._sentMessages.has(data.id)) {
368
- this._sentMessages.delete(data.id);
369
- return;
370
- }
205
+ if (!validStatuses.includes(status)) {
206
+ throw new ClientError(
207
+ `Invalid status. Valid statuses are: ${validStatuses.join(", ")}`,
208
+ "INVALID_STATUS"
209
+ );
210
+ }
371
211
 
372
- const msg = await this._processSocketMessage(data);
373
- this._cacheMessage(msg);
374
- this.emit("messageCreate", msg);
375
- } catch (error) {
376
- this.emit("error", error);
377
- }
378
- });
212
+ this._ensureConnected();
213
+ this.status = status;
214
+ this.socket.emit('status:update', { status });
215
+ }
379
216
 
380
- this.socket.on('message:deleted', (data) => {
381
- const { messageId } = data;
382
- this._markMessageDeleted(messageId);
383
- this.emit('messageDelete', data);
384
- });
217
+ /**
218
+ * Fetches information about the bot user
219
+ * @param {boolean} force - Force fetch from API instead of cache
220
+ * @returns {Promise<User>} Bot user object
221
+ */
222
+ async fetchMe(force = false) {
223
+ if (!force && this.user) {
224
+ return this.user;
225
+ }
385
226
 
386
- this.socket.on('message:edited', (data) => {
387
- const { messageId, content, editedAt } = data;
388
- this._updateMessageContent(messageId, content, editedAt);
389
- this.emit('messageEdit', data);
390
- });
227
+ try {
228
+ const res = await this._axios.get('/api/users/me');
229
+ const user = new User(res.data, this);
230
+ this.cache.users.set(user.id, user);
231
+ this.user = user;
232
+ return user;
233
+ } catch (error) {
234
+ throw error instanceof ClientError
235
+ ? error
236
+ : new ClientError(error.message, "FETCH_ME_ERROR");
237
+ }
238
+ }
391
239
 
392
- this.socket.on('typing:user-start', (data) => {
393
- this.emit('typingStart', data);
394
- });
240
+ /**
241
+ * Fetches a user by ID
242
+ * @param {string} id - User ID
243
+ * @param {boolean} force - Force fetch from API instead of cache
244
+ * @returns {Promise<User>} User object
245
+ */
246
+ async fetchUser(id, force = false) {
247
+ if (!force && this.cache.users.has(id)) {
248
+ return this.cache.users.get(id);
249
+ }
395
250
 
396
- this.socket.on('typing:user-stop', (data) => {
397
- this.emit('typingStop', data);
398
- });
399
-
400
- this.socket.on('user:status-update', (data) => {
401
- this._updateUserStatus(data);
402
- this.emit('userStatusUpdate', data);
403
- });
404
-
405
- this.socket.on('presence:update', (data) => {
406
- this.cache.presence.set(data.userId, data);
407
- this.emit('presenceUpdate', data);
408
- });
409
-
410
- this.socket.on('member:join', async (data) => {
411
- const member = await this.fetchUser(data.memberId).catch(() => null);
412
- const channel = await this.fetchChannel(data.channelId).catch(() => null);
413
-
414
- if (data.addedBy) {
415
- data.addedBy = new User(data.addedBy, this);
416
- }
417
-
418
- data.member = member;
419
- data.channel = channel;
420
-
421
- if (data.memberId === this.user?.id) {
422
- if (channel && !this.cache.channels.has(data.channelId)) {
423
- this.cache.channels.set(data.channelId, channel);
424
- }
425
-
426
- if (this.socket && this.socket.connected) {
427
- this.socket.emit('channel:join', { channelId: data.channelId });
428
- }
429
- }
430
-
431
- if (channel && member) {
432
- if (!channel.members) {
433
- channel.members = new Map();
434
- }
435
- channel.members.set(member.id, member);
436
- }
437
-
438
- this.emit('memberJoin', data);
439
- });
440
-
441
- this.socket.on('member:leave', async (data) => {
442
- const member = await this.fetchUser(data.memberId).catch(() => null);
443
- const channel = await this.fetchChannel(data.channelId).catch(() => null);
444
-
445
- if (data.removedBy) {
446
- data.removedBy = new User(data.removedBy, this);
447
- }
448
-
449
- data.member = member;
450
- data.channel = channel;
451
-
452
- if (data.memberId === this.user?.id) {
453
- this.cache.channels.delete(data.channelId);
454
- if (this.socket && this.socket.connected) {
455
- this.socket.emit('channel:leave', { channelId: data.channelId });
456
- }
457
- }
458
-
459
- if (channel && member && channel.members) {
460
- channel.members.delete(member.id);
461
- }
462
-
463
- this.emit('memberLeave', data);
464
- });
465
-
466
- this.socket.on('channel:update', (data) => {
467
- this._updateChannel(data);
468
- this.emit('channelUpdate', data);
469
- });
470
-
471
- this.socket.on('channel:delete', (data) => {
472
- this.cache.channels.delete(data.channelId);
473
- this.emit('channelDelete', data);
474
- });
475
-
476
- this.socket.on('rate:limited', (data) => {
477
- this.emit('rateLimited', data);
478
- });
479
- }
480
-
481
- _removeSocketHandlers() {
482
- if (!this.socket) return;
483
-
484
- // Remove todos os listeners customizados
485
- this.socket.off("message:new");
486
- this.socket.off("message:deleted");
487
- this.socket.off("message:edited");
488
- this.socket.off("typing:user-start");
489
- this.socket.off("typing:user-stop");
490
- this.socket.off("user:status-update");
491
- this.socket.off("presence:update");
492
- this.socket.off("member:join");
493
- this.socket.off("member:leave");
494
- this.socket.off("channel:update");
495
- this.socket.off("channel:delete");
496
- this.socket.off("rate:limited");
497
- }
498
-
499
- async _processSocketMessage(data) {
500
- const msg = new Message(data, this);
501
-
502
- if (!msg.author && data.user_id) {
503
- msg.author = new User({
504
- id: data.user_id,
505
- username: data.username,
506
- display_name: data.display_name,
507
- avatar_url: data.avatar_url
508
- }, this);
509
- }
510
-
511
- if (!msg.channel && data.channel_id) {
512
- msg.channel = await this.fetchChannel(data.channel_id);
251
+ try {
252
+ const res = await this._axios.get(`/api/users/${id}`);
253
+ const user = new User(res.data, this);
254
+ this.cache.users.set(user.id, user);
255
+ return user;
256
+ } catch (error) {
257
+ throw error instanceof ClientError
258
+ ? error
259
+ : new ClientError(error.message, "FETCH_USER_ERROR");
513
260
  }
514
-
515
- return msg;
516
261
  }
517
262
 
518
- _cacheMessage(msg) {
519
- if (msg.author) {
520
- this._ensureCached(this.cache.users, msg.author.id, msg.author);
521
- }
522
-
523
- if (msg.channel) {
524
- this._ensureCached(this.cache.channels, msg.channel.id, msg.channel);
525
-
526
- if (!this.cache.messages.has(msg.channel.id)) {
527
- this.cache.messages.set(msg.channel.id, []);
528
- }
263
+ // ============================================================================
264
+ // PUBLIC API METHODS - Channels
265
+ // ============================================================================
529
266
 
530
- const messages = this.cache.messages.get(msg.channel.id);
531
- messages.push(msg);
267
+ /**
268
+ * Fetches all available channels
269
+ * @returns {Promise<Channel[]>} Array of channel objects
270
+ */
271
+ async fetchChannels() {
272
+ try {
273
+ const res = await this._axios.get('/api/channels');
274
+ const channels = res.data.map(c => {
275
+ const channel = new Channel(c, this);
276
+ this.cache.channels.set(channel.id, channel);
277
+ return channel;
278
+ });
532
279
 
533
- if (messages.length > 50) {
534
- messages.shift();
535
- }
280
+ return channels;
281
+ } catch (error) {
282
+ throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_CHANNELS_ERROR");
536
283
  }
537
284
  }
538
285
 
539
- _markMessageDeleted(messageId) {
540
- for (const [channelId, messages] of this.cache.messages) {
541
- const msg = messages.find(m => m.id === messageId);
542
- if (msg) {
543
- msg.deleted = true;
544
- break;
545
- }
286
+ /**
287
+ * Fetches a specific channel by ID
288
+ * @param {string} id - Channel ID
289
+ * @param {boolean} force - Force fetch from API instead of cache
290
+ * @returns {Promise<Channel>} Channel object
291
+ */
292
+ async fetchChannel(id, force = false) {
293
+ if (!force && this.cache.channels.has(id)) {
294
+ return this.cache.channels.get(id);
546
295
  }
547
- }
548
296
 
549
- _updateMessageContent(messageId, content, editedAt) {
550
- for (const [channelId, messages] of this.cache.messages) {
551
- const msg = messages.find(m => m.id === messageId);
552
- if (msg) {
553
- msg.content = content;
554
- msg.editedAt = editedAt;
555
- msg.edited = true;
556
- break;
557
- }
297
+ try {
298
+ const res = await this._axios.get(`/api/channels/${id}`);
299
+ const channel = new Channel(res.data, this);
300
+ this.cache.channels.set(channel.id, channel);
301
+ return channel;
302
+ } catch (error) {
303
+ throw error instanceof ClientError
304
+ ? error
305
+ : new ClientError(error.message, "FETCH_CHANNEL_ERROR");
558
306
  }
559
307
  }
560
308
 
561
- _updateUserStatus(data) {
562
- const { userId, status, lastSeen } = data;
563
-
564
- if (this.cache.users.has(userId)) {
565
- const user = this.cache.users.get(userId);
566
- user.status = status;
567
- if (lastSeen) user.lastSeen = lastSeen;
309
+ /**
310
+ * Creates a new channel
311
+ * @param {Object} options - Channel options
312
+ * @param {string} options.name - Channel name
313
+ * @param {string} options.description - Channel description
314
+ * @returns {Promise<Channel>} Created channel object
315
+ */
316
+ async createChannel({ name, description = "" }) {
317
+ if (!name || name.trim() === "") {
318
+ throw new ClientError("Channel name is required", "INVALID_CHANNEL_NAME");
568
319
  }
569
320
 
570
- if (this.cache.presence.has(userId)) {
571
- const presence = this.cache.presence.get(userId);
572
- presence.status = status;
321
+ try {
322
+ const data = {
323
+ name: name.trim(),
324
+ description,
325
+ type: "text"
326
+ };
327
+
328
+ const res = await this._axios.post('/api/channels', data);
329
+ const channel = new Channel(res.data.channel, this);
330
+ this.cache.channels.set(channel.id, channel);
331
+ return channel;
332
+ } catch (error) {
333
+ throw error instanceof ClientError ? error : new ClientError(error.message, "CREATE_CHANNEL_ERROR");
573
334
  }
574
335
  }
575
336
 
576
- _updateChannel(data) {
577
- if (this.cache.channels.has(data.id)) {
578
- const channel = this.cache.channels.get(data.id);
579
- Object.assign(channel, data);
337
+ /**
338
+ * Updates a channel's information
339
+ * @param {string} channelId - Channel ID
340
+ * @param {Object} options - Update options
341
+ * @param {string} options.name - New channel name
342
+ * @param {string} options.description - New channel description
343
+ * @returns {Promise<Channel>} Updated channel object
344
+ */
345
+ async updateChannel(channelId, { name, description }) {
346
+ if (!name && !description) {
347
+ throw new ClientError("At least one field must be provided to update", "NO_UPDATE_FIELDS");
580
348
  }
581
- }
582
349
 
583
- _ensureCached(map, key, value) {
584
- const existing = map.get(key);
585
- if (existing) return existing;
586
- map.set(key, value);
587
- return value;
588
- }
350
+ try {
351
+ const data = { type: "text" };
352
+ if (name !== undefined) data.name = name.trim();
353
+ if (description !== undefined) data.description = description;
589
354
 
590
- _ensureConnected() {
591
- if (!this.socket || !this.socket.connected || !this.isConnected) {
592
- throw new ClientError(
593
- "Socket is not connected - please call login() first",
594
- "NOT_CONNECTED"
595
- );
355
+ const res = await this._axios.patch(`/api/channels/${channelId}`, data);
356
+ const channel = new Channel(res.data.channel, this);
357
+ this.cache.channels.set(channel.id, channel);
358
+ return channel;
359
+ } catch (error) {
360
+ throw error instanceof ClientError ? error : new ClientError(error.message, "UPDATE_CHANNEL_ERROR");
596
361
  }
597
362
  }
598
363
 
599
364
  /**
600
- * Set the user status
601
- * @param {string} status - Status: "online", "away", "dnd", "offline"
365
+ * Deletes a channel
366
+ * @param {string} channelId - Channel ID
367
+ * @returns {Promise<Object>} Deletion response
602
368
  */
603
- async setStatus(status) {
604
- const validStatuses = ["online", "offline", "away", "dnd"];
605
-
606
- if (!validStatuses.includes(status)) {
607
- throw new ClientError(
608
- `Invalid status. Valid statuses are: ${validStatuses.join(", ")}`,
609
- "INVALID_STATUS"
610
- );
369
+ async deleteChannel(channelId) {
370
+ try {
371
+ const res = await this._axios.delete(`/api/channels/${channelId}`);
372
+ this.cache.channels.delete(channelId);
373
+ return res.data;
374
+ } catch (error) {
375
+ throw error instanceof ClientError ? error : new ClientError(error.message, "DELETE_CHANNEL_ERROR");
611
376
  }
612
-
613
- this._ensureConnected();
614
- this.socket.emit('status:update', { status });
615
377
  }
616
378
 
617
-
618
- // async sendMessage(channelId, content, opts = {}) {
619
- // return new Promise(async (resolve, reject) => {
620
- // try {
621
- // this._ensureConnected();
622
-
623
- // let toSend = content;
624
-
625
- // if (content instanceof MessageEmbed) {
626
- // toSend = content.toText();
627
- // }
628
-
629
- // if (opts instanceof MessageAttachment) {
630
- // const uploadedFile = await this.uploadFile(opts);
631
-
632
- // if (uploadedFile) {
633
- // const mimetype = opts.name.split('.').pop().toLowerCase();
634
- // const messageType = ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(mimetype)
635
- // ? 'image'
636
- // : 'file';
637
-
638
- // opts = {
639
- // fileUrl: uploadedFile.url,
640
- // fileName: uploadedFile.originalName,
641
- // fileSize: uploadedFile.size,
642
- // messageType
643
- // };
644
- // }
645
- // }
646
-
647
- // if (opts.file) {
648
- // const fileData = await this._handleFileUpload(opts.file, opts.fileName);
649
- // opts.fileUrl = fileData.url;
650
- // opts.fileName = fileData.originalName;
651
- // opts.fileSize = fileData.size;
652
- // opts.messageType = opts.messageType || fileData.detectedType;
653
- // }
654
-
655
- // // const timeout = setTimeout(() => {
656
- // // reject(new ClientError("Message send timeout", "SEND_TIMEOUT"));
657
- // // }, 15000);
658
-
659
- // this.socket.emit(
660
- // 'message:send',
661
- // {
662
- // channelId,
663
- // content: toSend,
664
- // messageType: opts.messageType || 'text',
665
- // replyTo: opts.replyTo || null,
666
- // fileUrl: opts.fileUrl || null,
667
- // fileName: opts.fileName || null,
668
- // fileSize: opts.fileSize || null,
669
- // stickerId: opts.stickerId || null,
670
- // },
671
- // async (response) => {
672
- // // clearTimeout(timeout);
673
-
674
- // if (response && response.error) {
675
- // reject(new ClientError(response.error, "SEND_ERROR"));
676
- // } else {
677
- // this._sentMessages.add(response.id);
678
- // const msg = await this._processSocketMessage(response);
679
- // this._cacheMessage(msg);
680
- // resolve(msg);
681
- // }
682
- // }
683
- // );
684
-
685
- // } catch (error) {
686
- // reject(error instanceof ClientError ? error : new ClientError(error.message, "SEND_ERROR"));
687
- // }
688
- // });
689
- // }
690
-
379
+ // ============================================================================
380
+ // PUBLIC API METHODS - Channel Members
381
+ // ============================================================================
691
382
 
692
383
  /**
693
- * Send a message
384
+ * Fetches all members of a channel
694
385
  * @param {string} channelId - Channel ID
695
- * @param {string|MessageEmbed} content - Message content or MessageEmbed
696
- * @param {Object|MessageAttachment} opts - Extra options
697
- * @returns {Promise<Message>}
386
+ * @returns {Promise<User[]>} Array of user objects
698
387
  */
699
- async sendMessage(channelId, content, opts = {}) {
700
- return new Promise(async (resolve, reject) => {
701
- try {
702
- this._ensureConnected();
388
+ async fetchChannelMembers(channelId) {
389
+ try {
390
+ const res = await this._axios.get(`/api/channels/${channelId}/members`);
391
+ const channel = this.cache.channels.get(channelId);
703
392
 
704
- let toSend = content;
705
- let messageType = 'text';
706
- let embedData = null;
393
+ if (!channel) throw new ClientError("Canal não encontrado", "CHANNEL_NOT_FOUND");
707
394
 
708
- // Se o conteúdo é um MessageEmbed
709
- if (content instanceof MessageEmbed) {
710
- try {
711
- content.validate(); // Valida o embed
712
- embedData = content.toJSON();
713
- toSend = ''; // Embed não precisa de conteúdo de texto
714
- messageType = 'embed';
715
- } catch (error) {
716
- return reject(new ClientError(`Invalid embed: ${error.message}`, "INVALID_EMBED"));
717
- }
718
- }
395
+ const members = res.data.map(m => {
396
+ const user = new User(m, this);
397
+ this.cache.users.set(user.id, user);
398
+ channel.members.set(user.id, user);
399
+ return user;
400
+ });
719
401
 
720
- // Se opts é um MessageAttachment (mantém compatibilidade)
721
- if (opts instanceof MessageAttachment) {
722
- const uploadedFile = await this.uploadFile(opts);
723
- if (uploadedFile) {
402
+ return members;
403
+ } catch (error) {
404
+ throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_MEMBERS_ERROR");
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Adds a member to a channel
410
+ * @param {string} channelId - Channel ID
411
+ * @param {string} userId - User ID to add
412
+ * @param {string} role - Member role (default: 'member')
413
+ * @returns {Promise<Object>} Response data
414
+ */
415
+ async addChannelMember(channelId, userId, role = 'member') {
416
+ try {
417
+ const res = await this._axios.post(`/api/channels/${channelId}/members`, {
418
+ userId,
419
+ role
420
+ });
421
+ return res.data;
422
+ } catch (error) {
423
+ throw error instanceof ClientError ? error : new ClientError(error.message, "ADD_MEMBER_ERROR");
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Updates a channel member's information
429
+ * @param {string} channelId - Channel ID
430
+ * @param {string} userId - User ID
431
+ * @param {Object} data - Update data
432
+ * @returns {Promise<Object>} Response data
433
+ */
434
+ async updateChannelMember(channelId, userId, data) {
435
+ try {
436
+ const res = await this._axios.patch(`/api/channels/${channelId}/members/${userId}`, data);
437
+ return res.data;
438
+ } catch (error) {
439
+ throw error instanceof ClientError ? error : new ClientError(error.message, "UPDATE_MEMBER_ERROR");
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Removes a member from a channel
445
+ * @param {string} channelId - Channel ID
446
+ * @param {string} userId - User ID to remove
447
+ * @returns {Promise<Object>} Response data
448
+ */
449
+ async removeChannelMember(channelId, userId) {
450
+ try {
451
+ const res = await this._axios.delete(`/api/channels/${channelId}/members/${userId}`);
452
+ return res.data;
453
+ } catch (error) {
454
+ throw error instanceof ClientError ? error : new ClientError(error.message, "REMOVE_MEMBER_ERROR");
455
+ }
456
+ }
457
+
458
+ // ============================================================================
459
+ // PUBLIC API METHODS - Messages
460
+ // ============================================================================
461
+
462
+ /**
463
+ * Sends a message to a channel
464
+ * @param {string} channelId - Channel ID
465
+ * @param {string|MessageEmbed} content - Message content or embed
466
+ * @param {Object|MessageAttachment} opts - Additional options
467
+ * @returns {Promise<Message>} Sent message object
468
+ */
469
+ async sendMessage(channelId, content, opts = {}) {
470
+ return new Promise(async (resolve, reject) => {
471
+ try {
472
+ this._ensureConnected();
473
+
474
+ let toSend = content;
475
+ let messageType = 'text';
476
+ let embedData = null;
477
+
478
+ // Handle MessageEmbed as content
479
+ if (content instanceof MessageEmbed) {
480
+ try {
481
+ content.validate();
482
+ embedData = content.toJSON();
483
+ toSend = '';
484
+ messageType = 'embed';
485
+ } catch (error) {
486
+ return reject(new ClientError(`Invalid embed: ${error.message}`, "INVALID_EMBED"));
487
+ }
488
+ }
489
+
490
+ // Handle MessageAttachment as opts (backward compatibility)
491
+ if (opts instanceof MessageAttachment) {
492
+ const uploadedFile = await this.uploadFile(opts);
493
+ if (uploadedFile) {
724
494
  const mimetype = opts.name.split('.').pop().toLowerCase();
725
495
  const detectedType = ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(mimetype)
726
496
  ? 'image'
@@ -735,7 +505,7 @@ class Client extends EventEmitter {
735
505
  }
736
506
  }
737
507
 
738
- // Se opts.file existe (upload de arquivo)
508
+ // Handle file upload
739
509
  if (opts.file) {
740
510
  const fileData = await this._handleFileUpload(opts.file, opts.fileName);
741
511
  opts.fileUrl = fileData.url;
@@ -744,7 +514,7 @@ class Client extends EventEmitter {
744
514
  messageType = opts.messageType || fileData.detectedType;
745
515
  }
746
516
 
747
- // Se opts.embed existe (nova forma de enviar embed)
517
+ // Handle embed in opts
748
518
  if (opts.embed) {
749
519
  if (opts.embed instanceof MessageEmbed) {
750
520
  try {
@@ -760,7 +530,7 @@ class Client extends EventEmitter {
760
530
  }
761
531
  }
762
532
 
763
- // Sobrescreve messageType se fornecido explicitamente
533
+ // Override messageType if explicitly provided
764
534
  if (opts.messageType && !embedData) {
765
535
  messageType = opts.messageType;
766
536
  }
@@ -776,7 +546,7 @@ class Client extends EventEmitter {
776
546
  fileName: opts.fileName || null,
777
547
  fileSize: opts.fileSize || null,
778
548
  stickerId: opts.stickerId || null,
779
- embedData: embedData, // Nova propriedade
549
+ embedData: embedData,
780
550
  },
781
551
  async (response) => {
782
552
  if (response && response.error) {
@@ -795,93 +565,11 @@ class Client extends EventEmitter {
795
565
  });
796
566
  }
797
567
 
798
- async _handleFileUpload(file, fileName) {
799
- let fileBuffer;
800
- let finalFileName;
801
- let detectedType;
802
-
803
- if (Buffer.isBuffer(file)) {
804
- if (!fileName) {
805
- throw new ClientError('fileName is required when sending a Buffer', 'MISSING_FILENAME');
806
- }
807
- fileBuffer = file;
808
- finalFileName = fileName;
809
- }
810
- else if (typeof file === 'string') {
811
- if (file.startsWith('data:')) {
812
- const matches = file.match(/^data:([^;]+);base64,(.+)$/);
813
- if (!matches) {
814
- throw new ClientError('Invalid base64 string', 'INVALID_BASE64');
815
- }
816
-
817
- const mimeType = matches[1];
818
- const base64Data = matches[2];
819
- fileBuffer = Buffer.from(base64Data, 'base64');
820
-
821
- const mimeToExt = {
822
- 'image/png': '.png',
823
- 'image/jpeg': '.jpg',
824
- 'image/jpg': '.jpg',
825
- 'image/gif': '.gif',
826
- 'image/webp': '.webp',
827
- 'video/mp4': '.mp4',
828
- 'video/webm': '.webm',
829
- };
830
-
831
- const ext = mimeToExt[mimeType] || '.bin';
832
- finalFileName = fileName || `file${ext}`;
833
- }
834
- else if (file.match(/^[A-Za-z0-9+/=]+$/)) {
835
- if (!fileName) {
836
- throw new ClientError('fileName is required when sending base64 without data URI', 'MISSING_FILENAME');
837
- }
838
- fileBuffer = Buffer.from(file, 'base64');
839
- finalFileName = fileName;
840
- }
841
- else {
842
- if (!fs.existsSync(file)) {
843
- throw new ClientError('File not found', 'FILE_NOT_FOUND');
844
- }
845
- fileBuffer = fs.readFileSync(file);
846
- finalFileName = path.basename(file);
847
- }
848
- } else {
849
- throw new ClientError('Invalid file type. Expected Buffer, base64 string, or file path', 'INVALID_FILE_TYPE');
850
- }
851
-
852
- const ext = path.extname(finalFileName).toLowerCase();
853
- const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
854
- const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm'];
855
-
856
- if (imageExts.includes(ext)) {
857
- detectedType = 'image';
858
- } else if (videoExts.includes(ext)) {
859
- detectedType = 'video';
860
- } else {
861
- detectedType = 'file';
862
- }
863
-
864
- const formData = new FormData();
865
- formData.append('file', fileBuffer, finalFileName);
866
-
867
- try {
868
- const uploadResponse = await this._axios.post('/api/upload', formData, {
869
- headers: formData.getHeaders(),
870
- timeout: 30000
871
- });
872
-
873
- return {
874
- ...uploadResponse.data,
875
- detectedType
876
- };
877
- } catch (error) {
878
- throw new ClientError(`File upload error: ${error.message}`, 'UPLOAD_FAILED');
879
- }
880
- }
881
-
882
568
  /**
883
- * @param {string} messageId
884
- * @param {string} newContent
569
+ * Edits a message
570
+ * @param {string} messageId - Message ID
571
+ * @param {string} newContent - New message content
572
+ * @returns {Promise<Object>} Response data
885
573
  */
886
574
  async editMessage(messageId, newContent) {
887
575
  return new Promise((resolve, reject) => {
@@ -891,16 +579,10 @@ class Client extends EventEmitter {
891
579
  return reject(error);
892
580
  }
893
581
 
894
- // const timeout = setTimeout(() => {
895
- // reject(new ClientError("Message edit timeout", "EDIT_TIMEOUT"));
896
- // }, 15000);
897
-
898
582
  this.socket.emit(
899
583
  'message:edit',
900
584
  { messageId, content: newContent },
901
585
  (response) => {
902
- // clearTimeout(timeout);
903
-
904
586
  if (response && response.error) {
905
587
  reject(new ClientError(response.error, "EDIT_ERROR"));
906
588
  } else {
@@ -913,7 +595,9 @@ class Client extends EventEmitter {
913
595
  }
914
596
 
915
597
  /**
916
- * @param {string} messageId
598
+ * Deletes a message
599
+ * @param {string} messageId - Message ID
600
+ * @returns {Promise<Object>} Response data
917
601
  */
918
602
  async deleteMessage(messageId) {
919
603
  return new Promise((resolve, reject) => {
@@ -923,13 +607,7 @@ class Client extends EventEmitter {
923
607
  return reject(error);
924
608
  }
925
609
 
926
- // const timeout = setTimeout(() => {
927
- // reject(new ClientError("Message delete timeout", "DELETE_TIMEOUT"));
928
- // }, 15000);
929
-
930
610
  this.socket.emit('message:delete', { messageId }, (response) => {
931
- clearTimeout(timeout);
932
-
933
611
  if (response && response.error) {
934
612
  reject(new ClientError(response.error, "DELETE_ERROR"));
935
613
  } else {
@@ -941,8 +619,12 @@ class Client extends EventEmitter {
941
619
  }
942
620
 
943
621
  /**
944
- * @param {string} channelId
945
- * @param {object} options
622
+ * Fetches messages from a channel
623
+ * @param {string} channelId - Channel ID
624
+ * @param {Object} options - Fetch options
625
+ * @param {number} options.limit - Maximum number of messages (default: 50, max: 100)
626
+ * @param {string} options.before - Fetch messages before this message ID
627
+ * @returns {Promise<Message[]>} Array of message objects
946
628
  */
947
629
  async fetchChannelMessages(channelId, options = {}) {
948
630
  try {
@@ -975,8 +657,10 @@ class Client extends EventEmitter {
975
657
  }
976
658
 
977
659
  /**
978
- * @param {string} channelId
979
- * @param {string} messageId
660
+ * Fetches a specific message
661
+ * @param {string} channelId - Channel ID
662
+ * @param {string} messageId - Message ID
663
+ * @returns {Promise<Message>} Message object
980
664
  */
981
665
  async fetchMessage(channelId, messageId) {
982
666
  try {
@@ -987,385 +671,721 @@ class Client extends EventEmitter {
987
671
  }
988
672
  }
989
673
 
990
- async fetchChannels() {
991
- try {
992
- const res = await this._axios.get('/api/channels');
993
- const channels = res.data.map(c => {
994
- const channel = new Channel(c, this);
995
- this.cache.channels.set(channel.id, channel);
996
- return channel;
997
- });
674
+ // ============================================================================
675
+ // PUBLIC API METHODS - Typing Indicators
676
+ // ============================================================================
998
677
 
999
- return channels;
678
+ /**
679
+ * Starts typing indicator in a channel
680
+ * @param {string} channelId - Channel ID
681
+ */
682
+ startTyping(channelId) {
683
+ try {
684
+ this._ensureConnected();
685
+ this.socket.emit('typing:start', { channelId });
1000
686
  } catch (error) {
1001
- throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_CHANNELS_ERROR");
687
+ this.emit("error", error);
1002
688
  }
1003
689
  }
1004
690
 
1005
691
  /**
1006
- * @param {string} id
692
+ * Stops typing indicator in a channel
693
+ * @param {string} channelId - Channel ID
1007
694
  */
1008
- async fetchChannel(id, force = false) {
1009
- if (!force && this.cache.channels.has(id)) {
1010
- return this.cache.channels.get(id);
1011
- }
1012
-
695
+ stopTyping(channelId) {
1013
696
  try {
1014
- const res = await this._axios.get(`/api/channels/${id}`);
1015
- const channel = new Channel(res.data, this);
1016
- this.cache.channels.set(channel.id, channel);
1017
- return channel;
697
+ this._ensureConnected();
698
+ this.socket.emit('typing:stop', { channelId });
1018
699
  } catch (error) {
1019
- throw error instanceof ClientError
1020
- ? error
1021
- : new ClientError(error.message, "FETCH_CHANNEL_ERROR");
700
+ this.emit("error", error);
1022
701
  }
1023
702
  }
1024
703
 
704
+ // ============================================================================
705
+ // PUBLIC API METHODS - Emojis & Stickers
706
+ // ============================================================================
707
+
1025
708
  /**
1026
- * @param {object} options
1027
- * @param {string} options.name
1028
- * @param {string} options.description
709
+ * Fetches an emoji by ID
710
+ * @param {string} id - Emoji ID
711
+ * @param {boolean} force - Force fetch from API instead of cache
712
+ * @returns {Promise<Emoji>} Emoji object
1029
713
  */
1030
- async createChannel({ name, description = "" }) {
1031
- if (!name || name.trim() === "") {
1032
- throw new ClientError("Channel name is required", "INVALID_CHANNEL_NAME");
714
+ async fetchEmoji(id, force = false) {
715
+ if (!force && this.cache.emojis.has(id)) {
716
+ return this.cache.emojis.get(id);
1033
717
  }
1034
718
 
1035
719
  try {
1036
- const data = {
1037
- name: name.trim(),
1038
- description,
1039
- type: "text"
1040
- };
1041
-
1042
- const res = await this._axios.post('/api/channels', data);
1043
- const channel = new Channel(res.data.channel, this);
1044
- this.cache.channels.set(channel.id, channel);
1045
- return channel;
720
+ const res = await this._axios.get(`/api/emojis/${id}`);
721
+ const emoji = new Emoji(res.data);
722
+ this.cache.emojis.set(emoji.id, emoji);
723
+ return emoji;
1046
724
  } catch (error) {
1047
- throw error instanceof ClientError ? error : new ClientError(error.message, "CREATE_CHANNEL_ERROR");
725
+ throw error instanceof ClientError
726
+ ? error
727
+ : new ClientError(error.message, "FETCH_EMOJI_ERROR");
1048
728
  }
1049
729
  }
1050
730
 
1051
731
  /**
1052
- * @param {string} channelId
1053
- * @param {object} options
732
+ * Fetches all available emojis
733
+ * @param {Object} options - Fetch options
734
+ * @param {boolean} options.includeOthers - Include emojis from other users
735
+ * @param {string} options.search - Search query
736
+ * @returns {Promise<Emoji[]>} Array of emoji objects
1054
737
  */
1055
- async updateChannel(channelId, { name, description }) {
1056
- if (!name && !description) {
1057
- throw new ClientError("At least one field must be provided to update", "NO_UPDATE_FIELDS");
1058
- }
1059
-
738
+ async fetchAllEmojis(options = {}) {
1060
739
  try {
1061
- const data = { type: "text" };
1062
- if (name !== undefined) data.name = name.trim();
1063
- if (description !== undefined) data.description = description;
740
+ const endpoint = options.includeOthers ? '/api/emojis/all' : '/api/emojis';
741
+ const params = {};
1064
742
 
1065
- const res = await this._axios.patch(`/api/channels/${channelId}`, data);
1066
- const channel = new Channel(res.data.channel, this);
1067
- this.cache.channels.set(channel.id, channel);
1068
- return channel;
743
+ if (options.search) {
744
+ params.search = options.search;
745
+ }
746
+
747
+ const res = await this._axios.get(endpoint, { params });
748
+ const emojis = res.data.map(e => {
749
+ const emoji = new Emoji(e);
750
+ this.cache.emojis.set(emoji.id, emoji);
751
+ return emoji;
752
+ });
753
+
754
+ return emojis;
1069
755
  } catch (error) {
1070
- throw error instanceof ClientError ? error : new ClientError(error.message, "UPDATE_CHANNEL_ERROR");
756
+ throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_EMOJIS_ERROR");
1071
757
  }
1072
758
  }
1073
759
 
1074
760
  /**
1075
- * @param {string} channelId
761
+ * Fetches a sticker by ID
762
+ * @param {string} id - Sticker ID
763
+ * @param {boolean} force - Force fetch from API instead of cache
764
+ * @returns {Promise<Object>} Sticker data
1076
765
  */
1077
- async deleteChannel(channelId) {
766
+ async fetchSticker(id, force = false) {
767
+ if (!force && this.cache.stickers.has(id)) {
768
+ return this.cache.stickers.get(id);
769
+ }
770
+
1078
771
  try {
1079
- const res = await this._axios.delete(`/api/channels/${channelId}`);
1080
- this.cache.channels.delete(channelId);
772
+ const res = await this._axios.get(`/api/stickers/${id}`);
773
+ this.cache.stickers.set(res.data.id, res.data);
1081
774
  return res.data;
1082
775
  } catch (error) {
1083
- throw error instanceof ClientError ? error : new ClientError(error.message, "DELETE_CHANNEL_ERROR");
776
+ throw error instanceof ClientError
777
+ ? error
778
+ : new ClientError(error.message, "FETCH_STICKER_ERROR");
1084
779
  }
1085
780
  }
1086
781
 
1087
782
  /**
1088
- * @param {string} channelId
783
+ * Fetches all available stickers
784
+ * @param {Object} options - Fetch options
785
+ * @param {string} options.search - Search query
786
+ * @returns {Promise<Object[]>} Array of sticker objects
1089
787
  */
1090
- async fetchChannelMembers(channelId) {
788
+ async fetchAllStickers(options = {}) {
1091
789
  try {
1092
- const res = await this._axios.get(`/api/channels/${channelId}/members`);
1093
- const members = res.data.map(m => {
1094
- const user = new User(m, this);
1095
- this.cache.users.set(user.id, user);
1096
- return user;
790
+ const params = {};
791
+
792
+ if (options.search) {
793
+ params.search = options.search;
794
+ }
795
+
796
+ const res = await this._axios.get('/api/stickers/all', { params });
797
+ res.data.forEach(s => {
798
+ this.cache.stickers.set(s.id, s);
1097
799
  });
1098
800
 
1099
- return members;
801
+ return res.data;
1100
802
  } catch (error) {
1101
- throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_MEMBERS_ERROR");
803
+ throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_STICKERS_ERROR");
1102
804
  }
1103
805
  }
1104
806
 
807
+ // ============================================================================
808
+ // PUBLIC API METHODS - File Upload
809
+ // ============================================================================
810
+
1105
811
  /**
1106
- * @param {string} channelId
1107
- * @param {string} userId
1108
- * @param {string} role
812
+ * Uploads a file to the server
813
+ * @param {MessageAttachment} file - File attachment to upload
814
+ * @returns {Promise<Object>} Upload response with file URL
1109
815
  */
1110
- async addChannelMember(channelId, userId, role = 'member') {
816
+ async uploadFile(file) {
1111
817
  try {
1112
- const res = await this._axios.post(`/api/channels/${channelId}/members`, {
1113
- userId,
1114
- role
818
+ const formData = new FormData();
819
+ formData.append('file', file.buffer, { filename: file.name });
820
+ const res = await this._axios.post('/api/upload', formData, {
821
+ headers: formData.getHeaders(),
822
+ timeout: 30000
1115
823
  });
824
+
1116
825
  return res.data;
1117
826
  } catch (error) {
1118
- throw error instanceof ClientError ? error : new ClientError(error.message, "ADD_MEMBER_ERROR");
827
+ throw error instanceof ClientError ? error : new ClientError(error.message, "UPLOAD_ERROR");
1119
828
  }
1120
829
  }
1121
830
 
831
+ // ============================================================================
832
+ // PUBLIC API METHODS - Cache Management
833
+ // ============================================================================
834
+
1122
835
  /**
1123
- * @param {string} channelId
1124
- * @param {string} userId
1125
- * @param {object} data
836
+ * Clears all cached data
1126
837
  */
1127
- async updateChannelMember(channelId, userId, data) {
1128
- try {
1129
- const res = await this._axios.patch(`/api/channels/${channelId}/members/${userId}`, data);
1130
- return res.data;
1131
- } catch (error) {
1132
- throw error instanceof ClientError ? error : new ClientError(error.message, "UPDATE_MEMBER_ERROR");
1133
- }
838
+ clearCache() {
839
+ this.cache.users.clear();
840
+ this.cache.channels.clear();
841
+ this.cache.messages.clear();
842
+ this.cache.emojis.clear();
843
+ this.cache.stickers.clear();
1134
844
  }
1135
845
 
846
+ // ============================================================================
847
+ // PRIVATE METHODS - Socket Connection & Management
848
+ // ============================================================================
849
+
1136
850
  /**
1137
- * @param {string} channelId
1138
- * @param {string} userId
851
+ * Establishes WebSocket connection to the server
852
+ * @private
1139
853
  */
1140
- async removeChannelMember(channelId, userId) {
854
+ async _connectSocket() {
855
+ return new Promise((resolve, reject) => {
856
+ const connectionTimeout = setTimeout(() => {
857
+ if (this.socket) {
858
+ this.socket.disconnect();
859
+ }
860
+ reject(new ClientError(
861
+ `Connection timeout - failed to connect within ${this.config.connectionTimeout}ms`,
862
+ "CONNECTION_TIMEOUT"
863
+ ));
864
+ }, this.config.connectionTimeout);
865
+
866
+ this.socket = io(global.apiUrl, {
867
+ auth: { token: global.token },
868
+ extraHeaders: { 'Origin': global.apiUrl },
869
+ timeout: 5000,
870
+ reconnection: true,
871
+ reconnectionDelay: this.config.reconnectionDelay,
872
+ reconnectionAttempts: this.config.maxRetries,
873
+ transports: ['websocket', 'polling']
874
+ });
875
+
876
+ this.socket.on("connect", () => {
877
+ clearTimeout(connectionTimeout);
878
+ this.isConnected = true;
879
+ this.retryCount = 0;
880
+ this._setupSocketHandlers();
881
+ this._startHeartbeat();
882
+ resolve();
883
+ });
884
+
885
+ this.socket.on("disconnect", (reason) => {
886
+ this.isConnected = false;
887
+ this._stopHeartbeat();
888
+ this.emit("disconnect", reason);
889
+
890
+ if (reason === "io server disconnect") {
891
+ this.emit("error", new ClientError(
892
+ "Disconnected by server - token may be invalid or revoked",
893
+ "SERVER_DISCONNECT"
894
+ ));
895
+ }
896
+ });
897
+
898
+ this.socket.on("connect_error", (error) => {
899
+ clearTimeout(connectionTimeout);
900
+ this.retryCount++;
901
+
902
+ let errorCode = "CONNECTION_ERROR";
903
+ let errorMessage = "Failed to connect to server";
904
+
905
+ const errorStr = error.message.toLowerCase();
906
+
907
+ if (errorStr.includes("401") || errorStr.includes("unauthorized")) {
908
+ errorCode = "UNAUTHORIZED";
909
+ errorMessage = "Invalid token";
910
+ } else if (errorStr.includes("403") || errorStr.includes("forbidden")) {
911
+ errorCode = "FORBIDDEN";
912
+ errorMessage = "Token expired or revoked";
913
+ } else if (errorStr.includes("timeout")) {
914
+ errorCode = "TIMEOUT";
915
+ errorMessage = "Connection timeout";
916
+ }
917
+
918
+ const clientError = new ClientError(errorMessage, errorCode);
919
+ this.emit("error", clientError);
920
+
921
+ if (this.retryCount >= this.config.maxRetries) {
922
+ reject(clientError);
923
+ }
924
+ });
925
+
926
+ this.socket.on("reconnect", (attemptNumber) => {
927
+ this.isConnected = true;
928
+ this._startHeartbeat();
929
+ this.emit("reconnect", attemptNumber);
930
+ });
931
+
932
+ this.socket.on("reconnect_error", (error) => {
933
+ this.emit("reconnectError", error);
934
+ });
935
+
936
+ this.socket.on("reconnect_failed", () => {
937
+ this._stopHeartbeat();
938
+ this.emit("error", new ClientError(
939
+ "Failed to reconnect after maximum attempts",
940
+ "RECONNECT_FAILED"
941
+ ));
942
+ });
943
+ });
944
+ }
945
+
946
+ /**
947
+ * Joins all channel rooms on connection
948
+ * @private
949
+ */
950
+ async _joinAllChannelRooms() {
1141
951
  try {
1142
- const res = await this._axios.delete(`/api/channels/${channelId}/members/${userId}`);
1143
- return res.data;
952
+ const channels = await this.fetchChannels();
953
+
954
+ for (const channel of channels) {
955
+ if (this.socket && this.socket.connected) {
956
+ this.socket.emit('channel:join', { channelId: channel.id });
957
+ }
958
+ }
1144
959
  } catch (error) {
1145
- throw error instanceof ClientError ? error : new ClientError(error.message, "REMOVE_MEMBER_ERROR");
960
+ console.error('Error joining channel rooms:', error);
1146
961
  }
1147
962
  }
1148
963
 
1149
964
  /**
1150
- * @param {string} id
965
+ * Sets up all socket event handlers
966
+ * @private
1151
967
  */
1152
- async fetchUser(id, force = false) {
1153
- if (!force && this.cache.users.has(id)) {
1154
- return this.cache.users.get(id);
1155
- }
968
+ _setupSocketHandlers() {
969
+ this._removeSocketHandlers();
1156
970
 
1157
- try {
1158
- const res = await this._axios.get(`/api/users/${id}`);
1159
- const user = new User(res.data, this);
1160
- this.cache.users.set(user.id, user);
1161
- return user;
1162
- } catch (error) {
1163
- throw error instanceof ClientError
1164
- ? error
1165
- : new ClientError(error.message, "FETCH_USER_ERROR");
971
+ this.socket.on('message:new', async (data) => {
972
+ try {
973
+ if (this._sentMessages.has(data.id)) {
974
+ this._sentMessages.delete(data.id);
975
+ return;
976
+ }
977
+
978
+ const msg = await this._processSocketMessage(data);
979
+ this._cacheMessage(msg);
980
+ this.emit("messageCreate", msg);
981
+ } catch (error) {
982
+ this.emit("error", error);
983
+ }
984
+ });
985
+
986
+ this.socket.on('message:deleted', (data) => {
987
+ const { messageId } = data;
988
+ this._markMessageDeleted(messageId);
989
+ this.emit('messageDelete', data);
990
+ });
991
+
992
+ this.socket.on('message:edited', (data) => {
993
+ const { messageId, content, editedAt } = data;
994
+ this._updateMessageContent(messageId, content, editedAt);
995
+ this.emit('messageEdit', data);
996
+ });
997
+
998
+ this.socket.on('typing:user-start', (data) => {
999
+ this.emit('typingStart', data);
1000
+ });
1001
+
1002
+ this.socket.on('typing:user-stop', (data) => {
1003
+ this.emit('typingStop', data);
1004
+ });
1005
+
1006
+ this.socket.on('user:status-update', (data) => {
1007
+ this._updateUserStatus(data);
1008
+ this.emit('userStatusUpdate', data);
1009
+ });
1010
+
1011
+ this.socket.on('member:join', async (data) => {
1012
+ const member = await this.fetchUser(data.memberId).catch(() => null);
1013
+ const channel = this.cache.channels.get(data.channelId);
1014
+ if (!channel || !member) return;
1015
+ channel.members.set(member.id, member);
1016
+ channel.memberCount = (channel.memberCount || 0) + 1;
1017
+ this.emit('memberJoin', { channel, member });
1018
+ });
1019
+
1020
+ this.socket.on('member:leave', async (data) => {
1021
+ const channel = this.cache.channels.get(data.channelId);
1022
+ const member = this.cache.users.get(data.memberId);
1023
+
1024
+ if (channel && member) {
1025
+ const wasDeleted = channel.members.delete(member.id);
1026
+
1027
+ if (wasDeleted && channel.memberCount > 0) {
1028
+ channel.memberCount -= 1;
1029
+ }
1030
+ }
1031
+
1032
+ this.emit('memberLeave', { channel, member });
1033
+ });
1034
+
1035
+ this.socket.on('channel:update', (data) => {
1036
+ this._updateChannel(data);
1037
+ this.emit('channelUpdate', data);
1038
+ });
1039
+
1040
+ this.socket.on('channel:delete', (data) => {
1041
+ this.cache.channels.delete(data.channelId);
1042
+ this.emit('channelDelete', data);
1043
+ });
1044
+
1045
+ this.socket.on('rate:limited', (data) => {
1046
+ this.emit('rateLimited', data);
1047
+ });
1048
+ }
1049
+
1050
+ /**
1051
+ * Removes all socket event handlers
1052
+ * @private
1053
+ */
1054
+ _removeSocketHandlers() {
1055
+ if (!this.socket) return;
1056
+
1057
+ this.socket.off("message:new");
1058
+ this.socket.off("message:deleted");
1059
+ this.socket.off("message:edited");
1060
+ this.socket.off("typing:user-start");
1061
+ this.socket.off("typing:user-stop");
1062
+ this.socket.off("user:status-update");
1063
+ this.socket.off("presence:update");
1064
+ this.socket.off("member:join");
1065
+ this.socket.off("member:leave");
1066
+ this.socket.off("channel:update");
1067
+ this.socket.off("channel:delete");
1068
+ this.socket.off("rate:limited");
1069
+ }
1070
+
1071
+ /**
1072
+ * Starts the heartbeat interval to maintain connection
1073
+ * @private
1074
+ */
1075
+ _startHeartbeat() {
1076
+ this._stopHeartbeat();
1077
+
1078
+ const HEARTBEAT_INTERVAL = 30000;
1079
+
1080
+ this.heartbeatInterval = setInterval(() => {
1081
+ if (this.socket && this.isConnected) {
1082
+ this.socket.emit("presence:heartbeat", {
1083
+ status: this.status || "online",
1084
+ clientType: "bot"
1085
+ });
1086
+ }
1087
+ }, HEARTBEAT_INTERVAL);
1088
+
1089
+ if (this.socket && this.isConnected) {
1090
+ this.socket.emit("presence:heartbeat", {
1091
+ status: this.status || "online",
1092
+ clientType: "bot"
1093
+ });
1166
1094
  }
1167
1095
  }
1168
1096
 
1169
- async fetchMe(force = false) {
1170
- if (!force && this.user) {
1171
- return this.user;
1097
+ /**
1098
+ * Stops the heartbeat interval
1099
+ * @private
1100
+ */
1101
+ _stopHeartbeat() {
1102
+ if (this.heartbeatInterval) {
1103
+ clearInterval(this.heartbeatInterval);
1104
+ this.heartbeatInterval = null;
1172
1105
  }
1106
+ }
1173
1107
 
1174
- try {
1175
- const res = await this._axios.get('/api/users/me');
1176
- const user = new User(res.data, this);
1177
- this.cache.users.set(user.id, user);
1178
- this.user = user;
1179
- return user;
1180
- } catch (error) {
1181
- throw error instanceof ClientError
1182
- ? error
1183
- : new ClientError(error.message, "FETCH_ME_ERROR");
1108
+ // ============================================================================
1109
+ // PRIVATE METHODS - Error Handling
1110
+ // ============================================================================
1111
+
1112
+ /**
1113
+ * Handles axios request errors
1114
+ * @private
1115
+ */
1116
+ _handleAxiosError(error) {
1117
+ if (error.response) {
1118
+ const { status, data } = error.response;
1119
+ const errorCode = data?.error || 'UNKNOWN_ERROR';
1120
+ const errorMessage = data?.message || error.message;
1121
+
1122
+ switch (status) {
1123
+ case 401:
1124
+ throw new ClientError(
1125
+ errorMessage || "Invalid or expired token",
1126
+ errorCode || "UNAUTHORIZED"
1127
+ );
1128
+ case 403:
1129
+ throw new ClientError(
1130
+ errorMessage || "Token lacks necessary permissions",
1131
+ errorCode || "FORBIDDEN"
1132
+ );
1133
+ case 404:
1134
+ throw new ClientError(
1135
+ errorMessage || "Resource not found",
1136
+ errorCode || "NOT_FOUND"
1137
+ );
1138
+ case 429:
1139
+ throw new ClientError(
1140
+ errorMessage || "Rate limit exceeded",
1141
+ errorCode || "RATE_LIMITED"
1142
+ );
1143
+ default:
1144
+ throw new ClientError(
1145
+ errorMessage || "API request failed",
1146
+ errorCode
1147
+ );
1148
+ }
1149
+ } else if (error.code === 'ECONNABORTED') {
1150
+ throw new ClientError("Request timeout", "TIMEOUT");
1151
+ } else if (error.code === 'ECONNREFUSED') {
1152
+ throw new ClientError("Cannot connect to API server", "CONNECTION_REFUSED");
1153
+ } else {
1154
+ throw new ClientError(
1155
+ error.message || "Network error",
1156
+ error.code || "NETWORK_ERROR"
1157
+ );
1184
1158
  }
1185
1159
  }
1186
1160
 
1187
1161
  /**
1188
- * @param {string} userId
1162
+ * Ensures the socket is connected before performing operations
1163
+ * @private
1189
1164
  */
1190
- async fetchPresence(userId) {
1191
- try {
1192
- const res = await this._axios.get(`/api/presence/${userId}`);
1193
- this.cache.presence.set(userId, res.data);
1194
- return res.data;
1195
- } catch (error) {
1196
- throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_PRESENCE_ERROR");
1165
+ _ensureConnected() {
1166
+ if (!this.socket || !this.socket.connected || !this.isConnected) {
1167
+ throw new ClientError(
1168
+ "Socket is not connected - please call login() first",
1169
+ "NOT_CONNECTED"
1170
+ );
1197
1171
  }
1198
1172
  }
1199
1173
 
1174
+ // ============================================================================
1175
+ // PRIVATE METHODS - Message Processing
1176
+ // ============================================================================
1177
+
1200
1178
  /**
1201
- * @param {string} id
1179
+ * Processes raw socket message data into Message object
1180
+ * @private
1202
1181
  */
1203
- async fetchEmoji(id, force = false) {
1204
- if (!force && this.cache.emojis.has(id)) {
1205
- return this.cache.emojis.get(id);
1182
+ async _processSocketMessage(data) {
1183
+ const msg = new Message(data, this);
1184
+
1185
+ if (!msg.author && data.user_id) {
1186
+ msg.author = new User({
1187
+ id: data.user_id,
1188
+ username: data.username,
1189
+ display_name: data.display_name,
1190
+ avatar_url: data.avatar_url
1191
+ }, this);
1206
1192
  }
1207
1193
 
1208
- try {
1209
- const res = await this._axios.get(`/api/emojis/${id}`);
1210
- const emoji = new Emoji(res.data);
1211
- this.cache.emojis.set(emoji.id, emoji);
1212
- return emoji;
1213
- } catch (error) {
1214
- throw error instanceof ClientError
1215
- ? error
1216
- : new ClientError(error.message, "FETCH_EMOJI_ERROR");
1194
+ if (!msg.channel && data.channel_id) {
1195
+ msg.channel = await this.fetchChannel(data.channel_id);
1196
+ }
1197
+
1198
+ if (msg.channel.memberCount !== msg.channel.members.size) {
1199
+ await msg.channel.members.fetch();
1217
1200
  }
1201
+
1202
+ return msg;
1218
1203
  }
1219
1204
 
1220
1205
  /**
1221
- * @param {object} options
1206
+ * Caches a message and related entities
1207
+ * @private
1222
1208
  */
1223
- async fetchAllEmojis(options = {}) {
1224
- try {
1225
- const endpoint = options.includeOthers ? '/api/emojis/all' : '/api/emojis';
1226
- const params = {};
1209
+ _cacheMessage(msg) {
1210
+ if (msg.author) {
1211
+ this._ensureCached(this.cache.users, msg.author.id, msg.author);
1212
+ }
1227
1213
 
1228
- if (options.search) {
1229
- params.search = options.search;
1230
- }
1214
+ if (msg.channel) {
1215
+ const channel = msg.channel;
1216
+ this._ensureCached(this.cache.channels, channel.id, channel);
1231
1217
 
1232
- const res = await this._axios.get(endpoint, { params });
1233
- const emojis = res.data.map(e => {
1234
- const emoji = new Emoji(e);
1235
- this.cache.emojis.set(emoji.id, emoji);
1236
- return emoji;
1237
- });
1218
+ // guarda a mensagem no canal
1219
+ channel.messages.set(msg.id, msg);
1238
1220
 
1239
- return emojis;
1240
- } catch (error) {
1241
- throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_EMOJIS_ERROR");
1221
+ // limita a 50 mensagens
1222
+ if (channel.messages.size > 50) {
1223
+ const firstKey = channel.messages.firstKey(); // método do Collection
1224
+ channel.messages.delete(firstKey);
1225
+ }
1242
1226
  }
1243
1227
  }
1244
1228
 
1245
1229
  /**
1246
- * @param {string} id
1230
+ * Marks a message as deleted in cache
1231
+ * @private
1247
1232
  */
1248
- async fetchSticker(id, force = false) {
1249
- if (!force && this.cache.stickers.has(id)) {
1250
- return this.cache.stickers.get(id);
1251
- }
1252
-
1253
- try {
1254
- const res = await this._axios.get(`/api/stickers/${id}`);
1255
- this.cache.stickers.set(res.data.id, res.data);
1256
- return res.data;
1257
- } catch (error) {
1258
- throw error instanceof ClientError
1259
- ? error
1260
- : new ClientError(error.message, "FETCH_STICKER_ERROR");
1233
+ _markMessageDeleted(messageId) {
1234
+ for (const [channelId, messages] of this.cache.messages) {
1235
+ const msg = messages.find(m => m.id === messageId);
1236
+ if (msg) {
1237
+ msg.deleted = true;
1238
+ break;
1239
+ }
1261
1240
  }
1262
1241
  }
1263
1242
 
1264
1243
  /**
1265
- * @param {object} options
1244
+ * Updates message content in cache
1245
+ * @private
1266
1246
  */
1267
- async fetchAllStickers(options = {}) {
1268
- try {
1269
- const params = {};
1270
-
1271
- if (options.search) {
1272
- params.search = options.search;
1247
+ _updateMessageContent(messageId, content, editedAt) {
1248
+ for (const [channelId, messages] of this.cache.messages) {
1249
+ const msg = messages.find(m => m.id === messageId);
1250
+ if (msg) {
1251
+ msg.content = content;
1252
+ msg.editedAt = editedAt;
1253
+ msg.edited = true;
1254
+ break;
1273
1255
  }
1274
-
1275
- const res = await this._axios.get('/api/stickers/all', { params });
1276
- res.data.forEach(s => {
1277
- this.cache.stickers.set(s.id, s);
1278
- });
1279
-
1280
- return res.data;
1281
- } catch (error) {
1282
- throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_STICKERS_ERROR");
1283
1256
  }
1284
1257
  }
1285
1258
 
1286
- async uploadFile(file) {
1287
- try {
1288
- const formData = new FormData();
1289
- formData.append('file', file.buffer, { filename: file.name });
1290
- const res = await this._axios.post('/api/upload', formData, {
1291
- headers: formData.getHeaders(),
1292
- timeout: 30000
1293
- });
1259
+ // ============================================================================
1260
+ // PRIVATE METHODS - Cache Utilities
1261
+ // ============================================================================
1294
1262
 
1295
- return res.data;
1296
- } catch (error) {
1297
- throw error instanceof ClientError ? error : new ClientError(error.message, "UPLOAD_ERROR");
1263
+ /**
1264
+ * Updates user status in cache
1265
+ * @private
1266
+ */
1267
+ _updateUserStatus(data) {
1268
+ const { userId, status, lastSeen } = data;
1269
+
1270
+ if (this.cache.users.has(userId)) {
1271
+ const user = this.cache.users.get(userId);
1272
+ user.status = status;
1273
+ if (lastSeen) user.lastSeen = lastSeen;
1298
1274
  }
1299
1275
  }
1300
1276
 
1301
1277
  /**
1302
- * @param {string} channelId
1278
+ * Updates channel data in cache
1279
+ * @private
1303
1280
  */
1304
- startTyping(channelId) {
1305
- try {
1306
- this._ensureConnected();
1307
- this.socket.emit('typing:start', { channelId });
1308
- } catch (error) {
1309
- this.emit("error", error);
1281
+ _updateChannel(data) {
1282
+ if (this.cache.channels.has(data.id)) {
1283
+ const channel = this.cache.channels.get(data.id);
1284
+ Object.assign(channel, data);
1310
1285
  }
1311
1286
  }
1312
1287
 
1313
1288
  /**
1314
- * @param {string} channelId
1289
+ * Ensures a value is cached and returns it
1290
+ * @private
1315
1291
  */
1316
- stopTyping(channelId) {
1317
- try {
1318
- this._ensureConnected();
1319
- this.socket.emit('typing:stop', { channelId });
1320
- } catch (error) {
1321
- this.emit("error", error);
1322
- }
1292
+ _ensureCached(map, key, value) {
1293
+ const existing = map.get(key);
1294
+ if (existing) return existing;
1295
+ map.set(key, value);
1296
+ return value;
1323
1297
  }
1324
1298
 
1325
- disconnect() {
1326
- if (this.socket) {
1327
- this.socket.disconnect();
1328
- this.isConnected = false;
1329
- this.isReady = false;
1299
+ // ============================================================================
1300
+ // PRIVATE METHODS - File Upload Handling
1301
+ // ============================================================================
1302
+
1303
+ /**
1304
+ * Handles file upload from various input types
1305
+ * @private
1306
+ */
1307
+ async _handleFileUpload(file, fileName) {
1308
+ let fileBuffer;
1309
+ let finalFileName;
1310
+ let detectedType;
1311
+
1312
+ if (Buffer.isBuffer(file)) {
1313
+ if (!fileName) {
1314
+ throw new ClientError('fileName is required when sending a Buffer', 'MISSING_FILENAME');
1315
+ }
1316
+ fileBuffer = file;
1317
+ finalFileName = fileName;
1330
1318
  }
1331
- }
1319
+ else if (typeof file === 'string') {
1320
+ if (file.startsWith('data:')) {
1321
+ const matches = file.match(/^data:([^;]+);base64,(.+)$/);
1322
+ if (!matches) {
1323
+ throw new ClientError('Invalid base64 string', 'INVALID_BASE64');
1324
+ }
1332
1325
 
1333
- ready() {
1334
- return this.isReady && this.isConnected && this.socket && this.socket.connected;
1335
- }
1326
+ const mimeType = matches[1];
1327
+ const base64Data = matches[2];
1328
+ fileBuffer = Buffer.from(base64Data, 'base64');
1336
1329
 
1337
- clearCache() {
1338
- this.cache.users.clear();
1339
- this.cache.channels.clear();
1340
- this.cache.messages.clear();
1341
- this.cache.emojis.clear();
1342
- this.cache.stickers.clear();
1343
- this.cache.presence.clear();
1344
- }
1330
+ const mimeToExt = {
1331
+ 'image/png': '.png',
1332
+ 'image/jpeg': '.jpg',
1333
+ 'image/jpg': '.jpg',
1334
+ 'image/gif': '.gif',
1335
+ 'image/webp': '.webp',
1336
+ 'video/mp4': '.mp4',
1337
+ 'video/webm': '.webm',
1338
+ };
1345
1339
 
1346
- getCacheStats() {
1347
- return {
1348
- users: this.cache.users.size,
1349
- channels: this.cache.channels.size,
1350
- messages: Array.from(this.cache.messages.values()).reduce((acc, arr) => acc + arr.length, 0),
1351
- emojis: this.cache.emojis.size,
1352
- stickers: this.cache.stickers.size,
1353
- presence: this.cache.presence.size,
1354
- };
1355
- }
1340
+ const ext = mimeToExt[mimeType] || '.bin';
1341
+ finalFileName = fileName || `file${ext}`;
1342
+ }
1343
+ else if (file.match(/^[A-Za-z0-9+/=]+$/)) {
1344
+ if (!fileName) {
1345
+ throw new ClientError('fileName is required when sending base64 without data URI', 'MISSING_FILENAME');
1346
+ }
1347
+ fileBuffer = Buffer.from(file, 'base64');
1348
+ finalFileName = fileName;
1349
+ }
1350
+ else {
1351
+ if (!fs.existsSync(file)) {
1352
+ throw new ClientError('File not found', 'FILE_NOT_FOUND');
1353
+ }
1354
+ fileBuffer = fs.readFileSync(file);
1355
+ finalFileName = path.basename(file);
1356
+ }
1357
+ } else {
1358
+ throw new ClientError('Invalid file type. Expected Buffer, base64 string, or file path', 'INVALID_FILE_TYPE');
1359
+ }
1356
1360
 
1357
- getConnectionInfo() {
1358
- return {
1359
- connected: this.isConnected,
1360
- ready: this.isReady,
1361
- socketId: this.socket?.id,
1362
- retryCount: this.retryCount,
1363
- user: this.user ? {
1364
- id: this.user.id,
1365
- username: this.user.username,
1366
- isBot: this.user.isBot
1367
- } : null
1368
- };
1361
+ const ext = path.extname(finalFileName).toLowerCase();
1362
+ const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
1363
+ const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm'];
1364
+
1365
+ if (imageExts.includes(ext)) {
1366
+ detectedType = 'image';
1367
+ } else if (videoExts.includes(ext)) {
1368
+ detectedType = 'video';
1369
+ } else {
1370
+ detectedType = 'file';
1371
+ }
1372
+
1373
+ const formData = new FormData();
1374
+ formData.append('file', fileBuffer, finalFileName);
1375
+
1376
+ try {
1377
+ const uploadResponse = await this._axios.post('/api/upload', formData, {
1378
+ headers: formData.getHeaders(),
1379
+ timeout: 30000
1380
+ });
1381
+
1382
+ return {
1383
+ ...uploadResponse.data,
1384
+ detectedType
1385
+ };
1386
+ } catch (error) {
1387
+ throw new ClientError(`File upload error: ${error.message}`, 'UPLOAD_FAILED');
1388
+ }
1369
1389
  }
1370
1390
  }
1371
1391