260 lines
7.1 KiB
TypeScript
260 lines
7.1 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|