refactor: vendor content as regular files (remove git submodules) so CI builds don't need GitHub auth
This commit is contained in:
+259
@@ -0,0 +1,259 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { VoiceConnection } from "./voice/VoiceConnection.js";
|
||||
import { StreamConnection } from "./voice/StreamConnection.js";
|
||||
import { GatewayOpCodes } from "./GatewayOpCodes.js";
|
||||
import type {
|
||||
Client,
|
||||
DMChannel,
|
||||
GroupDMChannel,
|
||||
VoiceBasedChannel,
|
||||
} from "discord.js-selfbot-v13";
|
||||
import type { GatewayEvent, GatewayEventMap } from "./GatewayEvents.js";
|
||||
import type { WebRtcConnWrapper } from "./voice/WebRtcWrapper.js";
|
||||
import { generateStreamKey, parseStreamKey } from "../utils.js";
|
||||
|
||||
export class Streamer {
|
||||
private _voiceConnection?: VoiceConnection;
|
||||
private _client: Client;
|
||||
private _gatewayEmitter = new EventEmitter<GatewayEventMap>();
|
||||
|
||||
constructor(client: Client) {
|
||||
this._client = client;
|
||||
|
||||
//listen for messages
|
||||
this.client.on("raw", (packet: GatewayEvent) => {
|
||||
// @ts-expect-error I don't know how to make this work with TypeScript, so whatever
|
||||
this._gatewayEmitter.emit(packet.t, packet.d);
|
||||
});
|
||||
}
|
||||
|
||||
public get client(): Client {
|
||||
return this._client;
|
||||
}
|
||||
|
||||
public get opts() {
|
||||
return {};
|
||||
}
|
||||
|
||||
public get voiceConnection(): VoiceConnection | undefined {
|
||||
return this._voiceConnection;
|
||||
}
|
||||
|
||||
public sendOpcode(code: number, data: unknown): void {
|
||||
this.client.ws.broadcast({
|
||||
op: code,
|
||||
d: data,
|
||||
});
|
||||
}
|
||||
|
||||
public joinVoiceChannel(
|
||||
channel: DMChannel | GroupDMChannel | VoiceBasedChannel,
|
||||
): Promise<WebRtcConnWrapper> {
|
||||
let guildId: string | null = null;
|
||||
|
||||
if (
|
||||
channel.type === "GUILD_STAGE_VOICE" ||
|
||||
channel.type === "GUILD_VOICE"
|
||||
) {
|
||||
guildId = channel.guildId;
|
||||
}
|
||||
|
||||
return this.joinVoice(guildId, channel.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a voice channel and returns a WebRtcConnWrapper object.
|
||||
* @param guild_id the guild id of the voice channel. If null, it will join a DM voice channel.
|
||||
* @param channel_id the channel id of the voice channel
|
||||
* @returns the WebRtcConnWrapper object
|
||||
* @throws Error if the client is not logged in
|
||||
*/
|
||||
public joinVoice(
|
||||
guild_id: string | null,
|
||||
channel_id: string,
|
||||
): Promise<WebRtcConnWrapper> {
|
||||
return new Promise<WebRtcConnWrapper>((resolve, reject) => {
|
||||
if (!this.client.user) {
|
||||
reject("Client not logged in");
|
||||
return;
|
||||
}
|
||||
const user_id = this.client.user.id;
|
||||
const voiceConn = new VoiceConnection(
|
||||
this,
|
||||
guild_id,
|
||||
user_id,
|
||||
channel_id,
|
||||
(conn) => {
|
||||
resolve(conn);
|
||||
},
|
||||
);
|
||||
this._voiceConnection = voiceConn;
|
||||
this._gatewayEmitter.on("VOICE_STATE_UPDATE", (d) => {
|
||||
if (user_id !== d.user_id) return;
|
||||
voiceConn.setSession(d.session_id);
|
||||
});
|
||||
this._gatewayEmitter.on("VOICE_SERVER_UPDATE", (d) => {
|
||||
if (guild_id !== d.guild_id) return;
|
||||
|
||||
// channel_id is not set for guild voice calls
|
||||
if (d.channel_id && channel_id !== d.channel_id) return;
|
||||
|
||||
voiceConn.setTokens(d.endpoint, d.token);
|
||||
});
|
||||
this.signalVideo(false);
|
||||
});
|
||||
}
|
||||
|
||||
public createStream(): Promise<WebRtcConnWrapper> {
|
||||
return new Promise<WebRtcConnWrapper>((resolve, reject) => {
|
||||
if (!this.client.user) {
|
||||
reject("Client not logged in");
|
||||
return;
|
||||
}
|
||||
if (!this.voiceConnection) {
|
||||
reject("cannot start stream without first joining voice channel");
|
||||
return;
|
||||
}
|
||||
|
||||
this.signalStream();
|
||||
const {
|
||||
guildId: clientGuildId,
|
||||
channelId: clientChannelId,
|
||||
session_id,
|
||||
} = this.voiceConnection;
|
||||
const { id: clientUserId } = this.client.user;
|
||||
|
||||
if (!session_id) throw new Error("Session doesn't exist yet");
|
||||
const streamConn = new StreamConnection(
|
||||
this,
|
||||
clientGuildId,
|
||||
clientUserId,
|
||||
clientChannelId,
|
||||
(conn) => {
|
||||
resolve(conn);
|
||||
},
|
||||
);
|
||||
this.voiceConnection.streamConnection = streamConn;
|
||||
this._gatewayEmitter.on("STREAM_CREATE", (d) => {
|
||||
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
||||
|
||||
if (
|
||||
clientGuildId !== guildId ||
|
||||
clientChannelId !== channelId ||
|
||||
clientUserId !== userId
|
||||
)
|
||||
return;
|
||||
|
||||
streamConn.serverId = d.rtc_server_id;
|
||||
streamConn.streamKey = d.stream_key;
|
||||
streamConn.setSession(session_id);
|
||||
});
|
||||
this._gatewayEmitter.on("STREAM_SERVER_UPDATE", (d) => {
|
||||
const { channelId, guildId, userId } = parseStreamKey(d.stream_key);
|
||||
|
||||
if (
|
||||
clientGuildId !== guildId ||
|
||||
clientChannelId !== channelId ||
|
||||
clientUserId !== userId
|
||||
)
|
||||
return;
|
||||
|
||||
streamConn.setTokens(d.endpoint, d.token);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async setStreamPreview(image: Buffer): Promise<void> {
|
||||
if (!this.client.token) throw new Error("Please login :)");
|
||||
if (!this.voiceConnection?.streamConnection?.guildId) return;
|
||||
const data = `data:image/jpeg;base64,${image.toString("base64")}`;
|
||||
const { guildId } = this.voiceConnection.streamConnection;
|
||||
const server = await this.client.guilds.fetch(guildId);
|
||||
await server.members.me?.voice.postPreview(data);
|
||||
}
|
||||
|
||||
public stopStream(): void {
|
||||
const stream = this.voiceConnection?.streamConnection;
|
||||
|
||||
if (!stream) return;
|
||||
|
||||
stream.stop();
|
||||
|
||||
this.signalStopStream();
|
||||
|
||||
this.voiceConnection.streamConnection = undefined;
|
||||
this._gatewayEmitter.removeAllListeners("STREAM_CREATE");
|
||||
this._gatewayEmitter.removeAllListeners("STREAM_SERVER_UPDATE");
|
||||
}
|
||||
|
||||
public leaveVoice(): void {
|
||||
this.voiceConnection?.stop();
|
||||
|
||||
this.signalLeaveVoice();
|
||||
|
||||
this._voiceConnection = undefined;
|
||||
this._gatewayEmitter.removeAllListeners("VOICE_STATE_UPDATE");
|
||||
this._gatewayEmitter.removeAllListeners("VOICE_SERVER_UPDATE");
|
||||
}
|
||||
|
||||
public signalVideo(video_enabled: boolean): void {
|
||||
if (!this.voiceConnection) return;
|
||||
const { guildId: guild_id, channelId: channel_id } = this.voiceConnection;
|
||||
this.sendOpcode(GatewayOpCodes.VOICE_STATE_UPDATE, {
|
||||
guild_id: guild_id,
|
||||
channel_id,
|
||||
self_mute: false,
|
||||
self_deaf: true,
|
||||
self_video: video_enabled,
|
||||
});
|
||||
}
|
||||
|
||||
public signalStream(): void {
|
||||
if (!this.voiceConnection) return;
|
||||
const {
|
||||
type,
|
||||
guildId: guild_id,
|
||||
channelId: channel_id,
|
||||
botId: user_id,
|
||||
} = this.voiceConnection;
|
||||
|
||||
const streamKey = generateStreamKey(type, guild_id, channel_id, user_id);
|
||||
|
||||
this.sendOpcode(GatewayOpCodes.STREAM_CREATE, {
|
||||
type,
|
||||
guild_id,
|
||||
channel_id,
|
||||
preferred_region: null,
|
||||
});
|
||||
|
||||
this.sendOpcode(GatewayOpCodes.STREAM_SET_PAUSED, {
|
||||
stream_key: streamKey,
|
||||
paused: false,
|
||||
});
|
||||
}
|
||||
|
||||
public signalStopStream(): void {
|
||||
if (!this.voiceConnection) return;
|
||||
const {
|
||||
type,
|
||||
guildId: guild_id,
|
||||
channelId: channel_id,
|
||||
botId: user_id,
|
||||
} = this.voiceConnection;
|
||||
|
||||
const streamKey = generateStreamKey(type, guild_id, channel_id, user_id);
|
||||
this.sendOpcode(GatewayOpCodes.STREAM_DELETE, {
|
||||
stream_key: streamKey,
|
||||
});
|
||||
}
|
||||
|
||||
public signalLeaveVoice(): void {
|
||||
this.sendOpcode(GatewayOpCodes.VOICE_STATE_UPDATE, {
|
||||
guild_id: null,
|
||||
channel_id: null,
|
||||
self_mute: true,
|
||||
self_deaf: false,
|
||||
self_video: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user