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 +873 -845
- 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/structures/MessageCollector.js +1 -1
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();
|
|
@@ -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
|
-
*
|
|
302
|
-
* @
|
|
154
|
+
* Checks if the client is ready and connected
|
|
155
|
+
* @returns {boolean} True if ready and connected
|
|
303
156
|
*/
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
this.
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
|
|
381
|
-
this.
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
this.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
321
|
+
try {
|
|
322
|
+
const data = {
|
|
323
|
+
name: name.trim(),
|
|
324
|
+
description,
|
|
325
|
+
type: "text"
|
|
326
|
+
};
|
|
506
327
|
|
|
507
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
*
|
|
593
|
-
* @param {string}
|
|
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
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
//
|
|
611
|
-
//
|
|
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
|
-
*
|
|
463
|
+
* Sends a message to a channel
|
|
686
464
|
* @param {string} channelId - Channel ID
|
|
687
|
-
* @param {string|MessageEmbed} content - Message content or
|
|
688
|
-
* @param {Object|MessageAttachment} opts -
|
|
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
|
-
//
|
|
478
|
+
// Handle MessageEmbed as content
|
|
701
479
|
if (content instanceof MessageEmbed) {
|
|
702
480
|
try {
|
|
703
|
-
content.validate();
|
|
481
|
+
content.validate();
|
|
704
482
|
embedData = content.toJSON();
|
|
705
|
-
toSend = '';
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
*
|
|
876
|
-
* @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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
937
|
-
* @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
|
|
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
|
-
*
|
|
971
|
-
* @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
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
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
|
-
|
|
687
|
+
this.emit("error", error);
|
|
994
688
|
}
|
|
995
689
|
}
|
|
996
690
|
|
|
997
691
|
/**
|
|
998
|
-
*
|
|
692
|
+
* Stops typing indicator in a channel
|
|
693
|
+
* @param {string} channelId - Channel ID
|
|
999
694
|
*/
|
|
1000
|
-
|
|
1001
|
-
if (!force && this.cache.channels.has(id)) {
|
|
1002
|
-
return this.cache.channels.get(id);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
695
|
+
stopTyping(channelId) {
|
|
1005
696
|
try {
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1019
|
-
* @param {string}
|
|
1020
|
-
* @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
|
|
1021
713
|
*/
|
|
1022
|
-
async
|
|
1023
|
-
if (!
|
|
1024
|
-
|
|
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
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
725
|
+
throw error instanceof ClientError
|
|
726
|
+
? error
|
|
727
|
+
: new ClientError(error.message, "FETCH_EMOJI_ERROR");
|
|
1040
728
|
}
|
|
1041
729
|
}
|
|
1042
730
|
|
|
1043
731
|
/**
|
|
1044
|
-
*
|
|
1045
|
-
* @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
|
|
1046
737
|
*/
|
|
1047
|
-
async
|
|
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
|
|
1054
|
-
|
|
1055
|
-
if (description !== undefined) data.description = description;
|
|
740
|
+
const endpoint = options.includeOthers ? '/api/emojis/all' : '/api/emojis';
|
|
741
|
+
const params = {};
|
|
1056
742
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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, "
|
|
756
|
+
throw error instanceof ClientError ? error : new ClientError(error.message, "FETCH_EMOJIS_ERROR");
|
|
1063
757
|
}
|
|
1064
758
|
}
|
|
1065
759
|
|
|
1066
760
|
/**
|
|
1067
|
-
*
|
|
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
|
|
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.
|
|
1072
|
-
this.cache.
|
|
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
|
|
776
|
+
throw error instanceof ClientError
|
|
777
|
+
? error
|
|
778
|
+
: new ClientError(error.message, "FETCH_STICKER_ERROR");
|
|
1076
779
|
}
|
|
1077
780
|
}
|
|
1078
781
|
|
|
1079
782
|
/**
|
|
1080
|
-
*
|
|
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
|
|
788
|
+
async fetchAllStickers(options = {}) {
|
|
1083
789
|
try {
|
|
1084
|
-
const
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
792
|
+
if (options.search) {
|
|
793
|
+
params.search = options.search;
|
|
794
|
+
}
|
|
1096
795
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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, "
|
|
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
|
-
*
|
|
1116
|
-
* @param {
|
|
1117
|
-
* @
|
|
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
|
|
816
|
+
async uploadFile(file) {
|
|
1120
817
|
try {
|
|
1121
|
-
const
|
|
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, "
|
|
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
|
-
*
|
|
1130
|
-
* @param {string} userId
|
|
836
|
+
* Clears all cached data
|
|
1131
837
|
*/
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
*
|
|
851
|
+
* Establishes WebSocket connection to the server
|
|
852
|
+
* @private
|
|
1143
853
|
*/
|
|
1144
|
-
async
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
*
|
|
1162
|
+
* Ensures the socket is connected before performing operations
|
|
1163
|
+
* @private
|
|
1181
1164
|
*/
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
-
*
|
|
1179
|
+
* Processes raw socket message data into Message object
|
|
1180
|
+
* @private
|
|
1194
1181
|
*/
|
|
1195
|
-
async
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
*
|
|
1206
|
+
* Caches a message and related entities
|
|
1207
|
+
* @private
|
|
1214
1208
|
*/
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1209
|
+
_cacheMessage(msg) {
|
|
1210
|
+
if (msg.author) {
|
|
1211
|
+
this._ensureCached(this.cache.users, msg.author.id, msg.author);
|
|
1212
|
+
}
|
|
1219
1213
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1214
|
+
if (msg.channel) {
|
|
1215
|
+
const channel = msg.channel;
|
|
1216
|
+
this._ensureCached(this.cache.channels, channel.id, channel);
|
|
1223
1217
|
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
*
|
|
1230
|
+
* Marks a message as deleted in cache
|
|
1231
|
+
* @private
|
|
1239
1232
|
*/
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
-
*
|
|
1244
|
+
* Updates message content in cache
|
|
1245
|
+
* @private
|
|
1258
1246
|
*/
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
*
|
|
1278
|
+
* Updates channel data in cache
|
|
1279
|
+
* @private
|
|
1295
1280
|
*/
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
this.
|
|
1299
|
-
|
|
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
|
-
*
|
|
1289
|
+
* Ensures a value is cached and returns it
|
|
1290
|
+
* @private
|
|
1307
1291
|
*/
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1326
|
+
const mimeType = matches[1];
|
|
1327
|
+
const base64Data = matches[2];
|
|
1328
|
+
fileBuffer = Buffer.from(base64Data, 'base64');
|
|
1328
1329
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
|