Skip to content
Open
Show file tree
Hide file tree
Changes from 104 commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
3ae46f1
WIP
alexandre433 Mar 11, 2025
dd580e0
Updates the byte version, to the latest (forked by me) one
alexandre433 Mar 12, 2025
efc034c
Adds a new class to rename the previous one "RecieveStream" to "Recei…
alexandre433 Mar 12, 2025
25e231d
Updates the current version of the voice client to v8
alexandre433 Mar 12, 2025
ffda2b1
Removes debugging logs
alexandre433 Mar 12, 2025
9b3739f
Updates const name
alexandre433 Mar 14, 2025
1b3ef2e
Updates recieve class.
alexandre433 Mar 14, 2025
59c93d6
Updates Discord class usage for voices into 1 class only.
alexandre433 Mar 16, 2025
f0850b9
Reorganize imports
valzargaming Apr 9, 2025
99069d7
Imports
valzargaming Apr 9, 2025
ee994b6
Imports
valzargaming Apr 9, 2025
e26a968
Update .gitignore
valzargaming Apr 9, 2025
ae845ca
Import alias and broken function
valzargaming Apr 9, 2025
b6509c3
VOICE_CLIENT_CONNECT deprecated
valzargaming May 9, 2025
4772049
Imports
valzargaming May 16, 2025
e367550
Updates usage of trafficcophp/bytebuffer to a locally ovewritten class
alexandre433 Jun 1, 2025
9610825
Merge branch 'master' into feature/voice-recording
alexandre433 Jun 1, 2025
2b227c0
Updates function according to master change
alexandre433 Jun 1, 2025
a2619b0
WIP
alexandre433 Mar 11, 2025
d511657
Updates the byte version, to the latest (forked by me) one
alexandre433 Mar 12, 2025
5cdc95b
Adds a new class to rename the previous one "RecieveStream" to "Recei…
alexandre433 Mar 12, 2025
538f9d4
Updates the current version of the voice client to v8
alexandre433 Mar 12, 2025
4a372e8
Removes debugging logs
alexandre433 Mar 12, 2025
74e1908
Updates const name
alexandre433 Mar 14, 2025
3086ed3
Updates recieve class.
alexandre433 Mar 14, 2025
3fbd34b
Updates Discord class usage for voices into 1 class only.
alexandre433 Mar 16, 2025
f367066
Reorganize imports
valzargaming Apr 9, 2025
4b42a76
Imports
valzargaming Apr 9, 2025
dbae1dd
Imports
valzargaming Apr 9, 2025
75fd8b7
Update .gitignore
valzargaming Apr 9, 2025
b311875
Update .gitignore
valzargaming Apr 9, 2025
8fadcd8
Import alias and broken function
valzargaming Apr 9, 2025
65c6de9
VOICE_CLIENT_CONNECT deprecated
valzargaming May 9, 2025
1885f72
Imports
valzargaming May 16, 2025
0112d2f
Updates usage of trafficcophp/bytebuffer to a locally ovewritten class
alexandre433 Jun 1, 2025
5f36bad
Updates function according to master change
alexandre433 Jun 1, 2025
a43a498
Removes variable overriding
alexandre433 Jun 1, 2025
7e0ab95
Fixes import
alexandre433 Jun 1, 2025
d4ff0da
Fixes issue on variable type
alexandre433 Jun 1, 2025
f2f5e53
Fixes payload sent, since it's using v8
alexandre433 Jun 1, 2025
e82cc9f
Merge branch 'feature/voice-recording' of https://github.com/alexandr…
alexandre433 Jun 2, 2025
989b671
Merge branch 'feature/voice-recording-01-06-2025-master' into feature…
alexandre433 Jun 2, 2025
21a8b66
Merge branch 'master' into feature/voice-recording
valzargaming Jun 14, 2025
10e2f09
Move import of functions below import of classes
valzargaming Jun 14, 2025
80dcf4a
Make method compatible with parent methods
valzargaming Jun 14, 2025
82fa7f9
Fix typing for VoiceClient::startTime
valzargaming Jun 14, 2025
3d5d651
Discord imports
valzargaming Jun 14, 2025
db92397
Buffer imports
valzargaming Jun 14, 2025
93f28c6
Remove ReceiveStream imports
valzargaming Jun 14, 2025
c70e42f
Voice imports
valzargaming Jun 14, 2025
6e0025d
VoiceClient imports
valzargaming Jun 14, 2025
8f420f1
Helpers classes instead of dependency
valzargaming Jun 14, 2025
153664f
Remove trafficcophp/bytebuffer as a dependency
valzargaming Jun 14, 2025
e46f5d3
Author formatting
valzargaming Jun 14, 2025
60033dd
Copy iterator into array instead of iterating and concatentating
valzargaming Jun 14, 2025
5529ded
excpectedMax => expectedMax
valzargaming Jun 14, 2025
6bef84a
PHPDocs and params types
valzargaming Jun 14, 2025
8d44409
Reduce ops
valzargaming Jun 14, 2025
7459fa6
Update Buffer.php
valzargaming Jun 14, 2025
4af2454
Create BufferArrayAccessTrait.php
valzargaming Jun 14, 2025
8162df5
insert params
valzargaming Jun 14, 2025
d17e93c
Remove typing for insert's $format param
valzargaming Jun 14, 2025
e2cc7a6
Use existing FormatPackEnum class
valzargaming Jun 14, 2025
e5729af
Updating typings
valzargaming Jun 14, 2025
31205db
Update types
valzargaming Jun 14, 2025
e1484bf
Update Buffer.php
valzargaming Jun 14, 2025
648c6ea
use Discord\Helpers\ByteBuffer\Buffer
valzargaming Jun 14, 2025
72a5dd7
PHPDoc
valzargaming Jun 14, 2025
08708ab
VoicePacket Imports
valzargaming Jun 14, 2025
66495a4
Fix VoicePacket property typing
valzargaming Jun 14, 2025
acbccb1
declare(strict_types=1);
valzargaming Jun 14, 2025
79b1ec4
declare(strict_types=1);
valzargaming Jun 14, 2025
ed0c607
Type ordering
valzargaming Jun 14, 2025
b0d1302
Remove whitespace
valzargaming Jun 14, 2025
8d06d7f
VoicePayload class
valzargaming Jun 14, 2025
440fae2
Implement VoicePayload
valzargaming Jun 14, 2025
b060c3d
VoicePayload token getter and setter
valzargaming Jun 14, 2025
3a2210b
Protected visibility, VoiceClient::createDecoder
valzargaming Jun 15, 2025
80043ed
VoiceClient::removeDecoder
valzargaming Jun 15, 2025
2b1e4a8
VoiceClient::sendHeartbeat
valzargaming Jun 15, 2025
9237644
VoiceClient::decodeUDP
valzargaming Jun 15, 2025
da04f27
VoiceClient::readDCAOpus
valzargaming Jun 15, 2025
23c625d
VoiceClient::readOggOpus
valzargaming Jun 15, 2025
dc3562f
PHPDocs
valzargaming Jun 15, 2025
eab9181
PHPDocs
valzargaming Jun 15, 2025
2db5664
PHPDocs
valzargaming Jun 15, 2025
912032c
Discord property for VoiceClient
valzargaming Jun 15, 2025
9150506
VoiceSpeaking part, remove unused construct params
valzargaming Jun 16, 2025
0b25c37
Discord\WebSockets\EventData => Discord\Parts\EventData
valzargaming Jun 16, 2025
d131fba
Fixes issue on factory being null
alexandre433 Jun 16, 2025
f988bd6
Updates Voice class to VoiceManager
alexandre433 Jun 17, 2025
4fd3190
Simplifies some code to remove multiple usages of the same class e.g.…
alexandre433 Jun 17, 2025
51a6a04
Updates more usages for $this->bot instead of using other names, whic…
alexandre433 Jun 17, 2025
33224e2
Updates VoiceClient creation to match previous commit
alexandre433 Jun 17, 2025
5f96ff9
Updates some comments
alexandre433 Jun 17, 2025
bd709e4
Adds new bool checks on Channel
alexandre433 Jun 17, 2025
22f741c
Adds new classes for Process Ffmpeg
alexandre433 Jun 17, 2025
ffb2a22
Updates some flow on how the VoiceClient is created & the VoiceManage…
alexandre433 Jun 17, 2025
69298c5
Adds missing checks
alexandre433 Jun 18, 2025
a55c071
Creates Packet class, moved into Client folder inside Voice
alexandre433 Jun 18, 2025
94c50fb
Updates class names
alexandre433 Jun 18, 2025
8e3e4ca
Renames
alexandre433 Jun 18, 2025
14f9527
Adds some declare strict types
alexandre433 Jul 3, 2025
a46ef01
Adds static instance for discord class, to be able to call a helper f…
alexandre433 Jul 3, 2025
d7bb956
Reverts visibility to protected
alexandre433 Jul 3, 2025
00da654
Reorder imports
valzargaming Jul 4, 2025
d7e1bbd
Reverts back usage of logger
alexandre433 Jul 4, 2025
ead85a3
Updates usage of get voice client
alexandre433 Jul 4, 2025
fc3d73d
Updates function to remove duplication of "return $deferred->promise();"
alexandre433 Jul 4, 2025
23d3d3d
Remove usage of the DnsFactory from within the VoiceClient into the c…
alexandre433 Jul 4, 2025
b6727fc
Removes unused function (already handled on either VoiceManager or Vo…
alexandre433 Jul 4, 2025
9a7cf18
Fixes issue on heartbeat not working
alexandre433 Jul 4, 2025
b3dc1ef
Updates some visibilities on some properties
alexandre433 Jul 4, 2025
0c8bccf
[Voice] Adds FFI requirement
alexandre433 Jul 6, 2025
82feaa5
Fixes an issue where it threw an error (frame_size should be 960)
alexandre433 Jul 7, 2025
6389d8d
Adds `->leave()` function to make the bot leave the voice channel
alexandre433 Jul 7, 2025
6a762da
Adds return null on udp
alexandre433 Jul 7, 2025
866da52
Updates ffmpeg command, adds todo
alexandre433 Jul 7, 2025
19b68e6
Runs pint on voice folder
alexandre433 Jul 7, 2025
82a340f
Adds new exceptions
alexandre433 Jul 7, 2025
05e7a8d
Adds 2 new functions `->record()` and `->stopRecording()` (BETA)
alexandre433 Jul 7, 2025
a60fad6
Adds some doc blocks
alexandre433 Jul 7, 2025
f69bb35
Adds some docblock
alexandre433 Jul 7, 2025
d1abf14
Updates @since for 10.19.0
alexandre433 Jul 7, 2025
e83649d
Refactors voice client management and updates method names for clarity
alexandre433 Aug 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ composer.lock
phpunit.log
/.phpunit*
/coverage
/.vs
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"ratchet/pawl": "^0.4.3",
"react/datagram": "^1.8",
"symfony/options-resolver": "^5.1.11 || ^6.0 || ^7.0",
"trafficcophp/bytebuffer": "^0.3",
"monolog/monolog": "^2.1.1 || ^3.0",
"react/event-loop": "^1.2",
"ext-zlib": "*",
Expand Down
200 changes: 80 additions & 120 deletions src/Discord/Discord.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Discord;

use Discord\Exceptions\IntentException;
use Discord\Exceptions\Runtime\RequiredExtensionNotLoadedException;
use Discord\Factory\Factory;
use Discord\Helpers\BigInt;
use Discord\Helpers\CacheConfig;
Expand All @@ -34,14 +35,18 @@
use Discord\Repository\GuildRepository;
use Discord\Repository\PrivateChannelRepository;
use Discord\Repository\UserRepository;
use Discord\Voice\Voice;
use Discord\Voice\VoiceClient;
use Discord\Voice\VoiceManager;
use Discord\WebSockets\Event;
use Discord\WebSockets\Events\GuildCreate;
use Discord\WebSockets\Payload;
use Discord\WebSockets\Handlers;
use Discord\WebSockets\Intents;
use Discord\WebSockets\Op;
use Discord\WebSockets\Payload;
use Evenement\EventEmitterTrait;
use function React\Async\coroutine;
use function React\Promise\all;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger as Monolog;
Expand All @@ -53,13 +58,11 @@
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use React\Promise\Deferred;

use React\Promise\PromiseInterface;
use React\Socket\Connector as SocketConnector;
use Symfony\Component\OptionsResolver\OptionsResolver;

use function React\Async\coroutine;
use function React\Promise\all;

/**
* The Discord client class.
*
Expand All @@ -81,7 +84,6 @@
* @property PrivateChannelRepository $private_channels
* @property SoundRepository $sounds
* @property UserRepository $users

*/
class Discord
{
Expand All @@ -108,13 +110,6 @@ class Discord
*/
protected $logger;

/**
* An array of loggers for voice clients.
*
* @var ?LoggerInterface[] Loggers.
*/
protected $voiceLoggers = [];

/**
* An array of options passed to the client.
*
Expand All @@ -134,7 +129,7 @@ class Discord
*
* @var LoopInterface Event loop.
*/
protected $loop;
public $loop;

/**
* The WebSocket client factory.
Expand Down Expand Up @@ -192,13 +187,6 @@ class Discord
*/
protected $sessionId;

/**
* An array of voice clients that are currently connected.
*
* @var array Voice Clients.
*/
protected $voiceClients = [];

/**
* An array of large guilds that need to be requested for members.
*
Expand Down Expand Up @@ -347,6 +335,13 @@ class Discord
*/
private $application_commands;

/**
* The voice handler, of clients and packets.
*
* @var VoiceManager
*/
public VoiceManager $voice;

/**
* The transport compression setting.
*
Expand All @@ -361,6 +356,13 @@ class Discord
*/
protected $usePayloadCompression;

/**
* The instance of the Discord client.
*
* @var Discord|null Instance.
*/
protected static Discord $instance;

/**
* Creates a Discord client instance.
*
Expand All @@ -372,7 +374,7 @@ public function __construct(array $options = [])
{
// x86 need gmp extension for big integer operation
if (PHP_INT_SIZE === 4 && ! BigInt::init()) {
throw new \RuntimeException('ext-gmp is not loaded, it is required for 32-bits (x86) PHP.');
throw new RequiredExtensionNotLoadedException();
}

$options = $this->resolveOptions($options);
Expand All @@ -382,7 +384,8 @@ public function __construct(array $options = [])
$this->loop = $options['loop'];
$this->logger = $options['logger'];

if (!in_array(php_sapi_name(), ['cli', 'micro'])) {
if (! in_array(php_sapi_name(), ['cli', 'micro'])) {
// @todo: throw an exception instead?
$this->logger->critical('DiscordPHP will not run on a webserver. Please use PHP CLI to run a DiscordPHP bot.');
}

Expand Down Expand Up @@ -414,6 +417,22 @@ public function __construct(array $options = [])
$this->useTransportCompression = $options['useTransportCompression'];
$this->usePayloadCompression = $options['usePayloadCompression'];
$this->connectWs();

if (!isset(self::$instance)) {
// If the instance is not set, set it to this instance.
// This allows for static access to the Discord client.
self::$instance = $this;
}
}

# BETA - still testing if it works
public static function __callStatic($method, $args)
{
if (method_exists(self::class, $method)) {
return self::$instance->$method(...$args);
}

throw new \BadMethodCallException("Method {$method} does not exist in " . __CLASS__);
}

/**
Expand All @@ -423,9 +442,9 @@ public function __construct(array $options = [])
*/
protected function handleVoiceServerUpdate(Payload $data): void
{
if (isset($this->voiceClients[$data->d->guild_id])) {
if (isset($this->voice->clients[$data->d->guild_id])) {
$this->logger->debug('voice server update received', ['guild' => $data->d->guild_id, 'data' => $data->d]);
$this->voiceClients[$data->d->guild_id]->handleVoiceServerChange((array) $data->d);
$this->voice->clients[$data->d->guild_id]->handleVoiceServerChange((array) $data->d);
}
}

Expand Down Expand Up @@ -604,15 +623,19 @@ protected function handleGuildMembersChunk(Payload $data): void
*/
protected function handleVoiceStateUpdate(Payload $data): void
{
if (isset($this->voiceClients[$data->d->guild_id])) {
if (isset($this->voice->clients[$data->d->guild_id])) {
$this->logger->debug('voice state update received', ['guild' => $data->d->guild_id, 'data' => $data->d]);
$this->voiceClients[$data->d->guild_id]->handleVoiceStateUpdate($data->d);
$this->voice->clients[$data->d->guild_id]->handleVoiceStateUpdate($data->d);
}
}

/**
* Handles WebSocket connections received by the client.
*
* @uses \Discord\Discord::handleWsMessage
* @uses \Discord\Discord::handleWsClose
* @uses \Discord\Discord::handleWsError
*
* @param WebSocket $ws WebSocket client.
*/
public function handleWsConnection(WebSocket $ws): void
Expand Down Expand Up @@ -782,6 +805,12 @@ public function handleWsConnectionFailed(\Throwable $e): void
/**
* Handles dispatch events received by the WebSocket.
*
* @uses \Discord\Discord::handleVoiceStateUpdate
* @uses \Discord\Discord::handleVoiceServerUpdate
* @uses \Discord\Discord::handleResume
* @uses \Discord\Discord::handleReady
* @uses \Discord\Discord::handleGuildMembersChunk
*
* @param object $data Packet data.
*/
protected function handleDispatch(object $data): void
Expand All @@ -806,7 +835,6 @@ protected function handleDispatch(object $data): void
$this->{$handlers[$data->t]}(Payload::new($data->op, $data->d, $data->s, $data->t));
}


return;
}

Expand Down Expand Up @@ -852,11 +880,13 @@ protected function handleDispatch(object $data): void
$promise = coroutine([$handler, 'handle'], $data->d);
$promise->then([$deferred, 'resolve'], [$deferred, 'reject']);
};
} else {
/** @var PromiseInterface */
$promise = coroutine([$handler, 'handle'], $data->d);
$promise->then([$deferred, 'resolve'], [$deferred, 'reject']);

return;
}

