///|
/// A decoded voice gateway v8 message.
pub(all) enum VoiceMessage {
Ready(ssrc~ : UInt, ip~ : String, port~ : Int, modes~ : Array[String])
Hello(heartbeat_interval~ : Int)
SessionDescription(
mode~ : String,
secret_key~ : Bytes,
dave_protocol_version~ : Int
)
HeartbeatAck(nonce~ : Int64)
Speaking(ssrc~ : UInt, user_id~ : String, flags~ : Int)
ClientsConnect(user_ids~ : Array[String])
ClientDisconnect(user_id~ : String)
Resumed
DavePrepareTransition(transition_id~ : Int, protocol_version~ : Int)
DaveExecuteTransition(transition_id~ : Int)
DavePrepareEpoch(epoch~ : Int, protocol_version~ : Int)
DaveMlsExternalSender(payload~ : Bytes)
DaveMlsProposals(payload~ : Bytes)
DaveMlsAnnounceCommitTransition(transition_id~ : Int, commit~ : Bytes)
DaveMlsWelcome(transition_id~ : Int, welcome~ : Bytes)
Unknown(op~ : Int)
} derive(Debug, Eq)
///|
/// A malformed JSON or binary voice gateway envelope.
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) -> Int 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) -> UInt 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.
pub fn parse_text_frame(
text : String,
) -> (VoiceMessage, Int?) 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.
pub fn parse_binary_frame(
bytes : Bytes,
) -> (VoiceMessage, Int?) raise VoiceEnvelopeError {
if bytes.length() < 3 {
raise InvalidBinaryFrame(reason="frame is shorter than sequence and opcode")
}
let seq = (bytes[0].to_int() << 8) | bytes[1].to_int()
let op = bytes[2].to_int()
let payload = bytes[3:].to_owned()
let message = match op {
25 => DaveMlsExternalSender(payload~)
27 => DaveMlsProposals(payload~)
29 | 30 => {
if payload.length() < 2 {
raise InvalidBinaryFrame(
reason="opcode \{op} payload is missing transition_id",
)
}
let transition_id = (payload[0].to_int() << 8) | payload[1].to_int()
let mls_message = payload[2:].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))
}
///|
/// 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.
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.
pub fn encode_heartbeat(t~ : Int64, seq_ack~ : Int) -> 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.
pub fn encode_select_protocol(
address~ : String,
port~ : Int,
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.
pub fn encode_speaking(ssrc~ : UInt, flags~ : Int, delay? : Int = 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.
pub fn encode_resume(
server_id~ : String,
session_id~ : String,
token~ : String,
seq_ack~ : Int,
) -> 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.
pub fn encode_transition_ready(transition_id~ : Int) -> 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.
pub fn encode_invalid_commit_welcome(transition_id~ : Int) -> Json {
{ "op": 31, "d": { "transition_id": transition_id } }
}
///|
/// Prefix a client-to-server binary DAVE payload with its one-byte opcode.
pub fn encode_binary_client_frame(op : Int, payload : Bytes) -> Bytes {
let bytes : Array[Byte] = [op.to_byte()]
for byte in payload {
bytes.push(byte)
}
Bytes::from_array(bytes)
}