@misudev/dconnect 1.6.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yasin Çakmak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ <div align="center">
2
+
3
+ # dconnect
4
+
5
+ **Discord kullanıcı token’ı ile bir voice channel’a bağlanan CLI aracı.**
6
+ Gateway + Voice + UDP handshake dahil, _gerçek_ voice bağlantısı kurar.
7
+
8
+ > Eğitimsel ve deneysel amaçlıdır. Token’la oynuyorsan risk sende.
9
+
10
+ </div>
11
+
12
+ ---
13
+
14
+ ## ✨ Özellikler
15
+
16
+ - Discord **Gateway v10** bağlantısı
17
+ - **Gerçek voice channel** bağlantısı (bot değil, user token)
18
+ - Voice WebSocket + UDP discovery
19
+ - Otomatik heartbeat yönetimi
20
+ - Mute / Deaf opsiyonları
21
+ - Event tabanlı mimari
22
+ (`READY`, `VOICE_STATE_UPDATE`, `VOICE_SERVER_UPDATE`)
23
+ - Tek komut, hızlı bağlantı
24
+
25
+ ---
26
+
27
+ ## ⚠️ Güvenlik Uyarısı
28
+
29
+ Bu araç **doğrudan Discord hesabına bağlanır**.
30
+
31
+ - Token’ını **ASLA** paylaşma
32
+ - Ana hesabında kullanman önerilmez
33
+ - Discord ToS ihlali riski vardır
34
+ - **Educational purposes only**
35
+
36
+ Uygulama çalışmadan önce bu uyarıyı zaten terminalde gösterir.
37
+
38
+ ---
39
+
40
+ ## 📦 Kurulum
41
+
42
+ Node.js **18+** gereklidir.
43
+
44
+ ```bash
45
+ npm install -g dconnect
46
+ ```
47
+
48
+ veya projeden:
49
+
50
+ ```bash
51
+ pnpm install
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 🚀 Kullanım
57
+
58
+ ```bash
59
+ dconnect <token> --guild <guild_id> --channel <channel_id>
60
+ ```
61
+
62
+ ### Gerekli Parametreler
63
+
64
+ - `<token>` → Discord **user token**
65
+ - `--guild, -g` → Sunucu (Guild) ID
66
+ - `--channel, -c` → Voice Channel ID
67
+
68
+ ### Opsiyonlar
69
+
70
+ | Flag | Açıklama |
71
+ | ------------ | ------------------- |
72
+ | `--mute, -m` | Sese kapalı gir |
73
+ | `--deaf, -d` | Kulaklık kapalı gir |
74
+ | `--help` | Yardım menüsü |
75
+ | `--version` | Versiyon bilgisi |
76
+
77
+ ---
78
+
79
+ ## 🧪 Örnek
80
+
81
+ ```bash
82
+ dconnect MTIzNDU2Nzg5... --guild 123456789012345678 --channel 987654321098765432 --mute
83
+ ```
84
+
85
+ Başarılı bağlantıdan sonra:
86
+
87
+ - Gateway bağlantısı kurulur
88
+ - Voice server + state alınır
89
+ - UDP discovery yapılır
90
+ - Voice WebSocket hazır olur
91
+ - Hesap voice channel’a girer
92
+
93
+ Çıkmak için:
94
+
95
+ ```bash
96
+ Ctrl + C
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 🧠 Teknik Detaylar
102
+
103
+ - Discord Gateway **v10**
104
+ - Voice protocol **v4**
105
+ - Encryption mode: `xsalsa20_poly1305`
106
+ - UDP discovery + SSRC handling
107
+ - Tamamen **EventEmitter** tabanlı yapı
108
+
109
+ ---
110
+
111
+ ## 📜 Lisans
112
+
113
+ MIT License
114
+ © 2025
115
+
116
+ ---
117
+
118
+ > Bu proje bot değildir.
119
+ > User token kullanır.
120
+ > Ne yaptığını bilmiyorsan kullanma.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@misudev/dconnect",
3
+ "version": "1.6.2",
4
+ "description": "Connect to Discord voice channel",
5
+ "main": "index.mjs",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "discord",
11
+ "status",
12
+ "user",
13
+ "presence",
14
+ "spotify",
15
+ "lanyard"
16
+ ],
17
+ "author": "misu",
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "axios": "^1.10.0",
21
+ "boxen": "^8.0.1",
22
+ "chalk": "^5.4.1",
23
+ "cli-highlight": "^2.1.11",
24
+ "clipboardy": "^4.0.0",
25
+ "commander": "^12.1.0",
26
+ "consola": "^3.4.2",
27
+ "events": "^3.3.0",
28
+ "inquirer": "^13.1.0",
29
+ "open": "^10.1.2",
30
+ "ora": "^8.2.0",
31
+ "ws": "^8.18.3"
32
+ },
33
+ "bin": {
34
+ "dconnect": "src/index.mjs"
35
+ },
36
+ "funding": {
37
+ "type": "github",
38
+ "url": "https://github.com/sponsors/mishuw"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/mishuw/dconnect.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/mishuw/dconnect/issues"
46
+ },
47
+ "type": "module"
48
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,524 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import { WebSocket } from "ws";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import consola from "consola";
8
+ import { EventEmitter } from "events";
9
+ import dgram from "dgram";
10
+ import boxen from "boxen";
11
+
12
+ const GATEWAY_VERSION = "10";
13
+ const GATEWAY_ENCODING = "json";
14
+
15
+ const OP_CODES = {
16
+ DISPATCH: 0,
17
+ HEARTBEAT: 1,
18
+ IDENTIFY: 2,
19
+ VOICE_STATE_UPDATE: 4,
20
+ INVALID_SESSION: 9,
21
+ HELLO: 10,
22
+ HEARTBEAT_ACK: 11,
23
+ };
24
+
25
+ const VOICE_OP_CODES = {
26
+ IDENTIFY: 0,
27
+ SELECT_PROTOCOL: 1,
28
+ READY: 2,
29
+ HEARTBEAT: 3,
30
+ SESSION_DESCRIPTION: 4,
31
+ SPEAKING: 5,
32
+ HEARTBEAT_ACK: 6,
33
+ HELLO: 8,
34
+ };
35
+
36
+ const INTENTS = {
37
+ GUILDS: 1 << 0,
38
+ GUILD_VOICE_STATES: 1 << 7,
39
+ };
40
+
41
+ class VoiceConnection extends EventEmitter {
42
+ constructor(guildId, channelId, userId, sessionId, token, endpoint) {
43
+ super();
44
+ this.guildId = guildId;
45
+ this.channelId = channelId;
46
+ this.userId = userId;
47
+ this.sessionId = sessionId;
48
+ this.token = token;
49
+ this.endpoint = endpoint;
50
+ this.ws = null;
51
+ this.udp = null;
52
+ this.heartbeatInterval = null;
53
+ this.ready = false;
54
+ this.ssrc = null;
55
+ this.port = null;
56
+ this.ip = null;
57
+ this.secretKey = null;
58
+ }
59
+
60
+ async connect() {
61
+ return new Promise((resolve, reject) => {
62
+ const wsUrl = `wss://${this.endpoint.replace(":80", "")}?v=4`;
63
+ this.ws = new WebSocket(wsUrl);
64
+
65
+ const onOpen = () => {
66
+ this.sendIdentify();
67
+ };
68
+
69
+ const onMessage = (data) => {
70
+ try {
71
+ const payload = JSON.parse(data.toString());
72
+ this.handleVoicePayload(payload, resolve);
73
+ } catch (error) {
74
+ reject(error);
75
+ }
76
+ };
77
+
78
+ const onError = (error) => reject(error);
79
+ const onClose = (code, reason) => {
80
+ this.cleanup();
81
+ reject(new Error(`Voice closed: ${code} ${reason}`));
82
+ };
83
+
84
+ this.ws.once("open", onOpen);
85
+ this.ws.on("message", onMessage);
86
+ this.ws.once("error", onError);
87
+ this.ws.once("close", onClose);
88
+
89
+ this.ws.once("open", () => {
90
+ this.ws.removeListener("error", onError);
91
+ });
92
+ });
93
+ }
94
+
95
+ sendIdentify() {
96
+ const identifyPayload = {
97
+ op: VOICE_OP_CODES.IDENTIFY,
98
+ d: {
99
+ server_id: this.guildId,
100
+ user_id: this.userId,
101
+ session_id: this.sessionId,
102
+ token: this.token,
103
+ },
104
+ };
105
+ this.ws.send(JSON.stringify(identifyPayload));
106
+ }
107
+
108
+ async handleVoicePayload(payload, resolve) {
109
+ const { op, d } = payload;
110
+
111
+ switch (op) {
112
+ case VOICE_OP_CODES.READY:
113
+ this.ssrc = d.ssrc;
114
+ this.port = d.port;
115
+ this.ip = d.ip;
116
+ await this.setupUDP();
117
+ this.selectProtocol();
118
+ break;
119
+
120
+ case VOICE_OP_CODES.SESSION_DESCRIPTION:
121
+ this.secretKey = new Uint8Array(d.secret_key);
122
+ this.ready = true;
123
+ this.setupHeartbeat();
124
+ resolve(this);
125
+ break;
126
+
127
+ case VOICE_OP_CODES.HELLO:
128
+ this.setupHeartbeat();
129
+ break;
130
+ }
131
+ }
132
+
133
+ setupHeartbeat() {
134
+ if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
135
+
136
+ this.heartbeatInterval = setInterval(() => {
137
+ if (this.ws?.readyState === WebSocket.OPEN) {
138
+ this.ws.send(
139
+ JSON.stringify({
140
+ op: VOICE_OP_CODES.HEARTBEAT,
141
+ d: Date.now(),
142
+ }),
143
+ );
144
+ }
145
+ }, 30000);
146
+ }
147
+
148
+ async setupUDP() {
149
+ return new Promise((resolve, reject) => {
150
+ this.udp = dgram.createSocket("udp4");
151
+ const discoveryPacket = Buffer.alloc(70);
152
+ discoveryPacket.writeUInt32BE(this.ssrc, 0);
153
+
154
+ this.udp.once("message", () => resolve());
155
+ this.udp.once("error", reject);
156
+
157
+ this.udp.send(discoveryPacket, 0, 70, this.port, this.ip, (err) => {
158
+ if (err) reject(err);
159
+ });
160
+ });
161
+ }
162
+
163
+ selectProtocol() {
164
+ const protocolPayload = {
165
+ op: VOICE_OP_CODES.SELECT_PROTOCOL,
166
+ d: {
167
+ protocol: "udp",
168
+ data: {
169
+ address: this.ip,
170
+ port: this.port,
171
+ mode: "xsalsa20_poly1305",
172
+ },
173
+ },
174
+ };
175
+ this.ws.send(JSON.stringify(protocolPayload));
176
+ }
177
+
178
+ setSpeaking(speaking = true) {
179
+ if (this.ws?.readyState === WebSocket.OPEN) {
180
+ this.ws.send(
181
+ JSON.stringify({
182
+ op: VOICE_OP_CODES.SPEAKING,
183
+ d: {
184
+ speaking: speaking ? 1 : 0,
185
+ delay: 0,
186
+ ssrc: this.ssrc,
187
+ },
188
+ }),
189
+ );
190
+ }
191
+ }
192
+
193
+ cleanup() {
194
+ if (this.heartbeatInterval) {
195
+ clearInterval(this.heartbeatInterval);
196
+ this.heartbeatInterval = null;
197
+ }
198
+ if (this.udp) {
199
+ this.udp.close();
200
+ this.udp = null;
201
+ }
202
+ if (this.ws) {
203
+ this.ws.close(1000);
204
+ this.ws = null;
205
+ }
206
+ }
207
+ }
208
+
209
+ class DiscordGateway extends EventEmitter {
210
+ constructor(token) {
211
+ super();
212
+ this.token = token;
213
+ this.ws = null;
214
+ this.heartbeatInterval = null;
215
+ this.sequence = null;
216
+ this.sessionId = null;
217
+ this.userData = null;
218
+ this.voiceConnection = null;
219
+ }
220
+
221
+ async connect() {
222
+ const gatewayUrl = await this.getGatewayUrl();
223
+ return new Promise((resolve, reject) => {
224
+ this.ws = new WebSocket(gatewayUrl);
225
+
226
+ const onOpen = () => {
227
+ this.ws.removeListener("error", onError);
228
+ resolve();
229
+ };
230
+
231
+ const onError = (error) => reject(error);
232
+
233
+ this.ws.once("open", onOpen);
234
+ this.ws.once("error", onError);
235
+
236
+ this.ws.on("message", (data) => {
237
+ try {
238
+ const payload = JSON.parse(data.toString());
239
+ this.handlePayload(payload);
240
+ } catch (error) {
241
+ consola.error("Parse error:", error);
242
+ }
243
+ });
244
+
245
+ this.ws.on("close", (code, reason) => {
246
+ this.cleanup();
247
+ consola.warn(`Gateway closed: ${code} ${reason}`);
248
+ });
249
+ });
250
+ }
251
+
252
+ async getGatewayUrl() {
253
+ const response = await fetch("https://discord.com/api/gateway");
254
+ const { url } = await response.json();
255
+ return `${url}?v=${GATEWAY_VERSION}&encoding=${GATEWAY_ENCODING}`;
256
+ }
257
+
258
+ handlePayload(payload) {
259
+ const { op, d, s, t } = payload;
260
+ if (s !== null) this.sequence = s;
261
+
262
+ switch (op) {
263
+ case OP_CODES.HELLO:
264
+ this.handleHello(d);
265
+ break;
266
+ case OP_CODES.DISPATCH:
267
+ this.handleDispatch(t, d);
268
+ break;
269
+ case OP_CODES.INVALID_SESSION:
270
+ setTimeout(() => this.identify(), 1000);
271
+ break;
272
+ }
273
+ }
274
+
275
+ handleHello(data) {
276
+ this.heartbeatInterval = setInterval(() => {
277
+ if (this.ws?.readyState === WebSocket.OPEN) {
278
+ this.ws.send(
279
+ JSON.stringify({
280
+ op: OP_CODES.HEARTBEAT,
281
+ d: this.sequence,
282
+ }),
283
+ );
284
+ }
285
+ }, data.heartbeat_interval);
286
+
287
+ setImmediate(() => this.identify());
288
+ }
289
+
290
+ identify() {
291
+ const identifyPayload = {
292
+ op: OP_CODES.IDENTIFY,
293
+ d: {
294
+ token: this.token,
295
+ properties: {
296
+ os: "linux",
297
+ browser: "misu",
298
+ device: "misu",
299
+ },
300
+ compress: false,
301
+ intents: INTENTS.GUILDS | INTENTS.GUILD_VOICE_STATES,
302
+ },
303
+ };
304
+ this.ws.send(JSON.stringify(identifyPayload));
305
+ }
306
+
307
+ handleDispatch(event, data) {
308
+ switch (event) {
309
+ case "READY":
310
+ this.sessionId = data.session_id;
311
+ this.userData = data.user;
312
+ this.emit("ready", data);
313
+ break;
314
+ case "VOICE_STATE_UPDATE":
315
+ if (data.user_id === this.userData?.id) {
316
+ this.emit("voice_state_update", data);
317
+ }
318
+ break;
319
+ case "VOICE_SERVER_UPDATE":
320
+ this.emit("voice_server_update", data);
321
+ break;
322
+ }
323
+ }
324
+
325
+ async joinVoiceChannel(guildId, channelId, mute = false, deaf = false) {
326
+ const voiceStatePayload = {
327
+ op: OP_CODES.VOICE_STATE_UPDATE,
328
+ d: {
329
+ guild_id: guildId,
330
+ channel_id: channelId,
331
+ self_mute: mute,
332
+ self_deaf: deaf,
333
+ },
334
+ };
335
+ this.ws.send(JSON.stringify(voiceStatePayload));
336
+
337
+ return new Promise((resolve, reject) => {
338
+ let voiceState = null;
339
+ let voiceServer = null;
340
+ let timeout = null;
341
+
342
+ const checkReady = () => {
343
+ if (voiceState && voiceServer) {
344
+ clearTimeout(timeout);
345
+ this.off("voice_state_update", onVoiceState);
346
+ this.off("voice_server_update", onVoiceServer);
347
+ this.createVoiceConnection(
348
+ guildId,
349
+ channelId,
350
+ voiceState,
351
+ voiceServer,
352
+ )
353
+ .then(resolve)
354
+ .catch(reject);
355
+ }
356
+ };
357
+
358
+ const onVoiceState = (data) => {
359
+ if (data.guild_id === guildId && data.user_id === this.userData?.id) {
360
+ voiceState = data;
361
+ checkReady();
362
+ }
363
+ };
364
+
365
+ const onVoiceServer = (data) => {
366
+ if (data.guild_id === guildId) {
367
+ voiceServer = data;
368
+ checkReady();
369
+ }
370
+ };
371
+
372
+ this.on("voice_state_update", onVoiceState);
373
+ this.on("voice_server_update", onVoiceServer);
374
+
375
+ timeout = setTimeout(() => {
376
+ this.off("voice_state_update", onVoiceState);
377
+ this.off("voice_server_update", onVoiceServer);
378
+ reject(new Error("Voice connection timeout (10s)"));
379
+ }, 10000);
380
+ });
381
+ }
382
+
383
+ async createVoiceConnection(guildId, channelId, voiceState, voiceServer) {
384
+ this.voiceConnection = new VoiceConnection(
385
+ guildId,
386
+ channelId,
387
+ this.userData.id,
388
+ voiceState.session_id,
389
+ voiceServer.token,
390
+ voiceServer.endpoint,
391
+ );
392
+
393
+ this.voiceConnection.on("disconnected", () => {
394
+ this.voiceConnection = null;
395
+ });
396
+
397
+ await this.voiceConnection.connect();
398
+ return this.voiceConnection;
399
+ }
400
+
401
+ cleanup() {
402
+ if (this.heartbeatInterval) {
403
+ clearInterval(this.heartbeatInterval);
404
+ this.heartbeatInterval = null;
405
+ }
406
+ if (this.voiceConnection) {
407
+ this.voiceConnection.cleanup();
408
+ this.voiceConnection = null;
409
+ }
410
+ if (this.ws) {
411
+ this.ws.close(1000);
412
+ this.ws = null;
413
+ }
414
+ }
415
+ }
416
+
417
+ function validateToken(token) {
418
+ if (!token || token.length < 59) {
419
+ return { valid: false, error: "Invalid token format" };
420
+ }
421
+ return { valid: true };
422
+ }
423
+
424
+ program
425
+ .name("dconnect")
426
+ .description("Connect to Discord voice channel")
427
+ .version("1.0.0")
428
+ .argument("<token>", "Discord token")
429
+ .requiredOption("-g, --guild <id>", "Guild ID")
430
+ .requiredOption("-c, --channel <id>", "Voice channel ID")
431
+ .option("-m, --mute", "Join muted", false)
432
+ .option("-d, --deaf", "Join deafened", false)
433
+ .action(async (token, options) => {
434
+ const validation = validateToken(token);
435
+ if (!validation.valid) {
436
+ consola.error(`Invalid token: ${validation.error}`);
437
+ process.exit(1);
438
+ }
439
+
440
+ console.clear();
441
+ console.log(
442
+ boxen(
443
+ chalk.bold.red("⚠ SECURITY WARNING ⚠\n\n") +
444
+ chalk.yellow(
445
+ "This tool connects directly to your Discord account.\n",
446
+ ) +
447
+ chalk.yellow("• Token security is VERY IMPORTANT\n") +
448
+ chalk.yellow("• EDUCATIONAL PURPOSES ONLY\n") +
449
+ chalk.yellow("• NEVER share your token\n") +
450
+ chalk.yellow("• Your account may be at risk\n\n") +
451
+ chalk.white.bold("Press Enter to continue or Ctrl+C to cancel"),
452
+ {
453
+ padding: 1,
454
+ borderColor: "red",
455
+ borderStyle: "double",
456
+ margin: 1,
457
+ },
458
+ ),
459
+ );
460
+
461
+ process.stdin.setRawMode(true);
462
+ process.stdin.resume();
463
+ await new Promise((r) => process.stdin.once("data", r));
464
+ process.stdin.setRawMode(false);
465
+
466
+ const spinner = ora({
467
+ text: "Connecting to Discord...",
468
+ spinner: "dots",
469
+ color: "cyan",
470
+ hideCursor: true,
471
+ }).start();
472
+
473
+ try {
474
+ const startTime = Date.now();
475
+ const gateway = new DiscordGateway(token);
476
+ let voiceConn = null;
477
+
478
+ gateway.once("ready", async (data) => {
479
+ const connectTime = Date.now() - startTime;
480
+ spinner.succeed(`Connected in @${data.user.username} ${connectTime}ms`);
481
+
482
+ consola.info(`Guild: ${options.guild}`);
483
+ consola.info(`Channel: ${options.channel}`);
484
+ consola.info(`Mute: ${options.mute ? "Yes" : "No"}`);
485
+ consola.info(`Deaf: ${options.deaf ? "Yes" : "No"}`);
486
+ try {
487
+ voiceConn = await gateway.joinVoiceChannel(
488
+ options.guild,
489
+ options.channel,
490
+ options.mute,
491
+ options.deaf,
492
+ );
493
+
494
+ consola.success("✓ Voice connected!");
495
+
496
+ if (!options.mute) {
497
+ voiceConn.setSpeaking(true);
498
+ }
499
+
500
+ consola.info("Press Ctrl+C to exit");
501
+ } catch (err) {
502
+ consola.error(`Voice error: ${err.message}`);
503
+ process.exit(1);
504
+ }
505
+ });
506
+
507
+ await gateway.connect();
508
+
509
+ const exitHandler = () => {
510
+ consola.info("\nDisconnecting...");
511
+ if (voiceConn) voiceConn.cleanup();
512
+ gateway.cleanup();
513
+ process.exit(0);
514
+ };
515
+
516
+ process.on("SIGINT", exitHandler);
517
+ process.on("SIGTERM", exitHandler);
518
+ } catch (error) {
519
+ spinner.fail(`Connection failed: ${error.message}`);
520
+ process.exit(1);
521
+ }
522
+ });
523
+
524
+ program.parse(process.argv);
package/src/utils.mjs ADDED
@@ -0,0 +1,31 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import consola from 'consola';
4
+
5
+ export async function getUserPresence(userID) {
6
+ try {
7
+ const response = await axios.get(`https://api.lanyard.rest/v1/users/${userID}`);
8
+ return response.data;
9
+ } catch (error) {
10
+ consola.error(new Error(chalk.red(`An error occurred fetching data from the Lanyard API: ${error.message}`)));
11
+ process.exit(1);
12
+ }
13
+ }
14
+
15
+ export function isCustomEmoji(emoji) {
16
+ if (!emoji || typeof emoji !== 'object') return false;
17
+ return emoji.id && typeof emoji.id === 'string' && !emoji.id.startsWith('<a:');
18
+ }
19
+
20
+ export function formatDuration(duration) {
21
+ const hours = Math.floor(duration / 3600);
22
+ const minutes = Math.floor((duration % 3600) / 60);
23
+ const seconds = Math.floor(duration % 60);
24
+ if (hours > 0) {
25
+ return `${hours}hr ${minutes}min ${seconds}sec`;
26
+ } else if (minutes > 0) {
27
+ return `${minutes}min ${seconds}sec`;
28
+ } else {
29
+ return `${seconds} seconds`;
30
+ }
31
+ }