Skip to content

Voice support#33

Draft
llsc12 wants to merge 59 commits intomainfrom
voice
Draft

Voice support#33
llsc12 wants to merge 59 commits intomainfrom
voice

Conversation

@llsc12
Copy link
Owner

@llsc12 llsc12 commented Feb 19, 2026

This adds voice support to PaicordLib, with all models and voice protocol in place. Also partial support in Paicord.

@llsc12
Copy link
Owner Author

llsc12 commented Feb 19, 2026

got a majority of the required models in place now. working on voice gateway manager.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds voice support to PaicordLib by introducing the DiscordVoice module with models for voice connections and the voice protocol implementation. The changes include new data structures for RTP packets, voice gateway events, and related payloads following Discord's voice connection protocol.

Changes:

  • Added DiscordVoice module with voice protocol structures for RTP packets and voice gateway communication
  • Introduced new dependencies (swift-sodium and swift-opus) for encryption and audio encoding
  • Moved Gateway.Intent extension from Gateway.swift to VoiceGateway.swift

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
PaicordLib/Sources/PaicordLib/exports.swift Exports the new DiscordVoice module
PaicordLib/Sources/DiscordVoice/DiscordVoice.swift Main voice module file with imports and documentation reference
PaicordLib/Sources/DiscordModels/Types/VoiceUDP.swift RTP packet structure with encoding logic for voice data transmission
PaicordLib/Sources/DiscordModels/Types/VoiceGateway.swift Voice gateway opcodes, events, and Gateway.Intent extension
PaicordLib/Sources/DiscordModels/Types/VoiceGateway+Payloads.swift Payload structures for voice gateway events (Identify, Ready, Speaking, etc.)
PaicordLib/Sources/DiscordModels/Protocols/UDPEncodable.swift Protocol for UDP-encodable types
PaicordLib/Package.swift Added DiscordVoice target and dependencies (swift-sodium, swift-opus)
PaicordLib/.swiftpm/xcode/xcuserdata/llsc12.xcuserdatad/xcschemes/xcschememanagement.plist Xcode scheme management updates for new DiscordVoice scheme
Paicord.xcworkspace/xcshareddata/swiftpm/Package.resolved Package dependency resolution with new swift-opus and swift-sodium packages
DiscordMarkdownParser/.swiftpm/xcode/xcuserdata/llsc12.xcuserdatad/xcschemes/xcschememanagement.plist Xcode scheme order hint update
PaicordLib/Sources/DiscordModels/Types/Gateway.swift Whitespace formatting changes and removal of Gateway.Intent extension (moved to VoiceGateway.swift)

