djs-selfbot-v13 3.7.32 → 3.7.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "djs-selfbot-v13",
3
- "version": "3.7.32",
3
+ "version": "3.7.34",
4
4
  "description": "An unofficial discord.js fork for creating selfbots",
5
5
  "main": "./src/index.js",
6
6
  "types": "./typings/index.d.ts",
@@ -51,6 +51,8 @@
51
51
  },
52
52
  "homepage": "https://djs-selfbot.vercel.app/",
53
53
  "dependencies": {
54
+ "@dank074/discord-video-stream": "^6.0.0",
55
+ "@snazzah/davey": "^0.1.11",
54
56
  "@discordjs/builders": "^1.6.3",
55
57
  "@discordjs/collection": "^2.1.1",
56
58
  "@sapphire/async-queue": "^1.5.5",
@@ -68,6 +70,11 @@
68
70
  "werift-rtp": "^0.8.4",
69
71
  "ws": "^8.16.0"
70
72
  },
73
+ "optionalDependencies": {
74
+ "@snazzah/davey-linux-x64-gnu": "0.1.11",
75
+ "@snazzah/davey-linux-x64-musl": "0.1.11",
76
+ "@snazzah/davey-win32-x64-msvc": "0.1.11"
77
+ },
71
78
  "engines": {
72
79
  "node": ">=20.18"
73
80
  },
@@ -9,6 +9,8 @@ const { authenticator } = require('otplib');
9
9
  const BaseClient = require('./BaseClient');
10
10
  const ActionsManager = require('./actions/ActionsManager');
11
11
  const ClientVoiceManager = require('./voice/ClientVoiceManager');
12
+ const StreamSession = require('./voice/StreamSession');
13
+ const WebRtcStreamSession = require('./voice/WebRtcStreamSession');
12
14
  const WebSocketManager = require('./websocket/WebSocketManager');
13
15
  const { Error, TypeError } = require('../errors');
14
16
  const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
@@ -36,7 +38,7 @@ const VoiceRegion = require('../structures/VoiceRegion');
36
38
  const Webhook = require('../structures/Webhook');
37
39
  const Widget = require('../structures/Widget');
38
40
  const Application = require('../structures/interfaces/Application');
39
- const { Events, Status } = require('../util/Constants');
41
+ const { Events, Status, VoiceStatus } = require('../util/Constants');
40
42
  const DataResolver = require('../util/DataResolver');
41
43
  const Intents = require('../util/Intents');
42
44
  const DiscordAuthWebsocket = require('../util/RemoteAuth');
@@ -895,6 +897,71 @@ class Client extends BaseClient {
895
897
  });
896
898
  }
897
899
 
900
+ /**
901
+ * Joins a voice channel, starts a screenshare stream and plays a video.
902
+ * @param {import('./voice/StreamSession').StartStreamOptions} options Stream options
903
+ * @returns {Promise<import('./voice/StreamSession')>}
904
+ * @example
905
+ * const stream = await client.startStream({
906
+ * guildId: '123',
907
+ * channelId: '456',
908
+ * url: 'video.mp4',
909
+ * fps: 60,
910
+ * height: 720,
911
+ * bitrate: 4500,
912
+ * });
913
+ * stream.pause();
914
+ * stream.resume();
915
+ * stream.stop();
916
+ * stream.replay();
917
+ * stream.disconnect();
918
+ */
919
+ async startStream(options = {}) {
920
+ const {
921
+ guildId,
922
+ channelId,
923
+ url,
924
+ fps,
925
+ height,
926
+ width,
927
+ bitrate = 5000,
928
+ bitrateMax,
929
+ audioBitrate,
930
+ preset,
931
+ tune,
932
+ audio,
933
+ livestream = false,
934
+ downloadHttp = true,
935
+ } = options;
936
+
937
+ if (!url) throw new Error('STREAM_URL_REQUIRED');
938
+ if (!guildId || !channelId) throw new Error('STREAM_CHANNEL_REQUIRED');
939
+
940
+ let playUrl = url;
941
+ if (typeof url === 'string' && url.startsWith('http') && downloadHttp) {
942
+ playUrl = await WebRtcStreamSession.resolveUrl(url);
943
+ }
944
+
945
+ const session = new WebRtcStreamSession(this, {
946
+ guildId,
947
+ channelId,
948
+ url: playUrl,
949
+ fps,
950
+ height,
951
+ width,
952
+ bitrate,
953
+ bitrateMax,
954
+ audioBitrate,
955
+ preset,
956
+ tune,
957
+ audio,
958
+ livestream,
959
+ });
960
+
961
+ await session.start();
962
+ return session;
963
+ }
964
+
898
965
  /**
899
966
  * Calls {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
900
967
  * with the client as `this`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  const VoiceConnection = require('./VoiceConnection');
4
4
  const { Error } = require('../../errors');
5
- const { Events } = require('../../util/Constants');
5
+ const { Events, VoiceStatus, Opcodes } = require('../../util/Constants');
6
6
 
7
7
  /**
8
8
  * Manages voice connections for the client
@@ -60,6 +60,7 @@ class ClientVoiceManager {
60
60
 
61
61
  onVoiceStateUpdate(payload) {
62
62
  const { guild_id, session_id, channel_id } = payload;
63
+ if (payload.user_id !== this.client.user?.id) return;
63
64
  // @discordjs/voice
64
65
  if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
65
66
  this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
@@ -71,6 +72,9 @@ class ClientVoiceManager {
71
72
  this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`);
72
73
  if (!connection) return;
73
74
  if (!channel_id) {
75
+ if (connection.status === VoiceStatus.AUTHENTICATING || connection.status === VoiceStatus.CONNECTING) {
76
+ return;
77
+ }
74
78
  connection._disconnect();
75
79
  this.connection = null;
76
80
  return;
@@ -93,6 +97,73 @@ class ClientVoiceManager {
93
97
  * @typedef {Object} JoinChannelConfig
94
98
  */
