Skip to content

Implement DAVE protocol integration paths, add unit/feature tests, and address reviewer feedback#4

Merged
alexandre433 merged 117 commits into
mainfrom
copilot/insert-discord-dave-protocol
Jun 15, 2026
Merged

Implement DAVE protocol integration paths, add unit/feature tests, and address reviewer feedback#4
alexandre433 merged 117 commits into
mainfrom
copilot/insert-discord-dave-protocol

Conversation

Copilot AI commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Description

Delivers foundational DAVE support in the voice stack and follows through on review feedback to make the new paths functional, test-covered, and consistent across docs and helpers.

Foundational DAVE work:

  • Added DAVE runtime/state/frame foundation: Discord\Voice\Dave\Runtime, Discord\Voice\Dave\State, Discord\Voice\Dave\BinaryFrame.
  • Implemented robust DAVE opcode handling in voice WS, including binary opcode routing, protocol/epoch/transition state handling, and transition-ready acknowledgements.
  • Added binary voice gateway frame parsing/sending path for DAVE binary opcodes.
  • Implemented packet-level DAVE transform hooks on send/receive paths.
  • Implemented functional VoiceClient::encryptDaveFrame() and VoiceClient::decryptDaveFrame() behavior with protocol-aware runtime integration and failure handling.
  • Implemented MLS proposals handling path in WS (previous TODO), including commit/welcome vs invalid-commit/welcome binary responses.

Test coverage added:

  • tests/Unit/Dave/RuntimeTest.php
  • tests/Unit/Voice/VoiceClientDaveFrameTest.php
  • tests/Feature/Voice/WSDaveMlsProposalsTest.php
  • tests/bootstrap.php

Reviewer feedback addressed (round 1):

  • LibDaveNotFoundException::fromRuntimeError() only appends the "Load error:" suffix when the load error is a non-empty string (skips both null and '').
  • composer.json unit script no longer hard-codes a Linux-only DISCORDPHP_DAVE_LIBRARY=.cache/libdave/lib/libdave.so; per-platform export is documented in CONTRIBUTING/README instead.
  • CONTRIBUTING.md PHP prerequisite aligned with composer.json's ^8.3 requirement.
  • README.md no longer claims an automatic fallback to DAVE protocol version 0; clarifies that libdave is required and LibDaveNotFoundException is thrown when unavailable.
  • BufferArrayAccessTrait::writeUInt() switched to big-endian (N) so it round-trips with readUInt() and matches the RTP byte order used by Packet.
  • Ffmpeg::encode() now includes pipe in -protocol_whitelist so the default pipe:0 stdin input is not rejected by ffmpeg; affected tests updated to expect the new whitelist value.

Reviewer feedback addressed (round 2):

  • .github/dependabot.yml: reverted both the composer and github-actions schedules from weekly to daily.
  • Replaced voice->join(...) / manager->join(...) snippets with the actual Manager API name joinChannel(...) (and normalized to a single-arg form for consistency) across docs/TROUBLESHOOTING.md, examples/play-file.php, examples/queue.php, examples/record.php, README.md, and the Discord\Voice\Exceptions\VoiceException docblock.
  • VoiceClient::ALLOWED_URL_SCHEMES tightened to HTTPS-only (dropped http) so playFile() rejects plain-HTTP URLs; tests/Unit/Voice/VoiceClientSecurityTest.php updated to assert the new scheme set.
  • Replied to the trait/class duplication question on BufferArrayAccessTrait: the Discord\Voice\Helpers\ByteBuffer\BufferArrayAccessTrait copy is the documented legacy back-compat shim and was left in place; removal is deferred to a separate, focused PR.

Type of change

  • Bug fix
  • New feature
  • Refactor / code quality
  • Documentation
  • Chore / maintenance

Checklist

  • Ran composer check (Pint + CS-Fixer dry run + unit tests pass)
  • Ran composer phpstan (PHPStan level 5 passes)
  • Added or updated tests for new/changed behaviour
  • Updated documentation if behaviour changed (README, docs/, docblocks)