/// Payload Binary data Encrypted audio/video data n bytes
///
/// Discord expects a playout delay RTP extension header on every video packet.
public struct RTPPacket: Sendable {
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The RTPPacket struct has an encode() method that matches the UDPEncodable protocol signature, but doesn't formally conform to the protocol. Consider adding : UDPEncodable to the struct declaration to make this relationship explicit and ensure consistency with the protocol definition.

Suggested change
public struct RTPPacket: Sendable {
public struct RTPPacket: Sendable, UDPEncodable {

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
case .clientFlags: return "clientFlags"
case .clientPlatform: return "clientPlatform"
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Inconsistent indentation: lines 55-56 use extra indentation (8 spaces) compared to the other case statements in the switch (6 spaces). This should be aligned with the other cases for consistency.

Suggested change
case .clientFlags: return "clientFlags"
case .clientPlatform: return "clientPlatform"
case .clientFlags: return "clientFlags"
case .clientPlatform: return "clientPlatform"

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +90
// RFC5285 header
let profile = UInt16(0xBEDE).bigEndian
let lengthWords = UInt16(1).bigEndian

withUnsafeBytes(of: profile) { ext.append(contentsOf: $0) }
withUnsafeBytes(of: lengthWords) { ext.append(contentsOf: $0) }

// extension entry (id = 5, len = 2, 3 byte payload)
let id: UInt8 = 5
let len: UInt8 = 2 // encoded as len-1 in RFC5285
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

In the RTP extension header encoding, the "len" field is commented as "encoded as len-1 in RFC5285", but the actual value used is 2, which would represent a length of 3 bytes. However, the extension data payload is indeed 3 bytes (lines 99-101), plus 1 byte of padding. According to RFC5285, the length field should represent the number of 32-bit words, not bytes. The current value of 1 is correct for one 32-bit word (4 bytes total including the header byte), but the comment is misleading. The comment should clarify that lengthWords represents the number of 32-bit words in the extension, not the data length minus one.

Suggested change
// RFC5285 header
let profile = UInt16(0xBEDE).bigEndian
let lengthWords = UInt16(1).bigEndian
withUnsafeBytes(of: profile) { ext.append(contentsOf: $0) }
withUnsafeBytes(of: lengthWords) { ext.append(contentsOf: $0) }
// extension entry (id = 5, len = 2, 3 byte payload)
let id: UInt8 = 5
let len: UInt8 = 2 // encoded as len-1 in RFC5285
// RFC5285 header: 0xBEDE profile, then length in 32-bit words
let profile = UInt16(0xBEDE).bigEndian
// lengthWords is the number of 32-bit words following this header.
// Here: 1 word = 4 bytes total (1-byte extension header + 3-byte payload).
let lengthWords = UInt16(1).bigEndian
withUnsafeBytes(of: profile) { ext.append(contentsOf: $0) }
withUnsafeBytes(of: lengthWords) { ext.append(contentsOf: $0) }
// extension entry (id = 5, len = 2 -> 3-byte payload; len is encoded as data_length - 1 per RFC5285)
let id: UInt8 = 5
let len: UInt8 = 2

Copilot uses AI. Check for mistakes.
self.data = .clientConnect(try decodeData())
case .video:
self.data = .video(try decodeData())
case .clientDisconnect:
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Trailing whitespace after the colon on line 178. This should be removed to maintain code cleanliness.

Suggested change
case .clientDisconnect:
case .clientDisconnect:

Copilot uses AI. Check for mistakes.
}

/// https://docs.discord.food/topics/voice-connections#voice-platform
public struct ClientPlatform: Sendable, Codable {
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The struct ClientPlatform is empty and has no properties defined. Based on the documentation link, this struct should have properties for platform information. Either implement the struct with the required fields or add a TODO comment explaining what needs to be added.

Suggested change
public struct ClientPlatform: Sendable, Codable {
public struct ClientPlatform: Sendable, Codable {
// TODO: Implement fields for client platform information (e.g. voice platform details)
// as described in the Discord voice connections documentation linked above.

Copilot uses AI. Check for mistakes.
Comment on lines +254 to +290
// MARK: + Gateway.Intent
extension Gateway.Intent {
/// All intents that require no privileges.
/// https://discord.com/developers/docs/topics/gateway#privileged-intents
public static var unprivileged: [Gateway.Intent] {
Gateway.Intent.allCases.filter { !$0.isPrivileged }
}

/// https://discord.com/developers/docs/topics/gateway#privileged-intents
public var isPrivileged: Bool {
switch self {
case .guilds: return false
case .guildMembers: return true
case .guildModeration: return false
case .guildEmojisAndStickers: return false
case .guildIntegrations: return false
case .guildWebhooks: return false
case .guildInvites: return false
case .guildVoiceStates: return false
case .guildPresences: return true
case .guildMessages: return false
case .guildMessageReactions: return false
case .guildMessageTyping: return false
case .directMessages: return false
case .directMessageReactions: return false
case .directMessageTyping: return false
case .messageContent: return true
case .guildScheduledEvents: return false
case .autoModerationConfiguration: return false
case .autoModerationExecution: return false
case .guildMessagePolls: return false
case .directMessagePolls: return false
/// Undocumented cases are considered privileged just to be safe than sorry
case .__undocumented: return true
}
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The exact same Gateway.Intent extension code (lines 254-290 in VoiceGateway.swift) has been moved from Gateway.swift (previously lines 831-867). This creates code duplication. The extension should only exist in one location. Since this is a Gateway.Intent extension, it should remain in Gateway.swift, not in VoiceGateway.swift.

Suggested change
// MARK: + Gateway.Intent
extension Gateway.Intent {
/// All intents that require no privileges.
/// https://discord.com/developers/docs/topics/gateway#privileged-intents
public static var unprivileged: [Gateway.Intent] {
Gateway.Intent.allCases.filter { !$0.isPrivileged }
}
/// https://discord.com/developers/docs/topics/gateway#privileged-intents
public var isPrivileged: Bool {
switch self {
case .guilds: return false
case .guildMembers: return true
case .guildModeration: return false
case .guildEmojisAndStickers: return false
case .guildIntegrations: return false
case .guildWebhooks: return false
case .guildInvites: return false
case .guildVoiceStates: return false
case .guildPresences: return true
case .guildMessages: return false
case .guildMessageReactions: return false
case .guildMessageTyping: return false
case .directMessages: return false
case .directMessageReactions: return false
case .directMessageTyping: return false
case .messageContent: return true
case .guildScheduledEvents: return false
case .autoModerationConfiguration: return false
case .autoModerationExecution: return false
case .guildMessagePolls: return false
case .directMessagePolls: return false
/// Undocumented cases are considered privileged just to be safe than sorry
case .__undocumented: return true
}
}
}
// Gateway.Intent extension is defined in Gateway.swift to avoid duplication.

Copilot uses AI. Check for mistakes.
/// Version + Flags 1 Unsigned byte The RTP version and flags (always 0x80 for voice) 1 byte
/// Payload Type 2 Unsigned byte The type of payload (0x78 with the default Opus configuration) 1 byte
/// Sequence Unsigned short (big endian) The sequence number of the packet 2 bytes
/// Timestamp Unsigned integer (big endian) The RTC timestamp of the packet 4 bytes
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Typo in comment: "RTC timestamp" should be "RTP timestamp" based on the context. RTC typically refers to Real-Time Clock, while RTP (Real-time Transport Protocol) is the correct protocol for this packet structure.

Suggested change
/// Timestamp Unsigned integer (big endian) The RTC timestamp of the packet 4 bytes
/// Timestamp Unsigned integer (big endian) The RTP timestamp of the packet 4 bytes

Copilot uses AI. Check for mistakes.
case aead_aes256_gcm
case xsalsa20_poly1305
case xsalsa20_poly1305_suffix
case xsalsa20_poly1305_lit
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Typo in the encryption mode name: "xsalsa20_poly1305_lit" should likely be "xsalsa20_poly1305_lite" based on the similar mode "xsalsa20_poly1305_lite_rtpsize" on line 148.

Suggested change
case xsalsa20_poly1305_lit
case xsalsa20_poly1305_lite

Copilot uses AI. Check for mistakes.
import Sodium
import Opus

/// https://docs.discord.food/topics/voice-connections#voice-data-interpolation
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The file contains only imports and a documentation link comment, but no actual implementation. This appears to be an incomplete file that should either contain implementation code or be removed. The documentation link suggests there should be code related to voice data interpolation.

Suggested change
/// https://docs.discord.food/topics/voice-connections#voice-data-interpolation
/// https://docs.discord.food/topics/voice-connections#voice-data-interpolation
///
/// This file provides basic utilities for interpolating missing voice data samples.
/// The implementations below are intentionally minimal and self-contained, and can
/// be extended to match Discord's voice data interpolation behaviour as needed.
/// A helper for linearly interpolating missing PCM samples in Discord voice data.
public struct DiscordVoiceInterpolator {
/// Linearly interpolates a sequence of intermediate samples between `start` and `end`.
///
/// - Parameters:
/// - start: The starting sample value.
/// - end: The ending sample value.
/// - steps: The number of intermediate samples to generate.
/// - Returns: An array of `steps` interpolated sample values.
public static func interpolateSamples(start: Int16, end: Int16, steps: Int) -> [Int16] {
guard steps > 0 else { return [] }
let startValue = Double(start)
let endValue = Double(end)
let stepSize = (endValue - startValue) / Double(steps + 1)
return (1...steps).map { index in
let interpolated = startValue + Double(index) * stepSize
let clamped = min(max(interpolated, Double(Int16.min)), Double(Int16.max))
return Int16(clamped.rounded())
}
}
/// Fills `nil` samples in a frame by interpolating between known neighbour samples.
///
/// This assumes 16-bit PCM samples and performs simple linear interpolation.
/// Edge segments (leading or trailing `nil` values) are filled by extending the
/// nearest known sample.
///
/// - Parameter frame: An array of optional `Int16` samples where `nil` indicates
/// a missing sample.
/// - Returns: An array of non-optional `Int16` samples with missing values filled.
public static func interpolateFrame(_ frame: [Int16?]) -> [Int16] {
guard !frame.isEmpty else { return [] }
// First, locate indices of all non-nil "anchor" samples.
var anchors: [(index: Int, value: Int16)] = []
for (idx, sample) in frame.enumerated() {
if let s = sample {
anchors.append((idx, s))
}
}
// If there are no known samples, just return zeros.
if anchors.isEmpty {
return Array(repeating: 0, count: frame.count)
}
var result = Array(repeating: Int16(0), count: frame.count)
// Fill all known samples directly.
for (idx, value) in anchors {
result[idx] = value
}
// Fill leading `nil` segment by extending the first known value.
let firstAnchor = anchors.first!
if firstAnchor.index > 0 {
for idx in 0..<firstAnchor.index {
result[idx] = firstAnchor.value
}
}
// Fill segments between anchors using linear interpolation.
for windowIndex in 0..<(anchors.count - 1) {
let current = anchors[windowIndex]
let next = anchors[windowIndex + 1]
let gap = next.index - current.index - 1
guard gap > 0 else { continue }
let interpolated = interpolateSamples(
start: current.value,
end: next.value,
steps: gap
)
for offset in 0..<gap {
result[current.index + 1 + offset] = interpolated[offset]
}
}
// Fill trailing `nil` segment by extending the last known value.
let lastAnchor = anchors.last!
if lastAnchor.index < frame.count - 1 {
for idx in (lastAnchor.index + 1)..<frame.count {
result[idx] = lastAnchor.value
}
}
return result
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +87
.package(
url: "https://github.com/alta/swift-opus.git",
branch: "main"
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The swift-opus dependency is pinned only to the mutable main branch, which makes your build pipeline dependent on whatever code is currently published there. If that repository is compromised or publishes a malicious change to main, the malicious code will be automatically fetched and executed with the same privileges as your application (e.g., access to tokens and user data). Pin this dependency to an immutable identifier (a specific released version or commit hash) instead of a branch name to reduce supply-chain compromise risk.

Copilot uses AI. Check for mistakes.
@llsc12 llsc12 requested a review from Copilot February 23, 2026 14:43
@llsc12
Copy link
Owner Author

llsc12 commented Feb 23, 2026

/release

@github-actions
Copy link

🔧 /release build requested by @llsc12

Starting a Release-optimized PR build for commit 1cf1c9a. I will upload unsigned artifacts to the workflow run when finished.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 39 out of 39 changed files in this pull request and generated 11 comments.

Comment on lines +881 to +890
case .noConnection, .connecting, .configured:
switch message.payload.opcode.isSentForConnectionEstablishment {
case true:
break
case false:
/// Recursively try to send through the queue.
/// The send queue has slowdown mechanisms so it's fine.
self.send(message: message)
return
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The send(message:) path recursively re-enqueues non-establishment messages while state is .noConnection/.connecting/.configured. If the state never reaches .connected (currently onSuccessfulConnection() is unused), this can create an unbounded stream of queued tasks and log spam. Consider buffering until connected with a bounded queue/timeout, or failing fast with an error to the caller.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +247
print(
"Received an opcode \(opcode.description) that is supposed to be binary, but it came as JSON."
)
self.data = .none
break
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

VoiceGateway.Event.init(from:) uses print(...) during decoding when a binary-only opcode arrives as JSON. This introduces unwanted stdout output from a core model type. Prefer throwing a decoding error (or routing through the library logger) so callers can handle the issue deterministically.

Suggested change
print(
"Received an opcode \(opcode.description) that is supposed to be binary, but it came as JSON."
)
self.data = .none
break
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription:
"Received an opcode \(opcode.description) that is supposed to be binary-only, but it was decoded from JSON."
)
)

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +74
default:
fatalError(
"[Voice Crypto] Unsupported deprecated encryption mode: \(self)"
)
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

EncryptionMode.decrypt crashes the process via fatalError for any mode outside the two supported ones. Since the mode can be server-selected, this risks a runtime crash if Discord negotiates a deprecated/unknown mode. Prefer returning nil or throwing an error, and let higher layers decide whether to disconnect/fallback.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to 34
internal var cookies: [String: Cookie] = [:]

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

CookieStore.cookies was changed from private to internal, which exposes mutable cookie storage across the module. If this is only needed for tests, prefer keeping storage private and adding a test-only accessor (e.g. #if DEBUG/@testable extension) or a read-only snapshot API.

Suggested change
internal var cookies: [String: Cookie] = [:]
private var cookies: [String: Cookie] = [:]
/// Read-only snapshot of all stored cookies.
/// This exposes state without allowing external mutation of the underlying storage.
@usableFromInline
internal var cookieSnapshot: [String: Cookie] {
cookies
}

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +46
<key>DiscordVoice.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

.swiftpm/xcode/xcuserdata/... is user-specific Xcode state and typically shouldn’t be committed (it causes noisy diffs and conflicts across developers/CI). Consider adding **/xcuserdata/ to .gitignore and removing this file from the repo unless there’s a strong reason to keep it.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +41
let socketAddress = try SocketAddress(ipAddress: host, port: port)
let server = try await DatagramBootstrap(
group: NIOSingletons.posixEventLoopGroup
)
.bind(to: socketAddress)
.flatMapThrowing { channel in
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

VoiceConnection.connect binds the UDP socket to the remote voice server address (bind(to: socketAddress)), which is a local bind and will typically fail unless that IP is assigned locally. For a UDP client you generally need to bind to a local/ephemeral address (e.g. 0.0.0.0:0) and send to the remote via AddressedEnvelope.remoteAddress.

Copilot uses AI. Check for mistakes.
Comment on lines +337 to +344
switch event.data {
case .hello(let payload):
self.setupPingTask(
forConnectionWithId: self.connectionId.load(ordering: .relaxed),
every: .milliseconds(payload.heartbeat_interval)
)
case .ready(let payload):
setupUDP(payload)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

sendResumeOrIdentify() is never called, so after connecting the voice WS the client never sends Identify/Resume. In processEvent the .hello case should trigger Identify/Resume (and then start the heartbeat timer); otherwise the connection will stay in .configured and subsequent non-establishment sends will loop/retry indefinitely.

Copilot uses AI. Check for mistakes.
Comment on lines +519 to +526
)
let opcode = Gateway.Opcode.identify
self.send(
message: .init(
payload: resume,
opcode: .init(encodedWebSocketOpcode: opcode.rawValue)!
)
)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

sendResume() derives a WebSocketOpcode from Gateway.Opcode.identify and force-unwraps it. This will send the resume payload with the wrong WebSocket opcode (likely .binary), and can also crash if the mapping fails. Resume/Identify payloads should be sent as WebSocket .text frames.

Copilot uses AI. Check for mistakes.
Comment on lines +673 to +692
self.logger.debug(
"Decoded binary event",
metadata: [
"event": .string("\(event)"),
"opcode": .string(event.opcode.description),
]
)
return event
} else {
let event = try DiscordGlobalConfiguration.decoder.decode(
VoiceGateway.Event.self,
from: Data(buffer: buffer, byteTransferStrategy: .noCopy)
)
self.logger.debug(
"Decoded event",
metadata: [
"event": .string("\(event)"),
"opcode": .string(event.opcode.description),
]
)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Decoded voice gateway events are logged with the entire VoiceGateway.Event object in tryDecodeBufferAsEvent, including sensitive fields like SessionDescription.secretKey and MLS key material. This can leak ephemeral encryption keys to logs, allowing anyone with log access and captured traffic to decrypt voice streams. Avoid logging full event payloads and instead log only non-sensitive metadata (e.g., opcode, IDs) or explicitly redact key/MLS fields before logging.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +84
.package(
url: "https://github.com/alta/swift-opus.git",
branch: "main"
),
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The SwiftPM dependency on https://github.com/alta/swift-opus.git is pinned to the mutable main branch rather than an immutable tag or commit. If that upstream repository or branch is compromised, new builds will automatically pull and execute attacker-controlled code in your voice/audio pipeline. Pin this dependency to a specific version or commit SHA and only update it intentionally after reviewing upstream changes.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

✅ PR /release build finished for commit 1cf1c9a.

Artifacts (unsigned) uploaded as workflow artifacts. Downloads: macOS iOS.

View workflow run

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated 7 comments.

Comment on lines +281 to +284
await gw.voice.updateVoiceConnection(
.join(
channelId: channel.id,
guildId: channel.guild_id!
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The force unwrap on channel.guild_id! could cause a crash if guild_id is nil. Voice channels can exist in DMs or group DMs where guild_id would be nil. Add a guard statement to handle this case properly or verify that this button is only shown for guild voice channels.

Suggested change
await gw.voice.updateVoiceConnection(
.join(
channelId: channel.id,
guildId: channel.guild_id!
guard let guildId = channel.guild_id else { return }
await gw.voice.updateVoiceConnection(
.join(
channelId: channel.id,
guildId: guildId

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +145
private func buildEncryptedPacket(
rtpHeader: Data,
ciphertext: Data,
tag: Data,
nonceSuffix: Data
) -> ByteBuffer {

var buffer = ByteBuffer()
buffer.writeBytes(rtpHeader)
buffer.writeBytes(ciphertext)
buffer.writeBytes(tag)
buffer.writeBytes(nonceSuffix)

return buffer
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The function buildEncryptedPacket is defined but never used in this file. Consider removing it if it's not needed, or ensure it's being called where encryption is needed. If it's intended for future use, consider making it public or package-level with appropriate documentation.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +83
var nonceSuffixValue = UInt32.random(in: .min ... .max)
var beNonceSuffix = nonceSuffixValue.bigEndian
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The variable nonceSuffixValue is declared but its value is immediately converted and not used again. Consider removing the intermediate variable and directly using UInt32.random(in: .min ... .max).bigEndian for clarity and to reduce unnecessary allocations.

Copilot uses AI. Check for mistakes.
import struct NIOWebSocket.WebSocketOpcode

public actor UserGatewayManager: GatewayManager {
public actor UserGatewayManager {
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The UserGatewayManager no longer conforms to the GatewayManager protocol. This is a breaking API change that may affect existing code that expects UserGatewayManager to implement the GatewayManager protocol interface. Ensure this change is documented and all dependent code has been updated.

Suggested change
public actor UserGatewayManager {
public actor UserGatewayManager: GatewayManager {

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +31
let b64encoded = [
"AAEZQEEEoNRlRXBSfG9sxtr5jJxzVcUuTfJslWTSDfqcEtPF8pnyvue1yLTrW24vmka5hnjp67V0c+0wPu5jYTTrJWEmAQABAQA=",
"AAUbAEHwAAEAAQgUFa0c+EQQpQAAAAAAAAAAAgAAAAAAAgABAAEAAkBBBA6ffGO2L8WWMuAQ++A1Guoy24snd0uvi1PRIuJx+4dH6PMtRIfTH+ziN72vzIxctvdpozvYNHu67LyCakgni09AQQTWjD7BWWBExpHTTyoBuq1CF6wIIvSPKBMCYZoepq6kqKubOdIN/wLSlkZd0U118EVDVOMkt7+he3037GO6L0GjQEEEHjylB9FDUAK/ne9bxgQmSS8NdZy59gA4XrjkF4n1BQvbzepPUPln+KlzPUSJ9HazjqDXl19lUWG8YIxtS80K9wABCAVLf4aFQAAAAgABAgACAAACAAEBAAAAAAAAAAD//////////wBARzBFAiEAgAIMGdDd9QJBg489IMOK5grxwnrufTKc2kxwjPx+cgMCIAyukS8MJ9ifqTghaV6WPWTNenyK7W26KIbwpKu9RZhjAEBHMEUCIQCZpnz5onf5GTSD9EiQ4BU0iqZ5+017ntaXxZABk8f20AIgLkH7plqgAhSMUj3CWi7LM1wQJGAywVGlbJpV80In4GBASDBGAiEAz5mrzAcKQDdqgVEMNkUr53PM+Iki2TxZzsars+cAMwoCIQDjN91qFSm7pO8rbHiBLeod/tpwpKpsWk0QbzHpGHw/fw==",
"AAYdAAAAAQABCBQVrRz4RBClAAAAAAAAAAABAAAAAAADIgIg+vGT+WKMUdMhzDRQuUmb7m8ucQBmF7Q8BhrtHiHlhiEAQEYwRAIgQXho5VoNJ9QZOPlcrr1cxQMUNEaUwgYyoUEyaJjqKh0CICWeFzuXJyFWqcJCs7rK21oz9Vu4zLKh8gFtQa0jYke/IGXRtM8sLvaABUM6F7GckyemC6gvCXr+0pfHdaE5EAF+IFehvFBjnOgQyENbJbqs/fHjXOTDSgJg+EMKijIYlUQv"
]
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The test uses hardcoded base64-encoded binary data without explaining what these payloads represent or where they come from. Add comments describing what each test payload represents (e.g., "voice ready event", "session description", etc.) to make the test more maintainable and understandable.

Copilot uses AI. Check for mistakes.
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_INTEROP_MODE = objcxx;
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Setting SWIFT_OBJC_INTEROP_MODE to objcxx enables C++ interoperability which is required for some dependencies (likely DaveKit). However, this is a significant compiler mode change that affects the entire project. Ensure this is intentional and documented, as it may have implications for build times and compatibility with other dependencies.

Suggested change
SWIFT_OBJC_INTEROP_MODE = objcxx;
/* SWIFT_OBJC_INTEROP_MODE not forced to objcxx here; use default (objc) to avoid project-wide ObjC++ mode. */

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +16
init() {
self.opusEncoder = try! Opus.Encoder(format: Self.opusFormat, application: .voip)
self.opusDecoder = try! Opus.Decoder(format: Self.opusFormat, application: .voip)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The force unwrap operator on try! could cause a crash if Opus encoder initialization fails. Consider using proper error handling with do-catch or returning an optional/result type from the initializer to handle initialization failures gracefully.

Suggested change
init() {
self.opusEncoder = try! Opus.Encoder(format: Self.opusFormat, application: .voip)
self.opusDecoder = try! Opus.Decoder(format: Self.opusFormat, application: .voip)
init() throws {
self.opusEncoder = try Opus.Encoder(format: Self.opusFormat, application: .voip)
self.opusDecoder = try Opus.Decoder(format: Self.opusFormat, application: .voip)

Copilot uses AI. Check for mistakes.
@llsc12 llsc12 added enhancement New feature or request priority Issues from sponsors are prioritised labels Mar 11, 2026
@llsc12
Copy link
Owner Author

llsc12 commented Mar 11, 2026

really awesome! just need to add a few more things:

  • voice processing on input node
  • voice user interface

i had some more stuff to add to this but i forgot

@llsc12 llsc12 closed this Mar 11, 2026
@llsc12 llsc12 reopened this Mar 11, 2026
@llsc12
Copy link
Owner Author

llsc12 commented Mar 13, 2026

/release

@github-actions
Copy link

🔧 /release build requested by @llsc12

Starting a Release-optimized PR build for commit d42b3d5. I will upload unsigned artifacts to the workflow run when finished.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority Issues from sponsors are prioritised

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants