///|
/// A decoded voice gateway v8 message.
#non_exhaustive
pub(all) enum VoiceMessage {
Ready(ssrc~ : UInt32, ip~ : String, port~ : Int32, modes~ : Array[String])
Hello(heartbeat_interval~ : Int32)
SessionDescription(
mode~ : String,
secret_key~ : Bytes,
dave_protocol_version~ : Int32
)
HeartbeatAck(nonce~ : Int64)
Speaking(ssrc~ : UInt32, user_id~ : String, flags~ : Int32)
ClientsConnect(user_ids~ : Array[String])
ClientDisconnect(user_id~ : String)
Resumed
DavePrepareTransition(transition_id~ : Int32, protocol_version~ : Int32)
DaveExecuteTransition(transition_id~ : Int32)
DavePrepareEpoch(epoch~ : Int32, protocol_version~ : Int32)
DaveMlsExternalSender(payload~ : Bytes)
DaveMlsProposals(payload~ : Bytes)
DaveMlsAnnounceCommitTransition(transition_id~ : Int32, commit~ : Bytes)
DaveMlsWelcome(transition_id~ : Int32, welcome~ : Bytes)
Unknown(op~ : Int32)
} derive(Debug, Eq)
///|
/// A malformed JSON or binary voice gateway envelope.
#doc(hidden)
pub(all) suberror VoiceEnvelopeError {
InvalidTextFrame(reason~ : String)
InvalidBinaryFrame(reason~ : String)
} derive(Debug, Eq)
///|
fn invalid_text(reason : String) -> VoiceEnvelopeError {
InvalidTextFrame(reason~)
}
///|
fn int_field(value : Json, field : String) -> Int32 raise VoiceEnvelopeError {
guard value is Number(number, ..) else {
raise invalid_text("expected numeric field \{field}")
}
let integer = number.to_int()
if integer.to_double() != number {
raise invalid_text("expected integer field \{field}")
}
integer
}
///|
fn uint_field(value : Json, field : String) -> UInt32 raise VoiceEnvelopeError {
guard value is Number(number, ..) && number >= 0.0 else {
raise invalid_text("expected unsigned field \{field}")
}
let integer = number.to_uint()
if integer.to_double() != number {
raise invalid_text("expected unsigned integer field \{field}")
}
integer
}
///|
fn int64_field(value : Json, field : String) -> Int64 raise VoiceEnvelopeError {
guard value is Number(number, ..) else {
raise invalid_text("expected numeric field \{field}")
}
let integer = number.to_int64()
if integer.to_double() != number {
raise invalid_text("expected integer field \{field}")
}
integer
}
///|
fn string_array(
value : Json,
field : String,
) -> Array[String] raise VoiceEnvelopeError {
guard value is Array(values) else {
raise invalid_text("expected array field \{field}")
}
let result : Array[String] = []
for value in values {
guard value is String(item) else {
raise invalid_text("expected string values in \{field}")
}
result.push(item)
}
result
}
///|
fn byte_array(value : Json, field : String) -> Bytes raise VoiceEnvelopeError {
guard value is Array(values) else {
raise invalid_text("expected array field \{field}")
}
let result : Array[Byte] = []
for value in values {
let item = int_field(value, field)
if item < 0 || item > 255 {
raise invalid_text("byte outside 0..255 in \{field}")
}
result.push(item.to_byte())
}
Bytes::from_array(result)
}
///|
/// Decode a JSON voice gateway message and return its optional top-level
/// buffered-resume sequence number.
#doc(hidden)
pub fn parse_text_frame(
text : String,
) -> (VoiceMessage, Int32?) raise VoiceEnvelopeError {
let root = @json.parse(text) catch {
error => raise invalid_text("invalid JSON: \{error}")
}
guard root is { "op": op_value, "d": data, .. } else {
raise invalid_text("expected top-level op and d")
}
let op = int_field(op_value, "op")
let seq = match root {
{ "seq": value, .. } => Some(int_field(value, "seq"))
_ => None
}
let message = match op {
2 => {
guard data
is { "ssrc": ssrc, "ip": String(ip), "port": port, "modes": modes, .. } else {
raise invalid_text("invalid Ready payload")
}
VoiceMessage::Ready(
ssrc=uint_field(ssrc, "ssrc"),
ip~,
port=int_field(port, "port"),
modes=string_array(modes, "modes"),
)
}
4 => {
guard data
is {
"mode": String(mode),
"secret_key": secret_key,
"dave_protocol_version": version,
..
} else {
raise invalid_text("invalid Session Description payload")
}
SessionDescription(
mode~,
secret_key=byte_array(secret_key, "secret_key"),
dave_protocol_version=int_field(version, "dave_protocol_version"),
)
}
5 => {
guard data
is { "ssrc": ssrc, "user_id": String(user_id), "speaking": flags, .. } else {
raise invalid_text("invalid Speaking payload")
}
Speaking(
ssrc=uint_field(ssrc, "ssrc"),
user_id~,
flags=int_field(flags, "speaking"),
)
}
6 => {
guard data is { "t": nonce, .. } else {
raise invalid_text("invalid Heartbeat ACK payload")
}
HeartbeatAck(nonce=int64_field(nonce, "t"))
}
8 => {
guard data is { "heartbeat_interval": interval, .. } else {
raise invalid_text("invalid Hello payload")
}
Hello(heartbeat_interval=int_field(interval, "heartbeat_interval"))
}
9 => Resumed
11 => {
guard data is { "user_ids": user_ids, .. } else {
raise invalid_text("invalid Clients Connect payload")
}
ClientsConnect(user_ids=string_array(user_ids, "user_ids"))
}
13 => {
guard data is { "user_id": String(user_id), .. } else {
raise invalid_text("invalid Client Disconnect payload")
}
ClientDisconnect(user_id~)
}
21 => {
guard data
is {
"transition_id": transition_id,
"protocol_version": protocol_version,
..
} else {
raise invalid_text("invalid DAVE Prepare Transition payload")
}
DavePrepareTransition(
transition_id=int_field(transition_id, "transition_id"),
protocol_version=int_field(protocol_version, "protocol_version"),
)
}
22 => {
guard data is { "transition_id": transition_id, .. } else {
raise invalid_text("invalid DAVE Execute Transition payload")
}
DaveExecuteTransition(
transition_id=int_field(transition_id, "transition_id"),
)
}
24 => {
guard data is { "epoch": epoch, "protocol_version": protocol_version, .. } else {
raise invalid_text("invalid DAVE Prepare Epoch payload")
}
DavePrepareEpoch(
epoch=int_field(epoch, "epoch"),
protocol_version=int_field(protocol_version, "protocol_version"),
)
}
other => Unknown(op=other)
}
(message, seq)
}
///|
/// Decode a server-to-client binary DAVE frame. Its first two bytes are the
/// big-endian v8 sequence number and the third byte is the opcode.
#doc(hidden)
pub fn parse_binary_frame(
bytes : Bytes,
) -> (VoiceMessage, Int32?) raise VoiceEnvelopeError {
guard bytes is [u16be(seq), u8be(op), .. payload] else {
raise InvalidBinaryFrame(reason="frame is shorter than sequence and opcode")
}
let op = op.reinterpret_as_int()
let message = match op {
25 => DaveMlsExternalSender(payload=payload.to_owned())
27 => DaveMlsProposals(payload=payload.to_owned())
29 | 30 => {
guard payload is [u16be(transition_id), .. mls_message] else {
raise InvalidBinaryFrame(
reason="opcode \{op} payload is missing transition_id",
)
}
let transition_id = transition_id.reinterpret_as_int()
let mls_message = mls_message.to_owned()
if op == 29 {
DaveMlsAnnounceCommitTransition(transition_id~, commit=mls_message)
} else {
DaveMlsWelcome(transition_id~, welcome=mls_message)
}
}
other => Unknown(op=other)
}
(message, Some(seq.reinterpret_as_int()))
}
///|
/// Voice gateway Identify (opcode 0) payload, advertising the selected maximum
/// DAVE protocol version. It defaults to zero so low-level callers do not
/// advertise DAVE without an active backend; pass a supported maximum
/// explicitly to enable negotiation.
#doc(hidden)
pub fn encode_identify(
server_id~ : String,
user_id~ : String,
session_id~ : String,
token~ : String,
max_dave_protocol_version~ : UInt16 = 0,
) -> Json {
{
"op": 0,
"d": {
"server_id": Json::string(server_id),
"user_id": Json::string(user_id),
"session_id": Json::string(session_id),
"token": Json::string(token),
"max_dave_protocol_version": max_dave_protocol_version.to_int(),
},
}
}
///|
/// Voice gateway Heartbeat (opcode 3) payload carrying the nonce and
/// last acknowledged server sequence.
#doc(hidden)
pub fn encode_heartbeat(t~ : Int64, seq_ack~ : Int32) -> Json {
{
"op": 3,
"d": {
"t": Json::number(t.to_double(), repr=t.to_string()),
"seq_ack": seq_ack,
},
}
}
///|
/// Voice gateway Select Protocol (opcode 1) payload announcing the
/// discovered UDP address and chosen encryption mode.
#doc(hidden)
pub fn encode_select_protocol(
address~ : String,
port~ : Int32,
mode~ : String,
) -> Json {
{
"op": 1,
"d": {
"protocol": "udp",
"data": {
"address": Json::string(address),
"port": port,
"mode": Json::string(mode),
},
},
}
}
///|
/// Voice gateway Speaking (opcode 5) payload for the sender's SSRC.
#doc(hidden)
pub fn encode_speaking(
ssrc~ : UInt32,
flags~ : Int32,
delay~ : Int32 = 0,
) -> Json {
{
"op": 5,
"d": {
"speaking": flags,
"delay": delay,
"ssrc": Json::number(ssrc.to_double()),
},
}
}
///|
/// Voice gateway Resume (opcode 7) payload for reattaching to an
/// existing session.
#doc(hidden)
pub fn encode_resume(
server_id~ : String,
session_id~ : String,
token~ : String,
seq_ack~ : Int32,
) -> Json {
{
"op": 7,
"d": {
"server_id": Json::string(server_id),
"session_id": Json::string(session_id),
"token": Json::string(token),
"seq_ack": seq_ack,
},
}
}
///|
/// DAVE Transition Ready (opcode 23) payload acknowledging an announced
/// protocol transition.
#doc(hidden)
pub fn encode_transition_ready(transition_id~ : Int32) -> Json {
{ "op": 23, "d": { "transition_id": transition_id } }
}
///|
/// DAVE Invalid Commit/Welcome (opcode 31) payload asking the server to
/// reset the group after a rejected MLS message.
#doc(hidden)
pub fn encode_invalid_commit_welcome(transition_id~ : Int32) -> Json {
{ "op": 31, "d": { "transition_id": transition_id } }
}
///|
/// Prefix a client-to-server binary DAVE payload with its one-byte opcode.
#doc(hidden)
pub fn encode_binary_client_frame(op : Int32, payload : Bytes) -> Bytes {
let bytes : Array[Byte] = [op.to_byte()]
for byte in payload {
bytes.push(byte)
}
Bytes::from_array(bytes)
}