Calling support (1)

This commit is contained in:
March 7th
2022-05-21 20:58:33 +07:00
parent cc9c4f3145
commit f9ec41c9b6
11 changed files with 303 additions and 39 deletions

View File

@@ -3,6 +3,7 @@
const process = require('node:process');
const { setInterval, setTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { getVoiceConnection } = require('@discordjs/voice');
const RichPresence = require('discord-rpc-contructor');
const BaseClient = require('./BaseClient');
const ActionsManager = require('./actions/ActionsManager');
@@ -15,6 +16,7 @@ const ClientUserSettingManager = require('../managers/ClientUserSettingManager')
const GuildManager = require('../managers/GuildManager');
const RelationshipsManager = require('../managers/RelationshipsManager');
const UserManager = require('../managers/UserManager');
const VoiceStateManager = require('../managers/VoiceStateManager');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const ClientPresence = require('../structures/ClientPresence');
const GuildPreview = require('../structures/GuildPreview');
@@ -113,6 +115,8 @@ class Client extends BaseClient {
*/
this.voice = new ClientVoiceManager(this);
this.voiceStates = new VoiceStateManager({ client: this });
/**
* Shard helpers for the client (only if the process was spawned from a {@link ShardingManager})
* @type {?ShardClientUtil}
@@ -248,6 +252,15 @@ class Client extends BaseClient {
return this.readyAt ? Date.now() - this.readyAt : null;
}
/**
* Get connection to current call
* @type {?VoiceConnection}
* @readonly
*/
get callVoice() {
return getVoiceConnection(null);
}
/**
* Update Cloudflare Cookie and Discord Fingerprint
*/
@@ -335,7 +348,6 @@ class Client extends BaseClient {
* client.QRLogin();
*/
QRLogin(debug = false) {
console.log(this.options);
const QR = new DiscordAuthWebsocket(this, debug);
this.emit(Events.DEBUG, `Preparing to connect to the gateway`, QR);
return QR;

View File

@@ -29,6 +29,26 @@ class VoiceStateUpdate extends Action {
client.voice.onVoiceStateUpdate(data);
}
/**
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
* @event Client#voiceStateUpdate
* @param {VoiceState} oldState The voice state before the update
* @param {VoiceState} newState The voice state after the update
*/
client.emit(Events.VOICE_STATE_UPDATE, oldState, newState);
} else {
// Update the state
const oldState =
client.voiceStates.cache.get(data.user_id)?._clone() ?? new VoiceState({ client }, { user_id: data.user_id });
const newState = client.voiceStates._add(data);
// Emit event
if (data.user_id === client.user.id) {
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
client.voice.onVoiceStateUpdate(data);
}
/**
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
* @event Client#voiceStateUpdate

View File

@@ -24,19 +24,26 @@ class ClientVoiceManager {
client.on(Events.SHARD_DISCONNECT, (_, shardId) => {
for (const [guildId, adapter] of this.adapters.entries()) {
if (client.guilds.cache.get(guildId)?.shardId === shardId) {
adapter.destroy();
// Because it has 1 shard => adapter.destroy();
}
adapter.destroy();
}
});
}
onVoiceServer(payload) {
this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
if (payload.guild_id) {
this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
} else {
this.adapters.get(payload.channel_id)?.onVoiceServerUpdate(payload);
}
}
onVoiceStateUpdate(payload) {
if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
} else if (payload.channel_id && payload.session_id && payload.user_id === this.client.user?.id) {
this.adapters.get(payload.channel_id)?.onVoiceStateUpdate(payload);
}
}
}

View File

@@ -1,10 +1,12 @@
'use strict';
let ClientUser;
const { VoiceConnection, VoiceConnectionStatus } = require('@discordjs/voice');
const axios = require('axios');
const chalk = require('chalk');
const Discord = require('../../../index');
const { Events, Opcodes } = require('../../../util/Constants');
const { Networking } = require('../../../util/Voice');
async function checkUpdate() {
const res_ = await axios.get(`https://registry.npmjs.com/${encodeURIComponent('discord.js-selfbot-v13')}`);
@@ -29,9 +31,42 @@ module.exports = (client, { d: data }, shard) => {
try {
checkUpdate();
} catch (e) {
console.log(e);
console.log(`${chalk.redBright('[Fail]')} Check Update error:`, e.message);
}
}
if (client.options.patchVoice) {
/* eslint-disable */
VoiceConnection.prototype.configureNetworking = function () {
const { server, state } = this.packets;
if (!server || !state || this.state.status === VoiceConnectionStatus.Destroyed || !server.endpoint) return;
const networking = new Networking(
{
endpoint: server.endpoint,
serverId: server.guild_id ?? server.channel_id,
token: server.token,
sessionId: state.session_id,
userId: state.user_id,
},
Boolean(this.debug),
);
networking.once('close', this.onNetworkingClose);
networking.on('stateChange', this.onNetworkingStateChange);
networking.on('error', this.onNetworkingError);
networking.on('debug', this.onNetworkingDebug);
this.state = {
...this.state,
status: VoiceConnectionStatus.Connecting,
networking,
};
};
client.emit(
Events.DEBUG,
`${chalk.greenBright('[OK]')} Patched VoiceConnection.prototype.configureNetworking [@discordjs/voice]`,
);
/* eslint-enable */
}
client.session_id = data.session_id;
if (client.user) {
client.user._patch(data.user);

View File

@@ -1,8 +1,10 @@
'use strict';
const { joinVoiceChannel, entersState, VoiceConnectionStatus } = require('@discordjs/voice');
const { Channel } = require('./Channel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
const { Status } = require('../util/Constants');
/**
* Represents a direct message channel between two users.
@@ -97,18 +99,58 @@ class DMChannel extends Channel {
// Testing feature: Call
// URL: https://discord.com/api/v9/channels/DMchannelId/call/ring
/**
* Call this DMChannel. [TEST only]
* @returns {Promise<void>}
* Call this DMChannel. Return discordjs/voice VoiceConnection
* @param {Object} options Options for the call (selfDeaf, selfMute: Boolean)
* @returns {Promise<VoiceConnection>}
*/
call() {
console.log('TEST only, and not working !');
return this.client.api.channels(this.id).call.ring.post({
usingApplicationJson: true,
data: {
recipients: null,
},
call(options = {}) {
return new Promise((resolve, reject) => {
this.client.api.channels(this.id).call.ring.post({
usingApplicationJson: true,
data: {
recipients: null,
},
});
const connection = joinVoiceChannel({
channelId: this.id,
guildId: null,
adapterCreator: this.voiceAdapterCreator,
selfDeaf: options.selfDeaf ?? false,
selfMute: options.selfMute ?? false,
});
entersState(connection, VoiceConnectionStatus.Ready, 30000)
.then(connection => {
resolve(connection);
})
.catch(err => {
connection.destroy();
reject(err);
});
});
}
get shard() {
return this.client.ws.shards.first();
}
get voiceAdapterCreator() {
return methods => {
this.client.voice.adapters.set(this.id, methods);
return {
sendPayload: data => {
data.d = {
...data.d,
self_video: false,
};
if (this.shard.status !== Status.READY) return false;
console.log('DM channel send payload', data);
this.shard.send(data);
return true;
},
destroy: () => {
this.client.voice.adapters.delete(this.id);
},
};
};
}
}
TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']);

View File

@@ -1,12 +1,14 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { joinVoiceChannel, entersState, VoiceConnectionStatus } = require('@discordjs/voice');
const { Channel } = require('./Channel');
const Invite = require('./Invite');
const User = require('./User');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { Error } = require('../errors');
const MessageManager = require('../managers/MessageManager');
const { Status } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
/**
@@ -204,18 +206,58 @@ class PartialGroupDMChannel extends Channel {
// Testing feature: Call
// URL: https://discord.com/api/v9/channels/DMchannelId/call/ring
/**
* Call this DMChannel. [TEST only]
* @returns {Promise<void>}
* Call this Group DMChannel. Return discordjs/voice VoiceConnection
* @param {Object} options Options for the call (selfDeaf, selfMute: Boolean)
* @returns {Promise<VoiceConnection>}
*/
call() {
console.log('TEST only, and not working !');
return this.client.api.channels(this.id).call.ring.post({
usingApplicationJson: true,
data: {
recipients: null,
},
call(options = {}) {
return new Promise((resolve, reject) => {
this.client.api.channels(this.id).call.ring.post({
usingApplicationJson: true,
data: {
recipients: null,
},
});
const connection = joinVoiceChannel({
channelId: this.id,
guildId: null,
adapterCreator: this.voiceAdapterCreator,
selfDeaf: options.selfDeaf ?? false,
selfMute: options.selfMute ?? false,
});
entersState(connection, VoiceConnectionStatus.Ready, 30000)
.then(connection => {
resolve(connection);
})
.catch(err => {
connection.destroy();
reject(err);
});
});
}
get shard() {
return this.client.ws.shards.first();
}
get voiceAdapterCreator() {
return methods => {
this.client.voice.adapters.set(this.id, methods);
return {
sendPayload: data => {
data.d = {
...data.d,
self_video: false,
};
if (this.shard.status !== Status.READY) return false;
console.log('DM channel send payload', data);
this.shard.send(data);
return true;
},
destroy: () => {
this.client.voice.adapters.delete(this.id);
},
};
};
}
}
TextBasedChannel.applyToClass(PartialGroupDMChannel, false);

View File

@@ -132,6 +132,7 @@ class VoiceState extends Base {
* @readonly
*/
get member() {
if (!this.guild?.id) return null;
return this.guild.members.cache.get(this.id) ?? null;
}
@@ -141,6 +142,7 @@ class VoiceState extends Base {
* @readonly
*/
get channel() {
if (!this.guild?.id) return null;
return this.guild.channels.cache.get(this.channelId) ?? null;
}
@@ -169,6 +171,7 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>}
*/
setMute(mute = true, reason) {
if (!this.guild?.id) return null;
return this.guild.members.edit(this.id, { mute }, reason);
}
@@ -179,6 +182,7 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>}
*/
setDeaf(deaf = true, reason) {
if (!this.guild?.id) return null;
return this.guild.members.edit(this.id, { deaf }, reason);
}
@@ -188,6 +192,7 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>}
*/
disconnect(reason) {
if (!this.guild?.id) return this.callVoice?.disconnect();
return this.setChannel(null, reason);
}
@@ -199,6 +204,7 @@ class VoiceState extends Base {
* @returns {Promise<GuildMember>}
*/
setChannel(channel, reason) {
if (!this.guild?.id) return null;
return this.guild.members.edit(this.id, { channel }, reason);
}
@@ -215,16 +221,18 @@ class VoiceState extends Base {
* @returns {Promise<void>}
*/
async setRequestToSpeak(request = true) {
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
if (this.guild?.id) {
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN');
if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN');
await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({
data: {
channel_id: this.channelId,
request_to_speak_timestamp: request ? new Date().toISOString() : null,
},
});
await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({
data: {
channel_id: this.channelId,
request_to_speak_timestamp: request ? new Date().toISOString() : null,
},
});
}
}
/**
@@ -245,18 +253,20 @@ class VoiceState extends Base {
* @returns {Promise<void>}
*/
async setSuppressed(suppressed = true) {
if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed');
if (this.guild?.id) {
if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed');
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL');
const target = this.client.user.id === this.id ? '@me' : this.id;
const target = this.client.user.id === this.id ? '@me' : this.id;
await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({
data: {
channel_id: this.channelId,
suppress: suppressed,
},
});
await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({
data: {
channel_id: this.channelId,
suppress: suppressed,
},
});
}
}
/**
@@ -264,6 +274,8 @@ class VoiceState extends Base {
* @returns {string} URL Image of the user's streaming video
*/
async getPreview() {
if (!this.guild?.id) return null;
if (!this.streaming) throw new Error('USER_NOT_STREAMING');
// URL: https://discord.com/api/v9/streams/guild:guildid:voicechannelid:userid/preview
const data = await this.client.api.streams[

View File

@@ -37,6 +37,7 @@ const JSONBig = require('json-bigint');
* @property {boolean} [checkUpdate=true] Check for module updates at startup
* @property {boolean} [readyStatus=true] Sync state with Discord Client
* @property {boolean} [autoCookie=true] Automatically add Cookies to Request on startup
* @property {boolean} [patchVoice=true] Automatically patch @discordjs/voice module (support for call)
* @property {number} [shardCount=1] The total amount of shards used by all processes of this bot
* (e.g. recommended shard count, shard count of the ShardingManager)
* @property {CacheFactory} [makeCache] Function to create a cache.
@@ -140,6 +141,7 @@ class Options extends null {
checkUpdate: true,
readyStatus: true,
autoCookie: true,
patchVoice: true,
waitGuildTimeout: 15_000,
shardCount: 1,
makeCache: this.cacheWithLimits(this.defaultMakeCacheSettings),