/** @var PromiseInterface */
$promise = coroutine([$handler, 'handle'], $data->d);
$promise->then([$deferred, 'resolve'], [$deferred, 'reject']);
}

/**
Expand Down Expand Up @@ -1157,7 +1187,7 @@ public function requestSoundboardSounds(array $guildIds): void
*
* @param Payload|array $data Packet data.
*/
protected function send(Payload|array $data, bool $force = false): void
public function send(Payload|array $data, bool $force = false): void
{
// Wait until payload count has been reset
// Keep 5 payloads for heartbeats as required
Expand All @@ -1184,6 +1214,9 @@ protected function ready()
}
$this->emittedInit = true;

$this->voice = new VoiceManager($this);
$this->logger->info('voice class initialized');

$this->logger->info('client is ready');
$this->emit('init', [$this]);

Expand Down Expand Up @@ -1245,12 +1278,13 @@ public function updatePresence(?Activity $activity = null, bool $idle = false, s
* Gets a voice client from a guild ID. Returns null if there is no voice client.
*
* @param string $guild_id The guild ID to look up.
* @deprecated Use $discord->voice->getVoiceClient()
*
* @return VoiceClient|null
*/
public function getVoiceClient(string $guild_id): ?VoiceClient
{
return $this->voiceClients[$guild_id] ?? null;
return $this->voice->clients[$guild_id] ?? null;
}

/**
Expand All @@ -1266,95 +1300,11 @@ public function getVoiceClient(string $guild_id): ?VoiceClient
* @since 10.0.0 Removed argument $check that has no effect (it is always checked)
* @since 4.0.0
*
* @return PromiseInterface<VoiceClient>
* @return PromiseInterface<Voice>
*/
public function joinVoiceChannel(Channel $channel, $mute = false, $deaf = true, ?LoggerInterface $logger = null): 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,
];

