Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
b9d0c06
feat(voice): add foundational DAVE gateway and packet integration hooks
Copilot Apr 7, 2026
0d15b1e
refactor(voice): tighten DAVE handler parsing and runtime guards
Copilot Apr 7, 2026
c4fb678
feat(voice): implement DAVE frame handlers and add tests
Copilot Apr 7, 2026
9f15137
refactor(voice): polish runtime ffi definitions and finalize review f…
Copilot Apr 7, 2026
08b7079
feat: Enhance DAVE integration with session and encryption management
alexandre433 Apr 7, 2026
4d206fc
fix: Update DAVE payload handling and improve test channel initializa…
alexandre433 Apr 7, 2026
65f0abc
Register and implement 4 missing DAVE opcodes in WS dispatch table
alexandre433 Apr 19, 2026
a623256
test: add DAVE MLS handler tests and new test infrastructure
alexandre433 Apr 19, 2026
f3cd73c
feat(dave): fix bugs, expand DAVE tests, add remote user lifecycle co…
alexandre433 Apr 19, 2026
7a2536f
test: make resolveDaveProtocolVersion unavailable-test robust when DI…
alexandre433 Apr 19, 2026
1c10f30
feat(dave): extend configureCallbacks to cover processCommit/processW…
alexandre433 Apr 19, 2026
6f4f3d8
feat: enforce libdave availability at voice join; throw LibDaveNotFou…
alexandre433 Apr 19, 2026
84f8115
private => protected
valzargaming Apr 20, 2026
34c740e
docs: expand copilot-instructions with missing architecture, conventi…
alexandre433 Apr 19, 2026
db311f0
Fix shell escaping, setData boot trigger, and decrypt error handling
alexandre433 Apr 20, 2026
a382bed
Add regression tests for shell escaping, setData boot trigger, and de…
alexandre433 Apr 20, 2026
bc6c948
fix: add independent 32-bit nonce counter to prevent AES-GCM nonce re…
alexandre433 Apr 20, 2026
4e838e5
Fix VULN-09/14/15/20 in WS.php + regression tests
alexandre433 Apr 20, 2026
d8addd9
Fix remaining correctness and reliability issues (VULN-04 through VUL…
alexandre433 Apr 20, 2026
8993982
Add composer unit-dave script to run tests with libdave enabled
alexandre433 Apr 20, 2026
791723c
Swap unit/unit-no-dave: unit loads libdave by default
alexandre433 Apr 20, 2026
8e56d58
Add regression tests for remaining VULN-10/11/13/15/16/18/21
alexandre433 Apr 20, 2026
685e22c
fix: security hardening for secret key visibility, FFI buffer init, a…
alexandre433 Apr 20, 2026
187e010
Fix security vulnerabilities in VoiceClient and Ffmpeg
alexandre433 Apr 20, 2026
273a51c
fix: update PacketTest to pass explicit nonce after CRIT-3 encrypt guard
alexandre433 Apr 20, 2026
83281bf
Fix security vulnerabilities MED-6, MED-8, MED-9, INFO-1, LOW-4, LOW-…
alexandre433 Apr 20, 2026
3164a6a
fix(ogg): add bounds checks to OpusTags/OpusHead parsers (HIGH-1, MED…
alexandre433 Apr 20, 2026
0c0a79a
feat(dave): OS-aware setup, header-driven FFI defs, path auto-discovery
alexandre433 Apr 20, 2026
a17aab0
style: apply php-cs-fixer formatting
alexandre433 Apr 20, 2026
c05c0dc
refactor(dave): fetch SHA-256 digests from GitHub Releases API
alexandre433 Apr 20, 2026
82a0968
refactor(dave): remove inline CDEF fallback, require dave.h header
alexandre433 Apr 20, 2026
c6aeacf
docs: update copilot instructions for DAVE changes
alexandre433 Apr 20, 2026
e3a784f
docs: add visual DAVE E2EE protocol guide with Mermaid diagrams
alexandre433 Apr 20, 2026
7f82bdf
fix(dave): correct opcode for client connect in sequence diagram
alexandre433 Apr 20, 2026
84555ab
feat(dave): add warning logs for protocol downgrade and version clamping
alexandre433 Apr 24, 2026
e13dc84
feat(dave): add encrypt/decrypt failure counters with threshold warning
alexandre433 Apr 25, 2026
8133001
feat(dave): ensure passthrough mode and error log on session init fai…
alexandre433 Apr 25, 2026
48ed673
fix: reset MLS state and regenerate key package on Op 29 failure
alexandre433 Apr 25, 2026
fcf2a14
test(dave): add tests for opcode 26 no-op, setExternalSender and setS…
alexandre433 Apr 25, 2026
b24c40f
test(dave): guard against malformed binary payloads in MLS opcode han…
alexandre433 Apr 25, 2026
4fe832c
test(dave): add version cap, downgrade, passthrough recovery and fail…
alexandre433 Apr 25, 2026
b06f744
dx: high-impact developer experience improvements
alexandre433 Apr 25, 2026
f5f24cb
dx: medium-impact developer experience improvements
alexandre433 Apr 25, 2026
7733e13
docs(dave): annotate handle-less stub methods with @todo in Runtime
alexandre433 Apr 25, 2026
f189da8
Polish DX round: CHANGELOG, PR template, protocol docs, coverage CI, …
alexandre433 Apr 25, 2026
9952db4
docs: fill documentation gaps from DX improvement audit
alexandre433 Apr 25, 2026
3ae906d
fix(dave): emit ws-binary-message for binary frames; fix group_id res…
alexandre433 Apr 25, 2026
d162206
fix(dave): break stale-proposals infinite loop instead of sending INV…
alexandre433 Apr 25, 2026
d7069dc
test(integration): add live voice gateway connection tests
alexandre433 Apr 25, 2026
c242471
test(exceptions): cover voice exception hierarchy
alexandre433 Apr 25, 2026
0e42cdb
test(voice-client): cover playback guards and state transitions
alexandre433 Apr 25, 2026
1fe1781
test(voice-client): cover record/handleAudioData receive flow
alexandre433 Apr 25, 2026
f82279a
test(manager): cover joinChannel permission checks and payload
alexandre433 Apr 25, 2026
a3a4456
test(ws): cover hello/ready/speaking/sessionDescription handlers
alexandre433 Apr 25, 2026
ae3a089
test(ws): cover heartbeat and resume seq_ack bookkeeping
alexandre433 Apr 25, 2026
965ea5b
test(udp): cover IP discovery, heartbeat, sendBuffer roundtrip
alexandre433 Apr 25, 2026
670f1c8
test(packet): cover encrypt/decrypt roundtrip per encryption mode
alexandre433 Apr 25, 2026
07924a5
test(ogg): cover BOS/EOS/continued packets and multi-segment pages
alexandre433 Apr 25, 2026
4d956f4
test(dave): cover Runtime callback override and reset helpers
alexandre433 Apr 25, 2026
d16b9e9
test(voice): cover User/HeaderValuesEnum/Dave handles/UserConnected
alexandre433 Apr 25, 2026
0e1be3d
test(processes): cover Ffmpeg/DCA/OpusFfi process wrappers
alexandre433 Apr 25, 2026
d4d9430
fix(voice-client): expose ssrcToUserId so WS::handleSpeaking can upda…
alexandre433 Apr 25, 2026
15cc17f
docs: record session fixes and test coverage in CHANGELOG + instructions
alexandre433 Apr 25, 2026
f050b04
fix: remove unused dependencies and scripts from composer.json
alexandre433 Apr 25, 2026
6d33171
fix: CI failures from invalid Pest flag, PHP 8.1/8.2 compat, and Opus…
alexandre433 Apr 25, 2026
4edb2cf
fix: address PR reviewer feedback on DAVE/voice cleanup
Copilot Apr 25, 2026
5d66299
fix: address PR reviewer feedback on examples/docs/security
Copilot Apr 26, 2026
20a21a1
docs: keep joinChannel snippets to single-arg form for consistency
Copilot Apr 26, 2026
3b1fb05
fix(voice): block literal private/reserved IPs in playFile() URL vali…
alexandre433 Apr 26, 2026
6136bea
fix(ogg): add bounds guards in OggPage::fromBuffer()
alexandre433 Apr 26, 2026
ad95e43
fix: resolve all Phase 2 C1 failing tests and implement remaining bug…
alexandre433 Apr 26, 2026
6b31272
docs(dave): clarify BinaryFrame header size constants and decode null…
alexandre433 Apr 26, 2026
f6665b6
test: add VoiceClientReceiveExtTest covering decoder lifecycle and Re…
alexandre433 Apr 26, 2026
0719793
refactor: extract updateSpeakingStatus() and narrow speakingStatus/ss…
alexandre433 Apr 26, 2026
4c19326
test: Phase 1 audit remediation — test-first regression suite
alexandre433 Apr 26, 2026
4bd4b44
test: skip/remove failing gap-documentation tests in ComposerConfig a…
alexandre433 Apr 26, 2026
6f71232
fix: implement remote input security hardening (Phase 2 C2)
alexandre433 Apr 26, 2026
439949c
fix: resolve broad validation failures
alexandre433 Apr 26, 2026
434cb59
fix: ensure reconnecting login frame sends resume with seq_ack and pr…
alexandre433 Apr 27, 2026
e5209b4
feat: enhance DAVE protocol handling with key ratchet management and …
alexandre433 Apr 27, 2026
20824e8
feat: implement DAVE key package handling and logging enhancements
alexandre433 Apr 27, 2026
afda482
feat: refactor DAVE media transition handling and add tests for trans…
alexandre433 Apr 27, 2026
643be23
feat: enhance DAVE decryptor handling with passthrough and key ratche…
alexandre433 Apr 27, 2026
e9d4884
feat: improve RTP extension handling in DAVE decryption process and e…
alexandre433 Apr 27, 2026
a1a6ec6
feat: update documentation for DAVE protocol enhancements and trouble…
alexandre433 Apr 27, 2026
00416e3
refactor: centralize DAVE binary transition_id parsing into Transitio…
alexandre433 Apr 27, 2026
ef1ff7d
refactor: extract DAVE media crypto into MediaCryptoService
alexandre433 Apr 27, 2026
885af93
refactor: extract DAVE gateway logic into GatewayCoordinator
alexandre433 Apr 27, 2026
48eef8b
refactor: extract RTP header parsing into RtpHeader; add characteriza…
alexandre433 Apr 27, 2026
80ccd67
refactor: encapsulate Dave\State writes; normalize DAVE log messages;…
alexandre433 Apr 27, 2026
c7de8ed
docs: add DAVE internal flow section and class docblocks
alexandre433 Apr 27, 2026
201cdcd
fix: initialize readOpusTimer to null to prevent uninitialized proper…
alexandre433 Apr 27, 2026
1afc378
fix: filter RTCP packets in UDP and fix Packet unpack key names
alexandre433 Apr 27, 2026
6acf28f
fix: emit PCM from handleAudioData and auto-initialize OpusFfi in rec…
alexandre433 Apr 27, 2026
57d6603
fix: DAVE decrypt fan-out for unmapped SSRC / missing per-user decryptor
alexandre433 Apr 27, 2026
c0f1365
fix(opus): persist decoder handle per SSRC to eliminate inter-frame b…
alexandre433 Apr 28, 2026
0b48af1
feat(recording): implement WAV and OGG format support with WavWriter …
alexandre433 Apr 28, 2026
c0684a2
docs: update documentation for RecordingFormat API and OpusFfi fixes
alexandre433 Apr 28, 2026
189e886
refactor: replace EventEmitter inheritance with EventEmitterTrait in …
alexandre433 Apr 28, 2026
56b5a89
feat(recording): add directory creation and file handle management fo…
alexandre433 May 1, 2026
d30113e
feat(voice): enhance audio processing with PCM file playback and CRC …
alexandre433 May 1, 2026
e06cfe3
feat(dave): improve DAVE decryption handling and silence frame proces…
alexandre433 May 9, 2026
3824eab
chore: add CLAUDE.md and ignore Claude Code skill artifacts
alexandre433 May 9, 2026
c6d40d5
fix: drop ciphertext on DAVE auth failure, reject closed-buffer reads…
alexandre433 May 9, 2026
980e3ce
test: raise statement coverage from ~70% to 76.8% with unit + integra…
alexandre433 May 9, 2026
b5427bd
test: cover WS getters and passive log-only opcode handlers
alexandre433 May 9, 2026
0b8dec4
test: cover readOggOpus, GatewayCoordinator MLS happy paths, and rema…
alexandre433 May 9, 2026
480e351
chore: exclude legacy BC files from coverage source
alexandre433 May 9, 2026
98b76a6
docs: close out session — Unreleased changelog, TODO follow-ups, CLAU…
alexandre433 May 9, 2026
38fede7
feat: DAVE E2EE integration, recording formats, audit remediation, an…
alexandre433 May 11, 2026
d48c8e8
ci: install libopus0 in unit-lint and coverage jobs
alexandre433 May 13, 2026
7e87f1b
fix: cast char buffer to char* instead of addr of indexed element
alexandre433 May 13, 2026
d2e138c
docs: correct stale ssrcToUserId changelog entry and document BC breaks
alexandre433 May 13, 2026
e0139bb
fix: replace static FFI::cast() with instance call to silence PHP 8.4…
alexandre433 May 13, 2026
d0226fd
chore: remove todos.db — all tracked tasks completed
alexandre433 May 13, 2026
5d280c5
feat: add __get() for read-only access to speakingStatus and ssrcToUs…
alexandre433 May 13, 2026
635f217
removes changelog
alexandre433 May 14, 2026
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
66 changes: 66 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copilot Instructions

## Commands

| Purpose | Command | Notes |
| --- | --- | --- |
| Install dependencies | `COMPOSER_ROOT_VERSION=dev-main composer install --no-interaction --prefer-dist` | The root version override is required by the `team-reflex/discord-php` dev dependency. |
| Run the default Pest suite | `composer unit` | Expands to `pest --testdox`. |
| Run the parallel Pest suite | `composer pest` | Expands to `pest --parallel`. |
| Run one Pest test file | `./vendor/bin/pest tests/Unit/Voice/WSSequenceAckTest.php` | Use the file path when you want an exact class/file. Pest reads the same `tests/` tree and `phpunit.xml` config. |
| Run one Pest test by name | `./vendor/bin/pest --filter="heartbeat uses last received gateway sequence"` | Pest filters by the human-readable test description, so use a quoted description substring. |
| Run coverage | `composer coverage` | Requires Xdebug because the script sets `XDEBUG_MODE=coverage` and Pest writes the HTML report to `coverage/`. |
| Format with php-cs-fixer | `composer cs` | This is the formatter called out in `CONTRIBUTING.md`. |
| Check php-cs-fixer without rewriting files | `./vendor/bin/php-cs-fixer fix --dry-run --diff` | Use this when you only want a style check. |
| Run Pint | `composer pint` | Uses `pint.json` and enforces `declare(strict_types=1)`. |
| Run PHPLint | `phplint` | CI installs this as a standalone tool and uses `.phplint.yml`. |

For native DAVE coverage, run `./scripts/setup-libdave.sh` (supports Linux, macOS, and Windows on x64/ARM64). The script auto-detects the OS and architecture, downloads the correct asset, and verifies the SHA-256 digest from the GitHub Releases API. The `DISCORDPHP_DAVE_LIBRARY` env var is optional — `Runtime` auto-discovers the library at `.cache/libdave/{lib,bin}/` relative to the package root.

## High-level architecture

- `Discord\Voice\Manager` is the entry point for joining voice. It validates channel permissions, creates one voice client per guild, listens for the main gateway `VOICE_STATE_UPDATE` and `VOICE_SERVER_UPDATE` events, and only resolves the join promise after the voice client emits `ready`. Session IDs are tracked in `Discord::$voice_sessions`.
- `Discord\Voice\Client` is a backwards-compatible subclass of `VoiceClient`. Nearly all behavior lives in `VoiceClient`.
- `VoiceClient` is the high-level state machine for playback and recording. It owns speaking state, sequence/timestamp counters, receive streams, decoder processes, and public operations like `playFile()`, `playRawStream()`, `playOggStream()`, `record()`, and `stopRecording()`.
- `Discord\Voice\Client\WS` owns the voice gateway connection: identify/resume, heartbeats, reconnects, speaking events, client connect/disconnect events, session description handling, and all DAVE gateway opcodes. `Discord\Factory\SocketFactory` is used from here to create the UDP client.
- `Discord\Voice\Client\UDP` owns IP discovery, UDP heartbeats, and RTP packet send/receive. It wraps outbound and inbound voice frames in `Discord\Voice\Client\Packet`, which handles RTP headers plus libsodium encryption/decryption.
- Outbound audio flow is `playFile()` / `playRawStream()` -> `Processes\Ffmpeg::encode()` -> `OggStream` packet reads -> `UDP::sendBuffer()` -> `Packet::encrypt()`. `playFile()` accepts local files or URLs, `playRawStream()` feeds PCM into ffmpeg with `-f s16le`, and `Ffmpeg::encode()` always emits Opus on stdout for `playOggStream()` to consume.
- `playOggStream()` is where send timing is established. It buffers the Ogg stream, sets speaking state, delays the first send by 500 ms, and then `readOggOpus()` schedules one packet per frame on the React loop. That method also owns the 16-bit sequence rollover, 32-bit timestamp rollover, and EOF/reset behavior.
- Inbound audio flow starts when `record()` attaches a UDP message listener. `handleAudioData()` maps SSRCs through `speakingStatus`, creates per-user `ReceiveStream` instances on demand, decodes Opus to PCM, and emits `channel-opus` / `channel-pcm` events.
- DAVE E2EE support is split across `Discord\Voice\Client\WS`, `Discord\Voice\Dave\State`, and `Discord\Voice\Dave\Runtime`. The gateway negotiates DAVE protocol versions and MLS group transitions. **libdave is mandatory**: both `Manager::__construct()` and `WS::__construct()` throw `LibDaveNotFoundException` immediately if `DaveRuntime::isAvailable()` returns false — Discord has required DAVE E2EE for all voice/video connections since March 1st, 2026. The `Dave\Runtime` singleton wraps `ext-ffi` + the platform-specific native library (`libdave.so` / `libdave.dylib` / `libdave.dll`) and exposes `isAvailable()`, `getLastLoadError()`, `getLastDestroyError()`, `maxProtocolVersion()`, `configureCallbacks()` (test hook), `reset()` (test teardown), and `loadHeaderDefinitions()` (public for testing). FFI C declarations are loaded at runtime from the `dave.h` header installed alongside the library (at `.cache/libdave/include/dave/dave.h`); there is no inline CDEF fallback — a missing or unparseable header causes a `RuntimeException`. The header is preprocessed by `loadHeaderDefinitions()` which strips preprocessor directives, expands `DECLARE_OPAQUE_HANDLE()` macros, and removes `DAVE_EXPORT` attributes. `resolveDefaultLibraryPath()` auto-discovers the library by probing the package root and working directory before falling back to a bare filename for system library paths.
- `Discord\Voice\Dave` handle types — `SessionHandle`, `EncryptorHandle`, `DecryptorHandle`, `KeyRatchetHandle` — are thin PHP wrappers around opaque DAVE FFI pointer types. `BinaryFrame` parses and serialises binary voice gateway frames (opcode + sequence + payload bytes) for DAVE gateway messages. `Dave\State` tracks per-connection MLS state: protocol version, group ID, self user ID, passthrough mode flag, and the last received gateway sequence (`seq_ack`).
- `Discord\Voice\OggPage` and `Discord\Voice\OggStream` implement Ogg container parsing. `OggStream` feeds decoded Opus pages into the send pipeline; `OpusHead` and `OpusTags` parse the Ogg Opus header pages.
- `Discord\Voice\ByteBuffer` (`Buffer`, `ReadableBuffer`, `WriteableBuffer`, `AbstractBuffer`, `BufferArrayAccessTrait`, `FormatPackEnum`) is the active byte-buffer implementation used throughout the library. `Discord\Voice\Helpers\ByteBuffer` is a legacy duplicate of the same code retained for backwards compatibility — prefer the non-`Helpers` namespace for new code.
- `Discord\Voice\Processes\Ffmpeg` and `Discord\Voice\Processes\DCA` are child-process wrappers. `Ffmpeg::encode()` always emits Opus on stdout. `Discord\Voice\Processes\OpusFfi` is an FFI-based Opus decoder alternative (marked `@todo`; not yet used in production paths). Both decoders implement `Discord\Voice\Processes\OpusDecoderInterface`.
- `Discord\Voice\Client\User` is a value object (created on inbound audio) that bundles an SSRC, decoder `Process`, `ReceiveStream`, and optional `Speaking` part for a connected voice user.
- `VoiceClient::$ssrcToUserId` and `VoiceClient::$speakingStatus` are both `public`. `WS::handleSpeaking` writes to `ssrcToUserId` from outside `VoiceClient`'s class scope — any visibility narrowing will produce a fatal runtime error.
- `Discord\Voice\Exceptions` is a structured hierarchy: `Exceptions\Channels\` for channel-permission errors (`CantJoinMoreThanOneChannelException`, `CantSpeakInChannelException`, `ChannelMustAllowVoiceException`, `EnterChannelDeniedException`) and `Exceptions\Libraries\` for missing native dependency errors (`LibDaveNotFoundException`, `LibSodiumNotFoundException`, `FFmpegNotFoundException`, `OpusNotFoundException`, `DCANotFoundException`, `OutdatedDCAException`). All are bare `\Exception` subclasses; exceptions that need runtime context (e.g. `LibDaveNotFoundException`) expose a named static factory (`::fromRuntimeError()`) rather than building the message at the call site.
- `Discord\WebSockets\VoicePayload` is the typed DTO for voice gateway JSON payloads; `Discord\WebSockets\OpEnum` is the voice opcode enum. `Discord\Parts\Voice\UserConnected` represents a connected-user Part.

## Key conventions

- Preserve the public backwards-compatibility shims. `Discord\Voice\Client` still exists as an alias subclass for `VoiceClient`, and `ReceiveStream` still subclasses the legacy misspelled `RecieveStream`. Do not remove or rename these surfaces without keeping compatibility.
- `VoiceClient::setData()` is the boot trigger once `token`, `endpoint`, `session`, and `dnsConfig` are present. If you change join/setup flow, update `Manager`, `VoiceClient`, and `Client\WS` together so the handshake still reaches `ready`.
- Playback state changes are strict. Most public playback methods reject when the client is not ready or is already speaking; `pause()` keeps cadence by refreshing silence frames, `stop()` drains the buffer and inserts silence, and `setVolume()` / `setAudioApplication()` are intentionally blocked while audio is playing.
- DAVE frame transforms are injected through `Client\Packet` callbacks and routed through `VoiceClient::encryptDaveFrame()` / `decryptDaveFrame()`. Changes to packet handling need to preserve both RTP encryption and the optional DAVE media layer.
- `Client\WS` records the last gateway sequence in `Dave\State` and reuses it in both heartbeat and resume payloads as `seq_ack`. Changes around reconnect, resume, or binary voice frames need to keep that bookkeeping intact.
- DCA support is legacy. New work should prefer the Ogg/Opus path (`playOggStream()` / `playRawStream()` / `Ffmpeg::encode()`), while `playDCAStream()` remains for compatibility.
- Exceptions in `Exceptions\Libraries\` are bare `\Exception` subclasses. When the exception message depends on runtime state (e.g. an FFI load error), add a `public static function fromRuntimeError(): self` factory to the exception class and throw via `throw ExceptionClass::fromRuntimeError()` — never inline the message-building at the call site.
- Tests for DAVE and voice gateway behavior usually avoid live sockets. The existing tests build `WS` and `VoiceClient` instances with reflection, inject mocked Discord/WebSocket objects, and use `Runtime::configureCallbacks()` to simulate native DAVE behavior. Only the explicit runtime coverage in `tests/Unit/Dave/RuntimeTest.php` expects a real `libdave` library.
- New PHP source follows the repository-wide pattern: `declare(strict_types=1);`, PSR-4 `Discord\` namespaces, and the standard DiscordPHP file header. Use the existing php-cs-fixer/Pint configuration instead of hand-formatting.

## Test patterns

- **Directory split**: `tests/Unit/` covers pure-logic classes (no sockets, no async) — e.g. `OggStream`, `Packet`, `Dave\State`, `Dave\BinaryFrame`. `tests/Feature/Voice/` covers gateway/WebSocket behaviour with mocked sockets and injected Discord objects.
- **No shared helper file**: test helper functions are defined inline at the bottom of each test file (after a `// Helpers` comment). Name them descriptively with the file's subject (e.g. `makeWsForSessionInitTest`, `makeDiscordForManagerRequirementTest`).
- **Reflection-based construction**: classes with complex constructors (`Discord`, `Channel`, `WS`, `VoiceClient`) are built with `(new \ReflectionClass(Foo::class))->newInstanceWithoutConstructor()`. Private properties are injected via `\ReflectionProperty::setAccessible(true)` then `setValue()`. Protected test-only methods are invoked via `\ReflectionMethod::setAccessible(true)` then `invokeArgs()`.
- **Mocking**: use PHPUnit's `$this->getMockBuilder(SomeClass::class)->disableOriginalConstructor()->onlyMethods(['emit'])->getMock()`. Mockery (`Mockery::mock()`) is not used in this project.
- **Payload capture**: to assert what `WS` sends over the wire, pass an `array` by reference into the mock `WebSocket::send()` closure — `$ws->send = function (string $data) use (&$sentPayloads) { $sentPayloads[] = json_decode($data, true); };` — then assert against `$sentPayloads` after calling the method under test.
- **DAVE stubbing** (three strategies):
1. `Runtime::configureCallbacks(availabilityOverride: false)` — makes `isAvailable()` return false without unloading the real library. Use this to test the libdave-unavailable code path in non-RuntimeTest files.
2. `Runtime::configureCallbacks(availabilityOverride: true, ...)` — injects fake native callbacks (encrypt/decrypt/etc.) so the MLS session flows run without a real `libdave.so`. Use this in Feature tests that exercise DAVE MLS state machine transitions.
3. Real `libdave.so` — only `tests/Unit/Dave/RuntimeTest.php` loads the real library; all other tests must not depend on it.
- **DAVE test teardown**: every test file that calls `Runtime::configureCallbacks()` must include `afterEach(function (): void { Runtime::reset(); });` at the top level to restore the singleton state between tests.
- **Skipping without libdave**: use `$this->markTestSkipped('Requires libdave to be available.')` (PHPUnit style) when a test needs the real library and `DaveRuntime::isAvailable()` returns false.
- **Test coverage breadth**: The suite (402 tests) covers exceptions, VoiceClient playback/receive, Manager permission checks, WS handlers (hello/ready/speaking/sessionDescription), WS heartbeat+resume seq_ack bookkeeping, UDP transport, Packet encrypt/decrypt roundtrip, Ogg edge cases, Dave Runtime callback overrides, small value objects (User / HeaderValuesEnum / Dave handles / UserConnected), and process wrappers (Ffmpeg / DCA / OpusFfi). When adding new test files, run `./vendor/bin/pest --compact <file>` to verify, then the full `composer unit` to confirm nothing is broken.
- **`Dave\BinaryFrame` construction**: gateway binary messages are built with `BinaryFrame::encode(opcode, seq, payload)` (or the static helpers) and passed directly to the `WS` handler under test.
24 changes: 17 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "composer" # See documentation for possible values
directory: "/" # Location of package manifests
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "chore(deps)"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "chore(ci)"
23 changes: 23 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Description

<!-- Brief description of what this PR does and why -->

## 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)
- [ ] Linked any related issues below

## Related issues

<!-- Closes #123 -->
79 changes: 79 additions & 0 deletions .github/skills/discord-voice-spec/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: discord-voice-spec
description: Look up Discord Voice protocol specifications — gateway opcodes, DAVE/MLS E2EE, heartbeats, encryption modes, UDP/IP discovery, speaking payloads, buffered resume, and changelog entries. Use when implementing or debugging voice gateway behaviour, DAVE protocol transitions, or any protocol detail that should be verified against Discord's official documentation.
---

# Discord Voice Spec Skill

## Purpose

This skill provides authoritative answers about the Discord Voice protocol by querying Discord's official developer documentation. Use it whenever a task touches a protocol detail that should be verified — such as opcode numbers, payload shapes, encryption modes, DAVE MLS flow, heartbeat/seq_ack requirements, or resume behaviour.

## When to Activate

Activate this skill when the user:

- Asks what a voice gateway opcode does or what payload it carries
- Wants to verify or implement a handshake step (identify → ready → session description)
- Is working on DAVE E2EE — protocol negotiation, MLS transitions, `max_dave_protocol_version`
- Needs to know valid encryption modes (`aead_aes256_gcm_rtpsize`, `aead_xchacha20_poly1305_rtpsize`, etc.)
- Is debugging heartbeat / `seq_ack` / buffered-resume behaviour
- Wants to check UDP IP discovery or RTP packet structure
- Needs to know speaking bitmask values
- Is checking whether Discord has changed the protocol (changelog)

## Reference Paths

The key documentation pages to query:

| Topic | Path |
|---|---|
| Voice gateway protocol | `/developers/topics/voice-connections.mdx` |
| Voice REST resource | `/developers/resources/voice.mdx` |
| Opcodes & status codes | `/developers/topics/opcodes-and-status-codes.mdx` |
| Permissions | `/developers/topics/permissions.mdx` |
| Changelog | `/developers/change-log.mdx` |

## How to Use the MCP Tools

Two tools are available:

- `discord-mcp-search_documentation_discord` — keyword/semantic search across all docs. Use for broad queries ("DAVE protocol", "heartbeat seq_ack", "encryption modes").
- `discord-mcp-query_docs_filesystem_documentation_discord` — direct filesystem access. Use to read a specific page or grep for an exact term.

### Typical patterns

**Read a full page:**
```
command: cat /developers/topics/voice-connections.mdx
```

**Grep for a specific opcode or term:**
```
command: rg -i "seq_ack" /developers/topics/voice-connections.mdx
```

**Search for DAVE-related content:**
```
query: DAVE MLS protocol version negotiation
```

**Check for recent protocol changes:**
```
command: rg -i "voice" /developers/change-log.mdx -C 3
```

## Response Format

Always include:

1. **The exact opcode number or field name** from the docs
2. **The payload shape** (copy from docs if relevant)
3. **A note on the gateway version requirement** if the feature is version-gated (e.g. `seq_ack` is v8+)
4. **A link or path** to the source doc section so the developer can read more

If the docs do not cover the detail asked about, say so clearly — do not invent protocol behaviour.

## Scope

This skill is scoped to the **Discord Voice protocol only**. It does not cover the main Gateway, REST API, interactions, or other Discord subsystems. For those, use a separate reference or general web search.
Loading
Loading