1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "kebab-case")]
9pub enum Role {
10 Driver,
11 Observer,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum Direction {
17 RobotListens,
18 PilotListens,
19 Both,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Candidate {
24 pub url: String,
25 pub label: String,
26 pub priority: u32,
27 pub needs_probe: bool,
28 pub family: u8,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct ChannelInfo {
33 pub id: u8,
34 pub kind: String,
35 pub name: String,
36 pub codec: String,
37 #[serde(default)]
38 pub fps: u32,
39}
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct Announce {
43 pub candidates: Vec<Candidate>,
44 pub cert_fingerprints: Vec<String>,
45 pub alpns: Vec<String>,
46 pub nat_report: serde_json::Value,
47 pub channels: Vec<ChannelInfo>,
48 pub p2p_hint: String,
49 pub max_sessions: u32,
50 #[serde(default)]
53 pub relay: bool,
54}
55
56#[derive(Debug, Clone, Serialize)]
57#[serde(tag = "type", rename_all = "kebab-case")]
58pub enum Outbound {
59 Auth {
60 v: u8,
61 role: String,
62 robot_id: String,
63 public_key: String,
64 sig: String,
65 agent_version: String,
66 },
67 Announce(Announce),
68 Heartbeat {
69 sessions: Vec<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 status: Option<serde_json::Value>,
72 },
73 SessionAccepted {
74 session_id: String,
75 path_label: String,
76 },
77 SessionEnded {
78 session_id: String,
79 reason: String,
80 },
81}
82
83#[derive(Debug, Clone, Deserialize)]
84#[serde(tag = "type", rename_all = "kebab-case")]
85pub enum Inbound {
86 Challenge {
87 nonce: String,
88 },
89 AuthOk {
90 #[serde(default)]
91 robot_id: String,
92 },
93 Denied {
94 reason: String,
95 },
96 PilotConnecting {
97 session_id: String,
98 #[serde(default)]
99 pilot_ip: Option<String>,
100 #[serde(default = "driver")]
101 role: Role,
102 #[serde(default = "robot_listens")]
103 direction: Direction,
104 },
105 Punch {
106 session_id: String,
107 #[serde(default)]
108 pilot_ip: Option<String>,
109 },
110 SessionRevoked {
111 session_id: String,
112 #[serde(default)]
113 reason: String,
114 },
115 RelayOpen {
118 session_id: String,
119 url: String,
120 token: String,
121 #[serde(default = "driver")]
122 role: Role,
123 #[serde(default)]
124 pilot_ip: Option<String>,
125 },
126 #[serde(other)]
127 Unknown,
128}
129
130fn driver() -> Role {
131 Role::Driver
132}
133fn robot_listens() -> Direction {
134 Direction::RobotListens
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn announce_tags_as_type_announce() {
143 let a = Announce {
144 candidates: vec![],
145 cert_fingerprints: vec!["ab".into()],
146 alpns: vec!["h3".into()],
147 nat_report: serde_json::json!({}),
148 channels: vec![],
149 p2p_hint: "likely".into(),
150 max_sessions: 4,
151 relay: true,
152 };
153 let v = serde_json::to_value(Outbound::Announce(a)).unwrap();
154 assert_eq!(v["type"], "announce");
155 assert_eq!(v["alpns"][0], "h3");
156 }
157
158 #[test]
159 fn inbound_parses_and_tolerates_unknown() {
160 let m: Inbound = serde_json::from_str(r#"{"type":"pilot-connecting","session_id":"abc","pilot_ip":"1.2.3.4","role":"observer","direction":"robot-listens"}"#).unwrap();
161 match m {
162 Inbound::PilotConnecting { role, .. } => assert_eq!(role, Role::Observer),
163 _ => panic!(),
164 }
165 let m: Inbound = serde_json::from_str(r#"{"type":"something-new","x":1}"#).unwrap();
166 assert!(matches!(m, Inbound::Unknown));
167 }
168}