beniocord.js 2.0.7 → 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 +894 -874
- package/collection/index.d.ts +457 -0
- package/collection/index.js +543 -0
- package/package.json +1 -1
- package/structures/Channel.js +31 -3
- package/structures/Message.js +2 -2
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
|
-
//
|
|
44
|
+
// Global configuration
|
|
44
45
|
global.token = token.trim();
|
|
45
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
*
|
|
318
|
-
* @
|
|
154
|
+
* Checks if the client is ready and connected
|
|
155
|
+
* @returns {boolean} True if ready and connected
|
|
319
156
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
this.
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
397
|
-
this.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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
|
-
|
|
534
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
*
|
|
601
|
-
* @param {string}
|
|
365
|
+
* Deletes a channel
|
|
366
|
+
* @param {string} channelId - Channel ID
|
|
367
|
+
* @returns {Promise<Object>} Deletion response
|
|
602
368
|
*/
|
|
603
|
-
async
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
//
|
|
619
|
-
//
|
|
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
|
-
*
|
|
384
|
+
* Fetches all members of a channel
|
|
694
385
|
* @param {string} channelId - Channel ID
|
|
695
|
-
* @
|
|
696
|
-
* @param {Object|MessageAttachment} opts - Extra options
|
|
697
|
-
* @returns {Promise<Message>}
|
|
386
|
+
* @returns {Promise<User[]>} Array of user objects
|
|
698
387
|
*/
|
|
699
|
-
async
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
705
|
-
let messageType = 'text';
|
|
706
|
-
let embedData = null;
|
|
393
|
+
if (!channel) throw new ClientError("Canal não encontrado", "CHANNEL_NOT_FOUND");
|
|
707
394
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
*
|
|
884
|
-
* @param {string}
|
|
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
|
-
*
|
|
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
|
-
*
|
|
945
|
-
* @param {
|
|
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
|
-
*
|
|
979
|
-
* @param {string}
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
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
|
-
|
|
687
|
+
this.emit("error", error);
|
|
1002
688
|
}
|
|
1003
689
|
}
|
|
1004
690
|
|
|
1005
691
|
/**
|
|
1006
|
-
*
|
|
692
|
+
* Stops typing indicator in a channel
|
|
693
|
+
* @param {string} channelId - Channel ID
|
|
1007
694
|
*/
|
|
1008
|
-
|
|
1009
|
-
if (!force && this.cache.channels.has(id)) {
|
|
1010
|
-
return this.cache.channels.get(id);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
695
|
+
stopTyping(channelId) {
|
|
1013
696
|
try {
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1027
|
-
* @param {string}
|
|
1028
|
-
* @param {
|
|
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
|
|
1031
|
-
if (!
|
|
1032
|
-
|
|
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
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
|
725
|
+
throw error instanceof ClientError
|
|
726
|
+
? error
|
|
727
|
+
: new ClientError(error.message, "FETCH_EMOJI_ERROR");
|
|
1048
728
|
}
|
|
1049
729
|
}
|
|
1050
730
|
|
|
1051
731
|
/**
|
|
1052
|
-
*
|
|
1053
|
-
* @param {
|
|
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
|
|
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
|
|
1062
|
-
|
|
1063
|
-
if (description !== undefined) data.description = description;
|
|
740
|
+
const endpoint = options.includeOthers ? '/api/emojis/all' : '/api/emojis';
|
|
741
|
+
const params = {};
|
|
1064
742
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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, "
|
|
756
|
+
throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_EMOJIS_ERROR");
|
|
1071
757
|
}
|
|
1072
758
|
}
|
|
1073
759
|
|
|
1074
760
|
/**
|
|
1075
|
-
*
|
|
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
|
|
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.
|
|
1080
|
-
this.cache.
|
|
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
|
|
776
|
+
throw error instanceof ClientError
|
|
777
|
+
? error
|
|
778
|
+
: new ClientError(error.message, "FETCH_STICKER_ERROR");
|
|
1084
779
|
}
|
|
1085
780
|
}
|
|
1086
781
|
|
|
1087
782
|
/**
|
|
1088
|
-
*
|
|
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
|
|
788
|
+
async fetchAllStickers(options = {}) {
|
|
1091
789
|
try {
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
|
801
|
+
return res.data;
|
|
1100
802
|
} catch (error) {
|
|
1101
|
-
throw error instanceof ClientError ? error : new ClientError(error.message, "
|
|
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
|
-
*
|
|
1107
|
-
* @param {
|
|
1108
|
-
* @
|
|
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
|
|
816
|
+
async uploadFile(file) {
|
|
1111
817
|
try {
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
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, "
|
|
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
|
-
*
|
|
1124
|
-
* @param {string} userId
|
|
1125
|
-
* @param {object} data
|
|
836
|
+
* Clears all cached data
|
|
1126
837
|
*/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
*
|
|
1138
|
-
* @
|
|
851
|
+
* Establishes WebSocket connection to the server
|
|
852
|
+
* @private
|
|
1139
853
|
*/
|
|
1140
|
-
async
|
|
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
|
|
1143
|
-
|
|
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
|
-
|
|
960
|
+
console.error('Error joining channel rooms:', error);
|
|
1146
961
|
}
|
|
1147
962
|
}
|
|
1148
963
|
|
|
1149
964
|
/**
|
|
1150
|
-
*
|
|
965
|
+
* Sets up all socket event handlers
|
|
966
|
+
* @private
|
|
1151
967
|
*/
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
return this.cache.users.get(id);
|
|
1155
|
-
}
|
|
968
|
+
_setupSocketHandlers() {
|
|
969
|
+
this._removeSocketHandlers();
|
|
1156
970
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
-
*
|
|
1162
|
+
* Ensures the socket is connected before performing operations
|
|
1163
|
+
* @private
|
|
1189
1164
|
*/
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
*
|
|
1179
|
+
* Processes raw socket message data into Message object
|
|
1180
|
+
* @private
|
|
1202
1181
|
*/
|
|
1203
|
-
async
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
*
|
|
1206
|
+
* Caches a message and related entities
|
|
1207
|
+
* @private
|
|
1222
1208
|
*/
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1209
|
+
_cacheMessage(msg) {
|
|
1210
|
+
if (msg.author) {
|
|
1211
|
+
this._ensureCached(this.cache.users, msg.author.id, msg.author);
|
|
1212
|
+
}
|
|
1227
1213
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1214
|
+
if (msg.channel) {
|
|
1215
|
+
const channel = msg.channel;
|
|
1216
|
+
this._ensureCached(this.cache.channels, channel.id, channel);
|
|
1231
1217
|
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
*
|
|
1230
|
+
* Marks a message as deleted in cache
|
|
1231
|
+
* @private
|
|
1247
1232
|
*/
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
*
|
|
1244
|
+
* Updates message content in cache
|
|
1245
|
+
* @private
|
|
1266
1246
|
*/
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
-
*
|
|
1278
|
+
* Updates channel data in cache
|
|
1279
|
+
* @private
|
|
1303
1280
|
*/
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
this.
|
|
1307
|
-
|
|
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
|
-
*
|
|
1289
|
+
* Ensures a value is cached and returns it
|
|
1290
|
+
* @private
|
|
1315
1291
|
*/
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1326
|
+
const mimeType = matches[1];
|
|
1327
|
+
const base64Data = matches[2];
|
|
1328
|
+
fileBuffer = Buffer.from(base64Data, 'base64');
|
|
1336
1329
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
|