95
99
 
100
+ /**
101
+ * Clears stale voice/stream state before joining a channel.
102
+ * @param {VoiceChannel} channel The channel to join
103
+ * @returns {Promise<void>}
104
+ * @private
105
+ */
106
+ preJoinCleanup(channel) {
107
+ return new Promise(resolve => {
108
+ const guild = channel.guild;
109
+ if (!guild) {
110
+ resolve();
111
+ return;
112
+ }
113
+
114
+ const userId = this.client.user?.id;
115
+ const voiceState = guild.voiceStates.cache.get(userId);
116
+ const inVoice = Boolean(voiceState?.channelId);
117
+ const isStreaming = Boolean(voiceState?.streaming);
118
+
119
+ if (this.connection) {
120
+ this.connection.disconnect();
121
+ this.connection = null;
122
+ }
123
+
124
+ if (!inVoice && !isStreaming) {
125
+ resolve();
126
+ return;
127
+ }
128
+
129
+ const streamKey = `guild:${guild.id}:${voiceState.channelId}:${userId}`;
130
+ let settled = false;
131
+ const done = () => {
132
+ if (settled) return;
133
+ settled = true;
134
+ this.client.removeListener(Events.VOICE_STATE_UPDATE, onState);
135
+ setTimeout(resolve, 500).unref();
136
+ };
137
+
138
+ const onState = (_old, newState) => {
139
+ if (newState.id !== userId || newState.guild?.id !== guild.id) return;
140
+ if (!newState.channelId && !newState.streaming) done();
141
+ };
142
+
143
+ this.client.on(Events.VOICE_STATE_UPDATE, onState);
144
+ this.client.emit('debug', `[VOICE] preJoinCleanup: quitte le salon ${voiceState.channelId}`);
145
+
146
+ if (isStreaming) {
147
+ this.client.ws.broadcast({
148
+ op: Opcodes.STREAM_DELETE,
149
+ d: { stream_key: streamKey },
150
+ });
151
+ }
152
+
153
+ this.client.ws.broadcast({
154
+ op: Opcodes.VOICE_STATE_UPDATE,
155
+ d: {
156
+ guild_id: guild.id,
157
+ channel_id: null,
158
+ self_mute: false,
159
+ self_deaf: false,
160
+ },
161
+ });
162
+
163
+ setTimeout(done, 5000).unref();
164
+ });
165
+ }
166
+
96
167
  /**
97
168
  * Sets up a request to join a voice channel.
98
169
  * @param {VoiceChannel | StageChannel | DMChannel | GroupDMChannel | Snowflake} channel The voice channel to join
@@ -106,28 +177,32 @@ class ClientVoiceManager {
106
177
  throw new Error('VOICE_JOIN_CHANNEL', channel.full);
107
178
  }
108
179
 
180
+ const startJoin = () => {
109
181
  let connection = this.connection;
110
182
 
111
- if (connection) {
112
- if (connection.channel.id !== channel.id) {
113
- this.connection.updateChannel(channel);
114
- }
183
+ if (connection?.status === VoiceStatus.CONNECTED && connection.channel.id === channel.id) {
115
184
  resolve(connection);
116
185
  return;
117
- } else {
118
- connection = new VoiceConnection(this, channel);
119
- if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
120
- connection.on('debug', msg =>
121
- this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
122
- );
123
- connection.authenticate({
124
- self_mute: Boolean(config.selfMute),
125
- self_deaf: Boolean(config.selfDeaf),
126
- self_video: Boolean(config.selfVideo),
127
- });
128
- this.connection = connection;
129
186
  }
130
187
 
188
+ if (connection) {
189
+ connection.disconnect();
190
+ this.connection = null;
191
+ connection = null;
192
+ }
193
+
194
+ connection = new VoiceConnection(this, channel);
195
+ if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
196
+ connection.on('debug', msg =>
197
+ this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
198
+ );
199
+ connection.authenticate({
200
+ self_mute: Boolean(config.selfMute),
201
+ self_deaf: Boolean(config.selfDeaf),
202
+ self_video: Boolean(config.selfVideo),
203
+ });
204
+ this.connection = connection;
205
+
131
206
  connection.once('failed', reason => {
132
207
  this.connection = null;
133
208
  reject(reason);
@@ -144,6 +219,9 @@ class ClientVoiceManager {
144
219
  this.connection = null;
145
220
  });
146
221
  });
222
+ };
223
+
224
+ this.preJoinCleanup(channel).then(startJoin).catch(reject);
147
225
  });
148
226
  }
149
227
  }
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { EventEmitter } = require('events');
7
+
8
+ /**
9
+ * Active screenshare session returned by {@link Client#startStream}.
10
+ * @extends {EventEmitter}
11
+ */
12
+ class StreamSession extends EventEmitter {
13
+ /**
14
+ * Downloads an HTTP(S) video to a temp file (recommended on VPS).
15
+ * @param {string} url Remote video URL
16
+ * @returns {Promise<string>} Local file path
17
+ */
18
+ static async resolveUrl(url) {
19
+ const response = await fetch(url);
20
+ if (!response.ok) throw new Error(`STREAM_URL_HTTP_${response.status}`);
21
+ const buffer = Buffer.from(await response.arrayBuffer());
22
+ if (buffer.length < 1024) throw new Error('STREAM_URL_INVALID');
23
+ const tmpPath = path.join(os.tmpdir(), `djs-stream-${Date.now()}.mp4`);
24
+ fs.writeFileSync(tmpPath, buffer);
25
+ console.log(`[stream] vidéo téléchargée: ${tmpPath} (${(buffer.length / 1024 / 1024).toFixed(2)} Mo)`);
26
+ return tmpPath;
27
+ }
28
+ /**
29
+ * @param {import('../Client')} client Discord client
30
+ * @param {import('./VoiceConnection')} voiceConnection Voice connection
31
+ * @param {import('./VoiceConnection').StreamConnection} streamConnection Stream connection
32
+ * @param {StartStreamOptions} options Stream options
33
+ */
34
+ constructor(client, voiceConnection, streamConnection, options) {
35
+ super();
36
+ this.client = client;
37
+ this.voiceConnection = voiceConnection;
38
+ this.streamConnection = streamConnection;
39
+ this.options = options;
40
+ this.videoDispatcher = null;
41
+ this.audioDispatcher = null;
42
+ this.voiceAudioDispatcher = null;
43
+ this._positionMs = 0;
44
+ this._stopped = false;
45
+
46
+ const onError = err => this.emit('error', err);
47
+ streamConnection.on('error', onError);
48
+ voiceConnection.on('error', onError);
49
+ }
50
+
51
+ _getPositionMs() {
52
+ if (!this.videoDispatcher) return this._positionMs;
53
+ return Math.max(0, this.videoDispatcher.totalStreamTime - this.videoDispatcher.pausedTime);
54
+ }
55
+
56
+ _play(seek = 0) {
57
+ const { url, fps, height, width, bitrate, audioBitrate } = this.options;
58
+ const streamHeight = height ?? 720;
59
+ this.streamConnection.videoAttributes = {
60
+ width: width ?? Math.round((streamHeight * 16) / 9),
61
+ height: streamHeight,
62
+ fps: fps ?? 30,
63
+ };
64
+ const videoOptions = {
65
+ fps,
66
+ bitrate,
67
+ seek,
68
+ presetH26x: this.options.preset || 'ultrafast',
69
+ outputFFmpegArgs: [
70
+ '-pix_fmt',
71
+ 'yuv420p',
72
+ '-g',
73
+ String(fps),
74
+ '-keyint_min',
75
+ String(fps),
76
+ '-sc_threshold',
77
+ '0',
78
+ '-force_key_frames',
79
+ 'expr:gte(t,n_forced*1)',
80
+ ],
81
+ };
82
+
83
+ if (height) {
84
+ videoOptions.outputFFmpegArgs.unshift('-vf', `scale=-2:${height}`);
85
+ }
86
+
87
+ if (!url.startsWith('http')) {
88
+ videoOptions.inputFFmpegArgs = ['-re'];
89
+ } else {
90
+ videoOptions.inputFFmpegArgs = ['-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1'];
91
+ }
92
+
93
+ this.videoDispatcher = this.streamConnection.playVideo(url, videoOptions);
94
+
95
+ if (this.options.audio !== false) {
96
+ const audioOptions = {
97
+ type: 'unknown',
98
+ seek,
99
+ bitrate: audioBitrate ?? 128,
100
+ };
101
+ if (!url.startsWith('http')) {
102
+ audioOptions.inputFFmpegArgs = ['-re'];
103
+ } else {
104
+ audioOptions.inputFFmpegArgs = ['-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1'];
105
+ }
106
+ this.voiceAudioDispatcher = this.voiceConnection.playAudio(url, audioOptions);
107
+ this.audioDispatcher = this.streamConnection.playAudio(url, audioOptions);
108
+ this.audioDispatcher.setSyncVideoDispatcher(this.videoDispatcher);
109
+ }
110
+
111
+ this._stopped = false;
112
+ this.videoDispatcher.once('finish', () => {
113
+ this._positionMs = 0;
114
+ this.emit('finish');
115
+ });
116
+
117
+ return this.videoDispatcher;
118
+ }
119
+
120
+ /**
121
+ * Pauses video and audio playback.
122
+ */
123
+ pause() {
124
+ this.videoDispatcher?.pause();
125
+ this.audioDispatcher?.pause(true);
126
+ this.voiceAudioDispatcher?.pause(true);
127
+ this.streamConnection.sendScreenshareState(true);
128
+ }
129
+
130
+ /**
131
+ * Resumes video and audio playback.
132
+ */
133
+ resume() {
134
+ this.streamConnection.sendScreenshareState(false);
135
+ this.videoDispatcher?.resume();
136
+ this.audioDispatcher?.resume();
137
+ this.voiceAudioDispatcher?.resume();
138
+ }
139
+
140
+ /**
141
+ * Stops playback while keeping voice/stream connections.
142
+ */
143
+ stop() {
144
+ this._positionMs = this._getPositionMs();
145
+ this._stopped = true;
146
+ this.videoDispatcher?.destroy();
147
+ this.audioDispatcher?.destroy();
148
+ this.voiceAudioDispatcher?.destroy();
149
+ this.videoDispatcher = null;
150
+ this.audioDispatcher = null;
151
+ this.voiceAudioDispatcher = null;
152
+ }
153
+
154
+ /**
155
+ * Resumes playback from the last position.
156
+ * @returns {import('./dispatcher/VideoDispatcher')}
157
+ */
158
+ replay() {
159
+ const seekSec = (this._stopped ? this._positionMs : this._getPositionMs()) / 1000;
160
+ this.stop();
161
+ return this._play(seekSec);
162
+ }
163
+
164
+ /**
165
+ * Stops playback and disconnects from voice and stream.
166
+ */
167
+ disconnect() {
168
+ this.stop();
169
+ this.streamConnection.disconnect();
170
+ this.voiceConnection.disconnect();
171
+ }
172
+ }
173
+
174
+ module.exports = StreamSession;
175
+
176
+ /**
177
+ * @typedef {Object} StartStreamOptions
178
+ * @property {import('../../util/Snowflake')} guildId Guild id
179
+ * @property {import('../../util/Snowflake')} channelId Voice channel id
180
+ * @property {string} url Video URL or file path
181
+ * @property {number} [fps=30] Video framerate
182
+ * @property {number} [height] Output height (width auto-scaled)
183
+ * @property {number} [width] Video width sent to Discord (default: 16:9 from height)
184
+ * @property {number} [bitrate=2000] Video bitrate in kbps
185
+ * @property {number} [audioBitrate=128] Audio bitrate in kbps
186
+ * @property {'H264' | 'VP8'} [videoCodec='H264'] Video codec
187
+ * @property {string} [preset='ultrafast'] x264 preset
188
+ * @property {boolean} [audio=true] Whether to play audio
189
+ * @property {boolean} [video=false] Enable webcam (false = screenshare only)
190
+ * @property {boolean} [downloadHttp=true] Download HTTP URLs locally before playback
191
+ * @property {number} [bitrateMax] Max video bitrate in kbps
192
+ * @property {boolean} [livestream=false] Enable readrateInitialBurst (live sources)
193
+ */