beniocord.js 2.0.6 → 2.0.8

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