djs-selfbot-v13 3.7.32 → 3.7.33

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.33",
4
4
  "description": "An unofficial discord.js fork for creating selfbots",
5
5
  "main": "./src/index.js",
6
6
  "types": "./typings/index.d.ts",
@@ -9,6 +9,7 @@ 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');
12
13
  const WebSocketManager = require('./websocket/WebSocketManager');
13
14
  const { Error, TypeError } = require('../errors');
14
15
  const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
@@ -36,7 +37,7 @@ const VoiceRegion = require('../structures/VoiceRegion');
36
37
  const Webhook = require('../structures/Webhook');
37
38
  const Widget = require('../structures/Widget');
38
39
  const Application = require('../structures/interfaces/Application');
39
- const { Events, Status } = require('../util/Constants');
40
+ const { Events, Status, VoiceStatus } = require('../util/Constants');
40
41
  const DataResolver = require('../util/DataResolver');
41
42
  const Intents = require('../util/Intents');
42
43
  const DiscordAuthWebsocket = require('../util/RemoteAuth');
@@ -895,6 +896,78 @@ class Client extends BaseClient {
895
896
  });
896
897
  }
897
898
 
899
+ /**
900
+ * Joins a voice channel, starts a screenshare stream and plays a video.
901
+ * @param {import('./voice/StreamSession').StartStreamOptions} options Stream options
902
+ * @returns {Promise<import('./voice/StreamSession')>}
903
+ * @example
904
+ * const stream = await client.startStream({
905
+ * guildId: '123',
906
+ * channelId: '456',
907
+ * url: 'video.mp4',
908
+ * fps: 60,
909
+ * height: 720,
910
+ * bitrate: 4500,
911
+ * });
912
+ * stream.pause();
913
+ * stream.resume();
914
+ * stream.stop();
915
+ * stream.replay();
916
+ * stream.disconnect();
917
+ */
918
+ async startStream(options = {}) {
919
+ const {
920
+ guildId,
921
+ channelId,
922
+ url,
923
+ fps = 30,
924
+ height,
925
+ width,
926
+ bitrate = 2000,
927
+ audioBitrate,
928
+ videoCodec = 'H264',
929
+ preset,
930
+ audio,
931
+ video = false,
932
+ } = options;
933
+
934
+ if (!url) throw new Error('STREAM_URL_REQUIRED');
935
+ if (!guildId || !channelId) throw new Error('STREAM_CHANNEL_REQUIRED');
936
+
937
+ const channel =
938
+ this.guilds.cache.get(guildId)?.channels.cache.get(channelId) ?? this.channels.cache.get(channelId);
939
+
940
+ if (!channel) throw new Error('STREAM_CHANNEL_NOT_FOUND');
941
+
942
+ const voiceConnection = await this.voice.joinChannel(channel, {
943
+ selfVideo: Boolean(video),
944
+ videoCodec,
945
+ });
946
+
947
+ const streamConnection = await voiceConnection.createStreamConnection();
948
+
949
+ if (!video && voiceConnection.status === VoiceStatus.CONNECTED) {
950
+ voiceConnection.setVideoStatus(false);
951
+ }
952
+
953
+ await streamConnection.waitForDaveReady();
954
+ streamConnection.sendScreenshareState(false);
955
+
956
+ const session = new StreamSession(this, voiceConnection, streamConnection, {
957
+ url,
958
+ fps,
959
+ height,
960
+ width,
961
+ bitrate,
962
+ audioBitrate,
963
+ preset,
964
+ audio,
965
+ });
966
+
967
+ session._play();
968
+ return session;
969
+ }
970
+
898
971
  /**
899
972
  * Calls {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
900
973
  * 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,62 @@ 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 userId = this.client.user?.id;
109
+ const voiceState = channel.guild?.voiceStates.cache.get(userId);
110
+ const inTarget = voiceState?.channelId === channel.id;
111
+ const isStreaming = voiceState?.streaming || this.client.user?.voice?.streaming;
112
+ const needsCleanup = inTarget && (isStreaming || !this.connection);
113
+
114
+ if (!needsCleanup) {
115
+ resolve();
116
+ return;
117
+ }
118
+
119
+ const streamKey = `guild:${channel.guild.id}:${channel.id}:${userId}`;
120
+ let settled = false;
121
+ const done = () => {
122
+ if (settled) return;
123
+ settled = true;
124
+ this.client.removeListener(Events.VOICE_STATE_UPDATE, onState);
125
+ setTimeout(resolve, 400).unref();
126
+ };
127
+
128
+ const onState = (_old, newState) => {
129
+ if (newState.id !== userId) return;
130
+ if (!newState.channelId && !newState.streaming) done();
131
+ };
132
+
133
+ this.client.on(Events.VOICE_STATE_UPDATE, onState);
134
+
135
+ if (isStreaming) {
136
+ this.client.ws.broadcast({
137
+ op: Opcodes.STREAM_DELETE,
138
+ d: { stream_key: streamKey },
139
+ });
140
+ }
141
+
142
+ this.client.ws.broadcast({
143
+ op: Opcodes.VOICE_STATE_UPDATE,
144
+ d: {
145
+ guild_id: channel.guild.id,
146
+ channel_id: null,
147
+ self_mute: false,
148
+ self_deaf: false,
149
+ },
150
+ });
151
+
152
+ setTimeout(done, 4000).unref();
153
+ });
154
+ }
155
+
96
156
  /**
97
157
  * Sets up a request to join a voice channel.
98
158
  * @param {VoiceChannel | StageChannel | DMChannel | GroupDMChannel | Snowflake} channel The voice channel to join
@@ -106,28 +166,32 @@ class ClientVoiceManager {
106
166
  throw new Error('VOICE_JOIN_CHANNEL', channel.full);
107
167
  }
108
168
 
169
+ const startJoin = () => {
109
170
  let connection = this.connection;
110
171
 
111
- if (connection) {
112
- if (connection.channel.id !== channel.id) {
113
- this.connection.updateChannel(channel);
114
- }
172
+ if (connection?.status === VoiceStatus.CONNECTED && connection.channel.id === channel.id) {
115
173
  resolve(connection);
116
174
  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
175
  }
130
176
 
177
+ if (connection) {
178
+ connection.disconnect();
179
+ this.connection = null;
180
+ connection = null;
181
+ }
182
+
183
+ connection = new VoiceConnection(this, channel);
184
+ if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
185
+ connection.on('debug', msg =>
186
+ this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
187
+ );
188
+ connection.authenticate({
189
+ self_mute: Boolean(config.selfMute),
190
+ self_deaf: Boolean(config.selfDeaf),
191
+ self_video: Boolean(config.selfVideo),
192
+ });
193
+ this.connection = connection;
194
+
131
195
  connection.once('failed', reason => {
132
196
  this.connection = null;
133
197
  reject(reason);
@@ -144,6 +208,9 @@ class ClientVoiceManager {
144
208
  this.connection = null;
145
209
  });
146
210
  });
211
+ };
212
+
213
+ this.preJoinCleanup(channel).then(startJoin).catch(reject);
147
214
  });
148
215
  }
149
216
  }
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ /**
6
+ * Active screenshare session returned by {@link Client#startStream}.
7
+ * @extends {EventEmitter}
8
+ */
9
+ class StreamSession extends EventEmitter {
10
+ /**
11
+ * @param {import('../Client')} client Discord client
12
+ * @param {import('./VoiceConnection')} voiceConnection Voice connection
13
+ * @param {import('./VoiceConnection').StreamConnection} streamConnection Stream connection
14
+ * @param {StartStreamOptions} options Stream options
15
+ */
16
+ constructor(client, voiceConnection, streamConnection, options) {
17
+ super();
18
+ this.client = client;
19
+ this.voiceConnection = voiceConnection;
20
+ this.streamConnection = streamConnection;
21
+ this.options = options;
22
+ this.videoDispatcher = null;
23
+ this.audioDispatcher = null;
24
+ this.voiceAudioDispatcher = null;
25
+ this._positionMs = 0;
26
+ this._stopped = false;
27
+
28
+ const onError = err => this.emit('error', err);
29
+ streamConnection.on('error', onError);
30
+ voiceConnection.on('error', onError);
31
+ }
32
+
33
+ _getPositionMs() {
34
+ if (!this.videoDispatcher) return this._positionMs;
35
+ return Math.max(0, this.videoDispatcher.totalStreamTime - this.videoDispatcher.pausedTime);
36
+ }
37
+
38
+ _play(seek = 0) {
39
+ const { url, fps, height, width, bitrate, audioBitrate } = this.options;
40
+ const streamHeight = height ?? 720;
41
+ this.streamConnection.videoAttributes = {
42
+ width: width ?? Math.round((streamHeight * 16) / 9),
43
+ height: streamHeight,
44
+ fps: fps ?? 30,
45
+ };
46
+ const videoOptions = {
47
+ fps,
48
+ bitrate,
49
+ seek,
50
+ presetH26x: this.options.preset || 'ultrafast',
51
+ outputFFmpegArgs: [
52
+ '-pix_fmt',
53
+ 'yuv420p',
54
+ '-g',
55
+ String(fps),
56
+ '-keyint_min',
57
+ String(fps),
58
+ '-sc_threshold',
59
+ '0',
60
+ '-force_key_frames',
61
+ 'expr:gte(t,n_forced*1)',
62
+ ],
63
+ };
64
+
65
+ if (height) {
66
+ videoOptions.outputFFmpegArgs.unshift('-vf', `scale=-2:${height}`);
67
+ }
68
+
69
+ if (!url.startsWith('http')) {
70
+ videoOptions.inputFFmpegArgs = ['-re'];
71
+ }
72
+
73
+ this.videoDispatcher = this.streamConnection.playVideo(url, videoOptions);
74
+
75
+ if (this.options.audio !== false) {
76
+ const audioOptions = {
77
+ type: 'unknown',
78
+ seek,
79
+ bitrate: audioBitrate ?? 128,
80
+ };
81
+ if (!url.startsWith('http')) {
82
+ audioOptions.inputFFmpegArgs = ['-re'];
83
+ }
84
+ this.voiceAudioDispatcher = this.voiceConnection.playAudio(url, audioOptions);
85
+ this.audioDispatcher = this.streamConnection.playAudio(url, audioOptions);
86
+ this.audioDispatcher.setSyncVideoDispatcher(this.videoDispatcher);
87
+ }
88
+
89
+ this._stopped = false;
90
+ this.videoDispatcher.once('finish', () => {
91
+ this._positionMs = 0;
92
+ this.emit('finish');
93
+ });
94
+
95
+ return this.videoDispatcher;
96
+ }
97
+
98
+ /**
99
+ * Pauses video and audio playback.
100
+ */
101
+ pause() {
102
+ this.videoDispatcher?.pause();
103
+ this.audioDispatcher?.pause(true);
104
+ this.voiceAudioDispatcher?.pause(true);
105
+ this.streamConnection.sendScreenshareState(true);
106
+ }
107
+
108
+ /**
109
+ * Resumes video and audio playback.
110
+ */
111
+ resume() {
112
+ this.streamConnection.sendScreenshareState(false);
113
+ this.videoDispatcher?.resume();
114
+ this.audioDispatcher?.resume();
115
+ this.voiceAudioDispatcher?.resume();
116
+ }
117
+
118
+ /**
119
+ * Stops playback while keeping voice/stream connections.
120
+ */
121
+ stop() {
122
+ this._positionMs = this._getPositionMs();
123
+ this._stopped = true;
124
+ this.videoDispatcher?.destroy();
125
+ this.audioDispatcher?.destroy();
126
+ this.voiceAudioDispatcher?.destroy();
127
+ this.videoDispatcher = null;
128
+ this.audioDispatcher = null;
129
+ this.voiceAudioDispatcher = null;
130
+ }
131
+
132
+ /**
133
+ * Resumes playback from the last position.
134
+ * @returns {import('./dispatcher/VideoDispatcher')}
135
+ */
136
+ replay() {
137
+ const seekSec = (this._stopped ? this._positionMs : this._getPositionMs()) / 1000;
138
+ this.stop();
139
+ return this._play(seekSec);
140
+ }
141
+
142
+ /**
143
+ * Stops playback and disconnects from voice and stream.
144
+ */
145
+ disconnect() {
146
+ this.stop();
147
+ this.streamConnection.disconnect();
148
+ this.voiceConnection.disconnect();
149
+ }
150
+ }
151
+
152
+ module.exports = StreamSession;
153
+
154
+ /**
155
+ * @typedef {Object} StartStreamOptions
156
+ * @property {import('../../util/Snowflake')} guildId Guild id
157
+ * @property {import('../../util/Snowflake')} channelId Voice channel id
158
+ * @property {string} url Video URL or file path
159
+ * @property {number} [fps=30] Video framerate
160
+ * @property {number} [height] Output height (width auto-scaled)
161
+ * @property {number} [width] Video width sent to Discord (default: 16:9 from height)
162
+ * @property {number} [bitrate=2000] Video bitrate in kbps
163
+ * @property {number} [audioBitrate=128] Audio bitrate in kbps
164
+ * @property {'H264' | 'VP8'} [videoCodec='H264'] Video codec
165
+ * @property {string} [preset='ultrafast'] x264 preset
166
+ * @property {boolean} [audio=true] Whether to play audio
167
+ * @property {boolean} [video=false] Enable webcam (false = screenshare only)
168
+ */