diff --git a/composer.json b/composer.json index efa1f1181..2bf94f1f9 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "react/async": "^4.0 || ^3.0", "react/cache": "^0.5 || ^0.6 || ^1.0", "react/promise": "^3.0.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "discord-php-helpers/voice": "dev-main" }, "require-dev": { "symfony/var-dumper": "*", diff --git a/src/Discord/Builders/ChannelBuilder.php b/src/Discord/Builders/ChannelBuilder.php index fe2ff7e4a..16a32cb6a 100644 --- a/src/Discord/Builders/ChannelBuilder.php +++ b/src/Discord/Builders/ChannelBuilder.php @@ -14,21 +14,8 @@ namespace Discord\Builders; use Discord\Http\Exceptions\RequestFailedException; -use Discord\Parts\Channel\AnnouncementThread; use Discord\Parts\Channel\Channel; -use Discord\Parts\Channel\DM; -use Discord\Parts\Channel\GroupDM; -use Discord\Parts\Channel\GuildAnnouncement; -use Discord\Parts\Channel\GuildCategory; -use Discord\Parts\Channel\GuildDirectory; -use Discord\Parts\Channel\GuildForum; -use Discord\Parts\Channel\GuildMedia; -use Discord\Parts\Channel\GuildStageVoice; -use Discord\Parts\Channel\GuildText; -use Discord\Parts\Channel\GuildVoice; use Discord\Parts\Channel\Overwrite; -use Discord\Parts\Channel\PrivateThread; -use Discord\Parts\Channel\PublicThread; use Discord\Parts\Guild\Emoji; use Discord\Voice\Region; use JsonSerializable; diff --git a/src/Discord/Discord.php b/src/Discord/Discord.php index 5b0dc96d2..70fd90305 100644 --- a/src/Discord/Discord.php +++ b/src/Discord/Discord.php @@ -42,6 +42,7 @@ use Discord\Repository\PrivateChannelRepository; use Discord\Repository\SoundRepository; use Discord\Repository\UserRepository; +use Discord\Voice\Manager; use Discord\Voice\Region; use Discord\Voice\VoiceClient; use Discord\WebSockets\Event; @@ -54,6 +55,7 @@ use Evenement\EventEmitterTrait; use Monolog\Formatter\LineFormatter; use Monolog\Handler\StreamHandler; +use Monolog\Level; use Monolog\Logger as Monolog; use Psr\Log\LoggerInterface; use Ratchet\Client\Connector; @@ -211,14 +213,14 @@ class Discord * * @var VoiceClient[] Voice Clients. */ - protected $voiceClients = []; + public array $voiceClients = []; /** * An array of voice session IDs. * * @var string[] Voice Sessions. */ - protected $voice_sessions = []; + public array $voice_sessions = []; /** * An array of large guilds that need to be requested for members. @@ -368,6 +370,13 @@ class Discord */ protected $application_commands; + /** + * The voice handler, of clients and packets. + * + * @var Manager + */ + public Manager $voice; + /** * The transport compression setting. * @@ -663,12 +672,11 @@ protected function handleVoiceStateUpdate(object $data): void $voiceStateUpdate = $this->factory->part(VoiceStateUpdate::class, (array) $data->d, true); $this->logger->debug('voice state update received', ['guild' => $voiceStateUpdate->guild_id, 'data' => $voiceStateUpdate]); - if (! isset($this->voiceClients[$voiceStateUpdate->guild_id])) { - $this->logger->warning('voice client not found', ['guild' => $voiceStateUpdate->guild_id]); - - return; + if (isset($this->voice->clients[$data->d->guild_id])) { + /** @var VoiceClient */ + $client = $this->voice->clients[$data->d->guild_id]; + $client->handleVoiceStateUpdate($data->d); } - $this->voiceClients[$voiceStateUpdate->guild_id]->handleVoiceStateUpdate($voiceStateUpdate); } /** @@ -1375,7 +1383,7 @@ public function connectWs(): void * * @param Payload|array $data Packet data. */ - protected function send(object|array $data, bool $force = false): void + public function send(object|array $data, bool $force = false): void { // Wait until payload count has been reset // Keep 5 payloads for heartbeats as required @@ -1401,6 +1409,9 @@ protected function ready() } $this->emittedInit = true; + $this->voice = new Manager($this); + $this->logger->info('voice class initialized'); + $this->logger->info('client is ready'); $this->emit('init', [$this]); @@ -1465,44 +1476,9 @@ public function getVoiceClient(string $guild_id): ?VoiceClient * * @return PromiseInterface */ - public function joinVoiceChannel(Channel $channel, $mute = false, $deaf = true, ?LoggerInterface $logger = null): PromiseInterface + public function joinVoiceChannel(Channel $channel, $mute = false, $deaf = true): PromiseInterface { - $deferred = new Deferred(); - - if (! $channel->isVoiceBased()) { - $deferred->reject(new \RuntimeException('Channel must allow voice.')); - - return $deferred->promise(); - } - - if (isset($this->voiceClients[$channel->guild_id])) { - $deferred->reject(new \RuntimeException('You cannot join more than one voice channel per guild.')); - - return $deferred->promise(); - } - - $data = [ - 'user_id' => $this->id, - 'deaf' => $deaf, - 'mute' => $mute, - ]; - - $this->on(Event::VOICE_STATE_UPDATE, fn ($vs, $discord) => $this->voiceStateUpdate($vs, $channel, $data)); - $this->on(Event::VOICE_SERVER_UPDATE, fn ($vs, $discord) => $this->voiceServerUpdate($vs, $channel, $data, $deferred, $logger)); - - $payload = Payload::new( - Op::OP_UPDATE_VOICE_STATE, - [ - 'guild_id' => $channel->guild_id, - 'channel_id' => $channel->id, - 'self_mute' => $mute, - 'self_deaf' => $deaf, - ], - ); - - $this->send($payload); - - return $deferred->promise(); + return $this->voice->joinChannel($channel, $this, $this->voice_sessions, $mute, $deaf); } protected function voiceStateUpdate($vs, $channel, &$data) @@ -1527,11 +1503,16 @@ protected function voiceServerUpdate(VoiceServerUpdate $vs, Channel $channel, ar $data['dnsConfig'] = $this->options['dnsConfig']; $this->logger->info('received token and endpoint for voice session', ['guild' => $channel->guild_id, 'token' => $vs->token, 'endpoint' => $vs->endpoint]); - $vc = new VoiceClient($this, $this->ws, $this->voice_sessions, $channel, $data); + $new = false; + $manager = null; + if (! isset($this->voiceClients[$channel->guild_id])) { + $new = true; + $manager = new Manager($this); + } + $this->voiceClients[$channel->guild_id] ??= $vc = $this->voiceClients[$channel->guild_id] ?? new VoiceClient($this, $channel, $this->voice_sessions, $data, deferred: $deferred, manager: $manager); $vc->once('ready', function () use ($vc, $deferred, $channel) { $this->logger->info('voice client is ready'); - $this->voiceClients[$channel->guild_id] = $vc; $deferred->resolve($vc); }); $vc->once('error', function ($e) use ($deferred) { @@ -1543,7 +1524,21 @@ protected function voiceServerUpdate(VoiceServerUpdate $vs, Channel $channel, ar unset($this->voiceClients[$channel->guild_id]); }); - $vc->start(); + if ($new) { + $vc->boot(); + } else { + $vc->setData( + array_merge( + $vc->data, + [ + 'token' => $vs->token, + 'endpoint' => $vs->endpoint, + 'session' => $vc->data['session'] ?? null, + ], + ['dnsConfig' => $this->options['dnsConfig']] + ) + ); + } $this->voiceLoggers[$channel->guild_id] = $this->logger; $this->removeListener(Event::VOICE_SERVER_UPDATE, fn () => $this->voiceServerUpdate($vs, $channel, $data, $deferred, $logger)); @@ -1716,7 +1711,7 @@ protected function resolveOptions(array $options = []): array $options['loop'] ??= Loop::get(); if (null === $options['logger']) { - $streamHandler = new StreamHandler('php://stdout', Monolog::DEBUG); + $streamHandler = new StreamHandler('php://stdout', Level::Debug); $lineFormatter = new LineFormatter(null, null, true, true); $streamHandler->setFormatter($lineFormatter); $logger = new Monolog('DiscordPHP', [$streamHandler]); @@ -1892,7 +1887,7 @@ public function getCacheConfig($repository_class = AbstractRepository::class) */ public function __get(string $name) { - static $allowed = ['loop', 'options', 'logger', 'http', 'application_commands', 'voice_sessions']; + static $allowed = ['loop', 'options', 'logger', 'http', 'application_commands']; if (in_array($name, $allowed)) { return $this->{$name}; diff --git a/src/Discord/Parts/Channel/Channel.php b/src/Discord/Parts/Channel/Channel.php index 8a7b7ea53..e05b80cef 100644 --- a/src/Discord/Parts/Channel/Channel.php +++ b/src/Discord/Parts/Channel/Channel.php @@ -119,7 +119,7 @@ class Channel extends Part implements Stringable public const TYPE_GUILD_MEDIA = 16; public const TYPES = [ - self::TYPE_GUILD_TEXT => GuildText::class, + self::TYPE_GUILD_TEXT => GuildText::class, self::TYPE_DM => DM::class, self::TYPE_GUILD_VOICE => GuildVoice::class, self::TYPE_GROUP_DM => GroupDM::class, diff --git a/src/Discord/Parts/Channel/GuildMedia.php b/src/Discord/Parts/Channel/GuildMedia.php index 436907098..0f1c7b5fc 100644 --- a/src/Discord/Parts/Channel/GuildMedia.php +++ b/src/Discord/Parts/Channel/GuildMedia.php @@ -15,7 +15,7 @@ /** * Channel that can only contain threads, similar to GUILD_FORUM channels. - * + * * The GUILD_MEDIA channel type is still in active development. * Avoid implementing any features that are not documented, since they are subject to change without notice! */ diff --git a/src/Discord/Voice/Buffer.php b/src/Discord/Voice/OldBuffer.php similarity index 98% rename from src/Discord/Voice/Buffer.php rename to src/Discord/Voice/OldBuffer.php index 6e44da143..bc422c1c6 100644 --- a/src/Discord/Voice/Buffer.php +++ b/src/Discord/Voice/OldBuffer.php @@ -21,7 +21,7 @@ * * @since 3.2.0 */ -class Buffer extends BaseBuffer implements ArrayAccess +class OldBuffer extends BaseBuffer implements ArrayAccess { /** * Writes a 32-bit unsigned integer with big endian. diff --git a/src/Discord/Voice/OggStream.php b/src/Discord/Voice/OldOggStream.php similarity index 93% rename from src/Discord/Voice/OggStream.php rename to src/Discord/Voice/OldOggStream.php index 8572eb1b5..32bba2628 100644 --- a/src/Discord/Voice/OggStream.php +++ b/src/Discord/Voice/OldOggStream.php @@ -29,7 +29,7 @@ * * @internal */ -class OggStream +class OldOggStream { /** * Leftover bytes from the previous Ogg packet. @@ -67,13 +67,13 @@ private function __construct( * @param Buffer $buffer Buffer to read Ogg Opus packets from. * @param ?int $timeout Time in milliseconds before a buffer read times out. * - * @return PromiseInterface A promise containing the Ogg stream. + * @return PromiseInterface A promise containing the Ogg stream. */ public static function fromBuffer(Buffer $buffer, ?int $timeout = -1): PromiseInterface { return OggPage::fromBuffer($buffer, $timeout) ->then(fn (OggPage $pageHeader) => OggPage::fromBuffer($buffer, $timeout) - ->then(fn (OggPage $pageTags) => new OggStream($buffer, new OpusHead($pageHeader->segmentData), new OpusTags($pageTags->segmentData)))); + ->then(fn (OggPage $pageTags) => new OldOggStream($buffer, new OpusHead($pageHeader->segmentData), new OpusTags($pageTags->segmentData)))); } /** diff --git a/src/Discord/Voice/VoiceClient.php b/src/Discord/Voice/OldVoiceClient.php similarity index 98% rename from src/Discord/Voice/VoiceClient.php rename to src/Discord/Voice/OldVoiceClient.php index 3c7b734c0..63b44fcfa 100644 --- a/src/Discord/Voice/VoiceClient.php +++ b/src/Discord/Voice/OldVoiceClient.php @@ -49,7 +49,7 @@ * * @since 3.2.0 */ -class VoiceClient extends EventEmitter +class OldVoiceClient extends EventEmitter { /** Not speaking. */ public const NOT_SPEAKING = 0; @@ -597,7 +597,7 @@ protected function heartbeatAck($data): void $this->discord->getLogger()->debug('received heartbeat ack', ['response_time' => $diff]); $this->emit('ws-ping', [$diff]); - $this->emit('ws-heartbeat-ack', [$data->d]); + $this->emit('ws-heartbeat-ack', [$data->d['t']]); } /** @@ -678,13 +678,13 @@ protected function handleReady(object $data): void $udpfac->createClient("{$this->udpIp}:".$this->udpPort)->then(function (Socket $client): void { $this->client = $client; - $buffer = new Buffer(74); + $buffer = new OldBuffer(74); $buffer[1] = "\x01"; $buffer[3] = "\x46"; $buffer->writeUInt32BE($this->ssrc, 4); $this->udpHeartbeat = $this->discord->getLoop()->addPeriodicTimer(5, function () { - $buffer = new Buffer(9); + $buffer = new OldBuffer(9); $buffer[0] = "\xC9"; $buffer->writeUInt64LE($this->heartbeatSeq, 1); ++$this->heartbeatSeq; @@ -1286,7 +1286,7 @@ public function playOggStream($stream): PromiseInterface $this->setSpeaking(self::MICROPHONE); - OggStream::fromBuffer($this->buffer)->then(function (OggStream $os) use ($deferred, $loops) { + OldOggStream::fromBuffer($this->buffer)->then(function (OldOggStream $os) use ($deferred, $loops) { $this->startTime = microtime(true) + 0.5; $this->readOpusTimer = $this->discord->getLoop()->addTimer(0.5, fn () => $this->readOpus($deferred, $os, $loops)); }); @@ -1297,14 +1297,14 @@ public function playOggStream($stream): PromiseInterface /** * Reads and processes Opus audio packets from an OGG stream. * - * @param Deferred $deferred The deferred promise that will be resolved when the stream ends. - * @param OggStream &$ogg Reference to the OGG stream object to read packets from. - * @param int &$loops Reference to the loop counter used for timing calculations. + * @param Deferred $deferred The deferred promise that will be resolved when the stream ends. + * @param OldOggStream &$ogg Reference to the OGG stream object to read packets from. + * @param int &$loops Reference to the loop counter used for timing calculations. * * * @throws \Exception If packet retrieval fails. */ - public function readOpus(Deferred $deferred, OggStream &$ogg, int &$loops) + public function readOpus(Deferred $deferred, OldOggStream &$ogg, int &$loops) { $this->readOpusTimer = null; @@ -1822,13 +1822,13 @@ protected function handleAudioData($message): void /** * Decodes voice packet data. * - * @param string $decrypted The decrypted voice data. - * @param VoiceClient $vc The voice client instance. - * @param VoicePacket $voicePacket The voice packet to decode. + * @param string $decrypted The decrypted voice data. + * @param OldVoiceClient $vc The voice client instance. + * @param VoicePacket $voicePacket The voice packet to decode. * * @todo */ - public static function decodeVoicePacket(string $decrypted, VoiceClient $vc, VoicePacket $voicePacket): void + public static function decodeVoicePacket(string $decrypted, OldVoiceClient $vc, VoicePacket $voicePacket): void { $vp = VoicePacket::make($voicePacket->getHeader().$decrypted); @@ -1852,7 +1852,7 @@ public static function decodeVoicePacket(string $decrypted, VoiceClient $vc, Voi $vc->voiceDecoders[$ss->ssrc] = $decoder; } - $buff = new Buffer(strlen($vp->getData()) + 2); + $buff = new OldBuffer(strlen($vp->getData()) + 2); $buff->write(pack('s', strlen($vp->getData())), 0); $buff->write($vp->getData(), 2); diff --git a/src/Discord/Voice/SessionDescription.php b/src/Discord/Voice/SessionDescription.php index e96e46bcf..d08d1c3ae 100644 --- a/src/Discord/Voice/SessionDescription.php +++ b/src/Discord/Voice/SessionDescription.php @@ -52,4 +52,15 @@ public function getSecretKeyAttribute(): string { return pack('C*', ...$this->attributes['secret_key']); } + + public function __debugInfo(): array + { + $array = $this->jsonSerialize(); + + if (isset($array['secret_key'])) { + $array['secret_key'] = '*****'; + } + + return $array; + } } diff --git a/src/Discord/WebSockets/Payload.php b/src/Discord/WebSockets/Payload.php index 7cb08cda6..06712d4e8 100644 --- a/src/Discord/WebSockets/Payload.php +++ b/src/Discord/WebSockets/Payload.php @@ -85,10 +85,10 @@ public function __debugInfo() if (isset($array['d'])) { is_array($array['d']) ? (isset($array['d']['token']) - ? $array['d']['token'] = 'xxxxx' + ? $array['d']['token'] = '*****' : null) : (isset($array['d']->token) - ? $array['d']->token = 'xxxxx' + ? $array['d']->token = '*****' : null); }