$voiceStateUpdate = function ($vs, $discord) use ($channel, &$data, &$voiceStateUpdate) {
if ($vs->guild_id != $channel->guild_id) {
return; // This voice state update isn't for our guild.
}

$data['session'] = $vs->session_id;
$this->logger->info('received session id for voice session', ['guild' => $channel->guild_id, 'session_id' => $vs->session_id]);
$this->removeListener(Event::VOICE_STATE_UPDATE, $voiceStateUpdate);
};

$voiceServerUpdate = function ($vs, $discord) use ($channel, &$data, &$voiceServerUpdate, $deferred, $logger) {
if ($vs->guild_id != $channel->guild_id) {
return; // This voice server update isn't for our guild.
}

$data['token'] = $vs->token;
$data['endpoint'] = $vs->endpoint;
$data['dnsConfig'] = $discord->options['dnsConfig'];
$this->logger->info('received token and endpoint for voice session', ['guild' => $channel->guild_id, 'token' => $vs->token, 'endpoint' => $vs->endpoint]);

if (null === $logger) {
$logger = $this->logger;
}

$vc = new VoiceClient($this->ws, $this->loop, $channel, $logger, $data);

$vc->once('ready', function () use ($vc, $deferred, $channel, $logger) {
$logger->info('voice client is ready');
$this->voiceClients[$channel->guild_id] = $vc;

$vc->setBitrate($channel->bitrate);
$logger->info('set voice client bitrate', ['bitrate' => $channel->bitrate]);
$deferred->resolve($vc);
});
$vc->once('error', function ($e) use ($deferred, $logger) {
$logger->error('error initializing voice client', ['e' => $e->getMessage()]);
$deferred->reject($e);
});
$vc->once('close', function () use ($channel, $logger) {
$logger->warning('voice client closed');
unset($this->voiceClients[$channel->guild_id]);
});

$vc->start();

$this->voiceLoggers[$channel->guild_id] = $logger;
$this->removeListener(Event::VOICE_SERVER_UPDATE, $voiceServerUpdate);
};

$this->on(Event::VOICE_STATE_UPDATE, $voiceStateUpdate);
$this->on(Event::VOICE_SERVER_UPDATE, $voiceServerUpdate);

$payload = Payload::new(
Op::OP_VOICE_STATE_UPDATE,
[
'guild_id' => $channel->guild_id,
'channel_id' => $channel->id,
'self_mute' => $mute,
'self_deaf' => $deaf,
],
);

$this->send($payload);

return $deferred->promise();
return $this->voice->createClientAndJoinChannel($channel, $this, $mute, $deaf);
}

/**
Expand Down Expand Up @@ -1641,7 +1591,7 @@ public function getLogger(): LoggerInterface
*/
public function getHttp(): Http
{
return $this->http;
return $this->getHttpClient();
}

/**
Expand Down Expand Up @@ -1791,4 +1741,14 @@ public function __debugInfo(): array

return $config;
}

public function getWs(): ?WebSocket
{
return $this->ws;
}

public static function getInstance(): ?self
{
return self::$instance;
}
}
Loading