@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 +21 -0
- package/README.md +120 -0
- package/package.json +48 -0
- package/src/index.mjs +524 -0
- package/src/utils.mjs +31 -0
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
|
+
}
|