Related issues

Copilot AI requested a review from alexandre433 April 7, 2026 12:49
@alexandre433 alexandre433 marked this pull request as ready for review April 7, 2026 13:04
@alexandre433 alexandre433 marked this pull request as draft April 7, 2026 13:05

@alexandre433 alexandre433 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Also unit & feature tests are missing.
We haven't implemented any so far. but you can implement them.

Comment thread src/Discord/Voice/Client/WS.php Outdated
Comment thread src/Discord/Voice/VoiceClient.php
Copilot AI and others added 2 commits April 7, 2026 13:22

Copilot AI commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Also unit & feature tests are missing.
We haven't implemented any so far. but you can implement them.

Added in commit c4fb678. I implemented unit and feature tests, including runtime callback behavior, VoiceClient DAVE frame encryption/decryption behavior, and WS MLS proposals handling behavior.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/discord-php/Collection/zipball/27eae375a21d5086a665b803881cee09863b2269
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/discord-php/DiscordPHP-Http/zipball/8b2d055b213f3ab83cf25b2ecbc6d467a8cadad2
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/discord-php/DiscordPHP/zipball/62eff1719809290cb93fc7827c1693382c96bad8
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/discord/discord-interactions-php/zipball/a6fc0c877b75cf5ff5811f2ea69c5cc4ad6ac457
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/discord/libdave/contents/js/src
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (http block)
  • https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/nesQuick/ByteBuffer/zipball/e94e5c87c41bc79c0f738b0fa89bad11d27ae0b4
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/ratchetphp/Pawl/zipball/2c582373c78271de32cb04c755c4c0db7e09c9c0
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/datagram/zipball/9236e1f5a67a6029be17d551e9858c487836c301
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/http/zipball/8db02de41dcca82037367f67a2d4be365b1c4db9
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)
  • https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678
    • Triggering command: /usr/bin/php8.3 /usr/bin/php8.3 -n -c /tmp/eL5Uma /usr/bin/composer install --no-interaction --prefer-dist (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Add foundational DAVE protocol gateway integration and frame hooks Implement DAVE protocol integration paths and add unit/feature tests Apr 7, 2026
Copilot AI requested a review from alexandre433 April 7, 2026 13:26
alexandre433 and others added 12 commits April 7, 2026 15:56
- Introduced SessionHandle class for managing DAVE sessions.
- Expanded Runtime class with new methods for session lifecycle management, including creation, initialization, and resetting.
- Added support for encryptor and decryptor management in the State class, allowing for dynamic handling of encryption states.
- Updated VoiceClient to utilize the new DAVE state management for encrypting and decrypting frames.
- Implemented tests for session lifecycle, key package generation, and encryptor/decryptor functionality.
- Refactored existing tests to reset the DAVE runtime state appropriately.
- Added WSSequenceAckTest to ensure correct sequence acknowledgment handling in heartbeat and resume scenarios.
- Register Op 23 (VOICE_DAVE_TRANSITION_READY), 26 (VOICE_DAVE_MLS_KEY_PACKAGE),
  28 (VOICE_DAVE_MLS_COMMIT_WELCOME), and 31 (VOICE_DAVE_MLS_INVALID_COMMIT_WELCOME)
  in VOICE_OP_HANDLERS so Discord packets are no longer silently dropped.

- handleDaveTransitionReady (Op 23): applies the self encryptor for the pending
  protocol version then calls daveState->executeTransition() to finalize state.

- handleDaveMlsKeyPackage (Op 26): validates the frame is a BinaryFrame and
  returns passively; the server owns proposal aggregation so no outbound action
  is required.

- handleDaveMlsCommitWelcome (Op 28): first tries processCommit; on success
  calls prepareDaveMediaTransition + sendDaveTransitionReady. Falls back to
  processWelcome on commit failure; on success takes the same path. If both
  fail, delegates to handleInvalidDaveTransition for recovery.

- handleDaveMlsInvalidCommitWelcome (Op 31): logs a warning and calls
  handleInvalidDaveTransition(0, true) to send INVALID_COMMIT_WELCOME and
  re-enter the MLS handshake via initializeDaveRuntimeState + sendDaveKeyPackage.

Adds WSDaveStubHandlersTest covering all four handlers (5 test cases).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add WSDaveMlsAnnounceCommitTest: 7 tests for announce-commit-transition handler
- Add WSDaveMlsWelcomeTest: 8 tests for MLS welcome handler
- Add WSDaveStubHandlersTest: 5 tests for newly-implemented stub handlers
- Add BinaryFrameTest, StateTest, RuntimeTest, PacketTest, BufferTest
- Add OggPageTest, OggStreamTest, OpusHeadTest, OpusTagsTest
- Add ReceiveStreamTest, RecieveStreamTest, ProcessWrapperTest
- Update test infrastructure: Pest.php, phpunit.xml, bootstrap, fixtures
- Update CI workflow and composer scripts for parallel/coverage runs
- Fix ByteBuffer, Helpers/Buffer, ProcessAbstract, RecieveStream

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…verage

- fix: RTP extension stripping only on libsodium path ($daveApplied flag)
- fix: $regenerateKeyPackage param now gates key-package re-send
- fix: O(1) SSRC→userId map in VoiceClient (ssrcToUserId array)
- feat: max_dave_protocol_version in RESUME payload; handleResumed re-parses version
- feat: error logging on all configure*() bool returns in WS.php
- feat: BinaryFrame fromServerPayload/toServerPayload canonical aliases (deprecate fromPayload/toPayload)
- test: WSDaveErrorRecoveryTest (8 tests)
- test: WSDaveReconnectTest (10 tests)
- test: WSDaveRemoteUsersTest (10 tests)
- test: WSDaveSessionInitTest (15 tests)
- test: WSDaveTransitionsTest (11 tests)
- test: SSRCResolutionTest (4 tests)
- test: VoiceClientDaveFrameTest extended (2 new scenarios)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SCORDPHP_DAVE_LIBRARY is set

Runtime::reset() only clears the singleton; the next isAvailable() call
reloads from the env var, so the 'unavailable' branch was unreachable when
the env var was present. Fix: temporarily unset the env var and reset the
singleton inside the test, restoring both in a finally block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elcome; fix FFI-safe test mocking

- Runtime::configureCallbacks() now accepts createSessionCallback and availabilityOverride
  parameters, giving tests a safe injection point without needing real libdave
- State::recognizedUsers() now always returns list<string> (fixes PHP numeric-string
  array key coercion that caused strlen() TypeError in makeStringPointerArray)
- WSDaveSessionInitTest: use createSessionCallback to simulate session creation failure;
  use availabilityOverride to test the no-libdave resolveDaveProtocolVersion path;
  update 'resets existing session' assertion to work with or without libdave
- WSDaveTransitionsTest: use availabilityOverride instead of putenv/reset for
  'unavailable' tests (dlopen SONAME caching prevents putenv from unloading libdave)
- StateTest: update recognizedUsers() expectations from int to string (correct per docblock)
- All 156 tests pass with DISCORDPHP_DAVE_LIBRARY set (0 skipped)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ndException if missing

Discord mandated the DAVE E2EE protocol for all voice/video connections
since March 1st, 2026. Previously, the client silently fell back to
protocol version 0 (passthrough) when libdave was not available, which
Discord now rejects.

Changes:
- Add LibDaveNotFoundException with a static fromRuntimeError() factory
  that builds a descriptive message including the Discord announcement
  URL, installation instructions, and the FFI load error (if any).
- Throw LibDaveNotFoundException::fromRuntimeError() as the very first
  statement in Manager::__construct() and WS::__construct() so no
  connection attempt is ever made without a working libdave.
- Remove the now-dead isAvailable() branch from
  WS::resolveDaveProtocolVersion().
- Add feature tests for both enforcement points (WSDaveRequirementTest,
  ManagerDaveRequirementTest) and update existing DAVE tests that were
  using availabilityOverride: false on paths that are now unreachable.

Ref: https://discord.com/developers/docs/change-log#future-deprecation-and-discontinuation-of-non-e2ee-voice

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ons, and test patterns

- Fix critical DAVE fallback mismatch: instructions previously said the
  client falls back to protocol v0 when libdave is absent; in reality
  Manager and WS both throw LibDaveNotFoundException at construction.
- Add architecture entries for: Dave handle types (SessionHandle,
  EncryptorHandle, DecryptorHandle, KeyRatchetHandle), BinaryFrame,
  Dave\State fields, OggPage/OggStream/OpusHead/OpusTags, ByteBuffer
  (active vs legacy Helpers duplicate), Processes subsystem (Ffmpeg,
  DCA, OpusFfi, OpusDecoderInterface), Client\User value object, the
  full Exceptions hierarchy (Channels/ and Libraries/), VoicePayload,
  OpEnum, and UserConnected.
- Add convention: Exceptions\Libraries\ use bare \Exception subclasses;
  runtime-context messages go in a fromRuntimeError() static factory.
- Add '## Test patterns' section documenting: Feature vs Unit directory
  split, inline helpers convention, reflection-based construction,
  PHPUnit getMockBuilder mocking, payload-capture pattern, the three
  DAVE stubbing strategies (availabilityOverride false/true/real),
  mandatory afterEach(Runtime::reset()), markTestSkipped convention,
  and BinaryFrame construction for gateway message tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wrap all caller-supplied strings with escapeshellarg() in
  Ffmpeg::encode(), Ffmpeg::decode(), and ProcessAbstract::checkForExecutable()
  to prevent shell metacharacter injection via filenames and preArgs.

- Fix VoiceClient::setData() ignoring its $data parameter. The method
  checked $this->data (never updated) for the boot condition, so
  WS::make() never fired through Manager::joinChannel(). Added
  $this->data = array_merge($this->data, $data) as the first statement.

- Fix Packet::decrypt() finally block overriding the catch return.
  A return inside finally unconditionally overrides return false in catch,
  causing authentication failures to surface as null instead of false.
  Restructured so catch sets $resultMessage = false, finally only assigns
  $this->decryptedAudio, and a single return follows the block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…crypt error handling

- ProcessWrapperTest: assert shell metacharacters in filenames passed to
  Ffmpeg::encode() are escaped by escapeshellarg() and never appear raw
  in the constructed command string.

- VoiceClientSetDataTest (new): three tests covering setData() —
  data is merged into $this->data; boot() is not called when required
  keys are missing; boot() is called exactly once and endpoint/dnsConfig
  are set correctly when all four keys are present.

- PacketTest: assert decrypt() returns false (not null) when libsodium
  throws a SodiumException, verified by injecting a wrong-length key via
  reflection to force the catch branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
alexandre433 and others added 24 commits April 27, 2026 23:23
…ty error

VoiceClient::$readOpusTimer was declared as a typed nullable property without
a default value. reset() accesses it unconditionally, so calling stopRecording()
before any audio playback threw:

  Typed property Discord\Voice\VoiceClient::$readOpusTimer must not be
  accessed before initialization

Add = null to the declaration to match the existing $heartbeat pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Copilot <copilot@github.com>
- UDP::handleMessages now drops packets with RTP version != 2 or with
  payload type in 72-95 (RTCP PT 200-223 per RFC 5761) before
  constructing a Packet — prevents RTCP Sender/Receiver Reports from
  being mis-decrypted and logged as 'No audio data'
- Packet::unpack format string keys corrected from 'first'/'second' to
  'version_and_flags'/'payload_type' so $versionPlusFlags and
  $payloadType are actually populated
- Both properties typed as ?int (unpack 'C' returns int, not string)
  and default-initialised to null
- Tests: 5 new UDP RTCP-filter tests, 2 new Packet unpack-key tests
  (552 total, up from 545)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ord()

- VoiceClient::handleAudioData(): add writePCM() call in the FFI decoder
  path so channel-pcm events fire with decoded audio (was the root cause
  of all recordings being 0 bytes)
- VoiceClient::handleAudioData(): add else branch for the no-FFI-decoder
  case that writes raw Opus frames via writeOpus() so channel-opus still
  fires even without libopus installed
- VoiceClient::record(): auto-initialize OpusFfi when opusdecoder is null
  and libopus is available, removing the requirement for callers to call
  setDecoder() before recording works out of the box
- Add four new tests covering the FFI PCM path, the non-FFI Opus path,
  record() not overriding a pre-set decoder, and record() auto-init

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When audio frames arrive before the speaking event (op 5), ssrcToUserId
has no entry for the SSRC yet, so resolveDaveRemoteUserId returns null.
MediaCryptoService::decrypt previously skipped the per-user decryptor
path entirely and fell back to decryptMediaFrame which always fails.

The same race affects the userId-known case: user in channel but their
decryptor is not yet set up (MLS group not ready).

Fix: replace the strict guard with a fan-out - resolve the target
decryptor when possible, otherwise iterate all known decryptors. AEAD
verification failures are non-mutating so trying the wrong handle is
safe. Stop on first string result or on explicit false (non-DAVE frame).
The existing decryptMediaFrame fallback still runs if every handle
returns null.

Runtime::configureCallbacks gains decryptWithDecryptorCallback parameter
so unit tests can stub the per-handle decrypt path without real libdave.

State::getAllDecryptors() exposes the decryptors map for iteration.

4 new tests cover the new paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uzzing

OpusFfi::decode() was creating and destroying an Opus decoder on every
single frame. Opus is a stateful codec — destroying the decoder between
frames discards pitch predictors, SILK/CELT context, and PLC state,
causing audible buzzing and static artifacts at each 20ms boundary.

Fix: lazily create one decoder handle per (channels, rate) pair inside
OpusFfi and reuse it across calls. Add __destruct to clean up handles.
Also fix the over-allocated PCM buffer (frameSize*channels*2 → frameSize*channels).

VoiceClient now stores per-SSRC OpusFfi instances in $ffiDecoders[]
so different speakers never share codec state. The single $opusdecoder
property is preserved as the feature-flag / non-FFI fallback path.
stopRecording() clears ffiDecoders alongside voiceDecoders.

Tests: +2 per-SSRC isolation tests (324 total, 0 failures)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CHANGELOG: add unreleased entries for RecordingFormat enum/WavWriter,
  record() format param, OpusFfi persistent decoder fix, per-SSRC
  ffiDecoders isolation, readOpusTimer null init; update test count to 581
- README: fix channel-pcm callback signature (string $pcm, VoiceClient $vc),
  add WAV auto-recording example, add RecordingFormat format note
- docs/AUDIO_PIPELINE: update inbound diagram with per-SSRC OpusFfi node

fix(examples): replace deprecated discord on('ready') with on('init')

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Buffer, RecieveStream, and VoiceClient classes
CLAUDE.md gives future Claude Code sessions the project entry-point and
architecture guidance distilled from .github/copilot-instructions.md and
docs/. .agents/, skills-lock.json, and todos.db are local Claude/skill
state and should not be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, surface Ogg load errors

- MediaCryptoService::decrypt: revert e06cfe3's "pass through ciphertext as
  Opus on auth failure" — DAVE is E2EE; on auth fail the spec is to drop the
  frame, not emit decryptable garbage to the upstream codec. Restores
  string|false return for the auth-fail path.
- Helpers/Buffer::read: reject immediately with "Buffer closed" when the
  buffer is already closed and not enough bytes are buffered, instead of
  parking a Deferred that will never resolve. d30113e moved EOF detection
  from immediate-timeout to close()-driven, but read() never honoured the
  close flag, so post-close reads hung indefinitely. Three Ogg tests fixed
  by closing the bounded fixture buffer to signal EOF.
- VoiceClient::playOggStream: attach a rejection handler to
  OggStream::fromBuffer so a buffer-close mid-handshake (e.g. ffmpeg exits
  before producing a valid Ogg header) reaches the playback Deferred
  instead of leaking as an unhandled-promise-rejection notice.
- Ffmpeg::encode: keep "file" in -protocol_whitelist. SSRF defence is
  enforced one layer up in playFile() (URL scheme allowlist + private/
  reserved/loopback IP block); without "file" in the whitelist, every
  local path passed to playFile fails with "Protocol 'file' not on
  whitelist", which makes local audio playback impossible. Rewrote the
  PlayFileSecurityTest aspirational-policy assertion to document the
  intentional trade-off rather than failing on it.
- Tests for BufferTest/Encoder/ProcessWrapper updated to expect the new
  reject-on-close behaviour and the corrected ffmpeg flag set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion suites

Unit coverage additions:
- ByteBufferRoundtripTest: writeIntN/readIntN round-trips, getLastEmptyPosition,
  array-access trait helpers (writeUInt32BE/writeUInt64LE/writeShort/writeRaw,
  offsetGet/Set/Exists/Unset). Bumps ByteBuffer/Buffer 39%→88%, trait 30%→~85%.
- SocketFactoryTest: covers the constructor's resolver-or-WS guard.
- VoiceClientChannelOpsTest: switchChannel happy/error paths, disconnect,
  mainSend payload forwarding via a Discord double.
- VoiceClientStateTest: setBitrate/setVolume/setAudioApplication validation
  and "while playing" guards, close/pause/unpause/stop guard rails, isReady,
  getReceiveStream / getRecieveStream alias, handleVoiceStateUpdate ignore
  branches, playPcmFile rejects on missing path.
- VoiceClientPlayPathsTest: playOggStream / playDCAStream / playRawStream /
  playFile / playPcmFile early-reject paths including the SSRF guards
  (scheme allowlist, private/reserved IPs, localhost).
- VoiceClientPcmRecordingTest: handleAudioData unknown-SSRC warn branch and
  RecordingFormat::PCM per-user file-handle wiring (including dirname mkdir).

Integration coverage (gated on DISCORD_BOT_TOKEN + CHANNEL_ID):
- New "full session: join → play → record → disconnect" test runs the entire
  outbound RTP+DAVE pipeline plus the inbound UDP listener in a single bot
  connection, avoiding Discord's rapid-rejoin rate limits. Drives ~10s of
  real Opus through readOggOpus → UDP::sendBuffer → DAVE encrypt and ~3s
  of recording before stopRecording.
- Other four integration tests gated behind INTEGRATION_FULL=1 because they
  re-connect the bot and conflict with the full-session test in batch.
- beforeEach adds a one-shot 8s cooldown between integration tests to give
  Discord's voice gateway time to clear the previous session.

Together: VoiceClient 51%→67%, WS 71%→86%, UDP 78%→84%, suite 559→644 tests
passing, total statement coverage 69.9%→76.8%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests for the thin WS surface the existing feature suite did not
already exercise: getVoiceClient, getDaveProtocolVersion, getRawKey,
handleCloseVoiceDisconnected, handleFlags, handlePlatform, and
handleUndocumented. All five are reflection-driven and stay in pure-unit
territory (no live socket).

Bumps total statement coverage to 76.99% with the live integration suite
and 74.28% from unit-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ining Runtime callbacks

- Drop final on Discord\\Voice\\Client\\UDP so PHPUnit can subclass-mock it.
  readOggOpus + sendBuffer are otherwise untestable from unit-level because
  every path touches UDP, and PHPUnit cannot generate a mock subclass for
  a final class. Mockery is not used in this project, so this is the
  minimum-impact option; the class remains effectively final at the
  documentation level (single concrete implementation, no other subclass).

- VoiceClientReadOggOpusTest covers the three live readOggOpus branches:
  paused-frame insertSilence + reschedule, Ogg EOF deferred-resolve, and
  the non-paused getPacket → sendBuffer dispatch loop. Drives a real
  React loop briefly so timer scheduling + packet ordering execute.

- CoordinatorHandlersTest exercises GatewayCoordinator without going
  through WS at all: a small TestCoordHost implements
  GatewayCoordinatorHost and captures sendDaveBinary / send / close
  calls. Runtime::configureCallbacks() injects fake processCommit /
  processWelcome / keyPackage results so the success branches in
  handleDaveMlsCommitWelcome (clean commit + commit-ignored fall-through
  to welcome), handleDaveMlsAnnounceCommitTransition (success + ignored),
  handleDaveMlsWelcome, and handleDaveMlsExternalSender execute end-to-end.

- RuntimeExtraCallbacksTest fills the Runtime callback hooks the existing
  RuntimeCallbacksTest had not exercised: keyPackageCallback,
  keyRatchetCallback, createDecryptorCallback, decryptorPassthroughCallback,
  decryptorKeyRatchetCallback, decryptWithDecryptorCallback (string +
  auth-fail false), buildMlsCommitWelcome short-circuit at v0, and the
  isAvailable=false fall-throughs for encryptMediaFrame / decryptMediaFrame
  / buildMlsCommitWelcome.

Bumps total statement coverage to 78% (GatewayCoordinator 82.5%→88.7%,
WS 86.4%→88.3%, suite 644→667 passing tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/Discord/Voice/Helpers/ (duplicate ByteBuffer kept only for backwards
compatibility) and src/Discord/WebSockets/OpEnum.php are already excluded
from PHPStan analysis per phpstan.neon. Mirror that in phpunit.xml so the
coverage report reflects what we actually maintain — total goes from
~78% to 83.26% with the same test set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…DE.md updates

- CHANGELOG.md "Unreleased" now documents the security/correctness fixes
  (DAVE auth-failure drops the frame, Buffer rejects on close,
  playOggStream surfaces fromBuffer errors, Ffmpeg keeps the file
  protocol with the higher-layer SSRF guard) and the test/coverage work
  (83.26% statements with legacy BC excluded; 78% raw; new unit + live
  integration files; PHPUnit source exclusions). UDP `final` removal is
  flagged under Changed.
- TODO.md tracks the remaining real coverage gaps that need real
  subprocesses or real libdave handles, and records the ssrcToUserId /
  speakingStatus / UDP visibility BC items plus the security-policy
  rationale for keeping the `file` protocol whitelisted.
- CLAUDE.md: integration-test section now documents the INTEGRATION_FULL
  gating, and the "Conventions That Bite" entry for ssrcToUserId is
  rewritten to match the actual current visibility (protected, not
  public; tests use ReflectionProperty).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d test coverage expansion

- Full DAVE/MLS E2EE protocol via libdave FFI (mandatory from 2026-03-01)
- RecordingFormat enum + WavWriter for per-user WAV/OGG recording
- VoiceException marker interface for unified catch blocks
- PHPStan level 5 clean (0 errors), CS-Fixer, phplint via composer
- 689 unit tests at 83.26% statement coverage; guarded native FFI test suite
- Dead code removed (Packet init methods, WS delegate stubs)
- Security: DAVE auth failure drops frame instead of leaking ciphertext

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests in VoiceClientReceiveExtTest that instantiate OpusFfi directly
were crashing because libopus.so.0 was absent from the runner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FFI::addr($buffer[0]) passes a PHP string to FFI::addr — invalid.
$ffi->cast('char*', $buffer) correctly yields a char* to the buffer start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old Fixed entry incorrectly stated ssrcToUserId was widened to public;
it was later narrowed back to protected via updateSpeakingStatus(). Correct
the entry and add [BC break] notices for speakingStatus, ssrcToUserId, and
voiceDecoders visibility/type changes under the Changed section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… deprecation

Static FFI::cast() is deprecated in PHP 8.4; use $ffi->cast() instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
alexandre433 and others added 2 commits May 14, 2026 00:03
…erId

Allows external callers to read the two protected properties without
widening visibility back to public.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alexandre433 alexandre433 merged commit 4fb8412 into main Jun 15, 2026
4 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DAVE voice encryption not supported

4 participants