1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static WEBRTC_CLIENT_CHANNEL_OPENED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "webrtc_channel_opened",
18 "WebRTC data channel opened (supports multiple channels)",
19 json!({
20 "type": "send_message",
21 "channel": "netget",
22 "message": "Hello, peer!"
23 }),
24 )
25 .with_parameters(vec![Parameter {
26 name: "channel_label".to_string(),
27 type_hint: "string".to_string(),
28 description: "Data channel label".to_string(),
29 required: true,
30 }])
31});
32
33pub static WEBRTC_CLIENT_MESSAGE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35 EventType::new(
36 "webrtc_message_received",
37 "Message received from WebRTC peer on data channel",
38 json!({
39 "type": "send_message",
40 "message": "Reply message"
41 }),
42 )
43 .with_parameters(vec![
44 Parameter {
45 name: "channel_label".to_string(),
46 type_hint: "string".to_string(),
47 description: "Data channel label where message was received".to_string(),
48 required: true,
49 },
50 Parameter {
51 name: "message".to_string(),
52 type_hint: "string".to_string(),
53 description: "Received message (text or hex-encoded binary)".to_string(),
54 required: true,
55 },
56 Parameter {
57 name: "is_binary".to_string(),
58 type_hint: "boolean".to_string(),
59 description: "Whether message is hex-encoded binary data".to_string(),
60 required: true,
61 },
62 ])
63});
64
65pub static WEBRTC_CLIENT_SIGNALING_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
67 EventType::new(
68 "webrtc_signaling_connected",
69 "Connected to WebSocket signaling server",
70 json!({
71 "type": "wait_for_more"
72 }),
73 )
74 .with_parameters(vec![
75 Parameter {
76 name: "peer_id".to_string(),
77 type_hint: "string".to_string(),
78 description: "Registered peer ID on signaling server".to_string(),
79 required: true,
80 },
81 Parameter {
82 name: "server_url".to_string(),
83 type_hint: "string".to_string(),
84 description: "Signaling server WebSocket URL".to_string(),
85 required: true,
86 },
87 ])
88});
89
90pub struct WebRtcClientProtocol;
92
93impl WebRtcClientProtocol {
94 pub fn new() -> Self {
95 Self
96 }
97}
98
99impl Protocol for WebRtcClientProtocol {
101 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
102 vec![ParameterDefinition {
103 name: "ice_servers".to_string(),
104 description: "STUN/TURN servers for ICE. Default: NONE — host candidates \
105 only, which is what a loopback or LAN peer uses. Set this to \
106 reach anything behind NAT; the default used to be Google's \
107 public STUN server, so merely starting a client talked to a \
108 third party."
109 .to_string(),
110 type_hint: "array".to_string(),
111 required: false,
112 example: json!(["stun:stun.example.com:3478", "turn:turn.example.com:3478"]),
113 default: None,
114 }]
115 }
116
117 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
118 vec![
119 ActionDefinition {
120 name: "send_message".to_string(),
121 description:
122 "Send a text or binary message over a data channel (use 'hex:' prefix for binary)"
123 .to_string(),
124 parameters: vec![
125 Parameter {
126 name: "channel".to_string(),
127 type_hint: "string".to_string(),
128 description: "Channel label (default: 'netget')".to_string(),
129 required: false,
130 },
131 Parameter {
132 name: "message".to_string(),
133 type_hint: "string".to_string(),
134 description: "Message text or 'hex:HEXDATA' for binary".to_string(),
135 required: true,
136 },
137 ],
138 example: json!({
139 "type": "send_message",
140 "channel": "netget",
141 "message": "Hello, WebRTC peer!"
142 }),
143 log_template: None,
144 },
145 ActionDefinition {
146 name: "send_binary".to_string(),
147 description: "Send binary data over a data channel (hex-encoded)".to_string(),
148 parameters: vec![
149 Parameter {
150 name: "channel".to_string(),
151 type_hint: "string".to_string(),
152 description: "Channel label (default: 'netget')".to_string(),
153 required: false,
154 },
155 Parameter {
156 name: "hex_data".to_string(),
157 type_hint: "string".to_string(),
158 description: "Hex-encoded binary data (e.g., '48656c6c6f')".to_string(),
159 required: true,
160 },
161 ],
162 example: json!({
163 "type": "send_binary",
164 "channel": "netget",
165 "hex_data": "48656c6c6f"
166 }),
167 log_template: None,
168 },
169 ActionDefinition {
170 name: "apply_answer".to_string(),
171 description: "Apply the SDP answer from the remote peer to complete connection (manual mode)"
172 .to_string(),
173 parameters: vec![Parameter {
174 name: "answer_json".to_string(),
175 type_hint: "string".to_string(),
176 description: "SDP answer JSON from peer".to_string(),
177 required: true,
178 }],
179 example: json!({
180 "type": "apply_answer",
181 "answer_json": "{\"type\":\"answer\",\"sdp\":\"...\"}"
182 }),
183 log_template: None,
184 },
185 ActionDefinition {
186 name: "create_channel".to_string(),
187 description: "Create a new data channel on the existing connection".to_string(),
188 parameters: vec![Parameter {
189 name: "channel_label".to_string(),
190 type_hint: "string".to_string(),
191 description: "Label for the new data channel".to_string(),
192 required: true,
193 }],
194 example: json!({
195 "type": "create_channel",
196 "channel_label": "file-transfer"
197 }),
198 log_template: None,
199 },
200 ActionDefinition {
201 name: "send_offer".to_string(),
202 description: "Send SDP offer to remote peer via signaling server (WebSocket mode)"
203 .to_string(),
204 parameters: vec![Parameter {
205 name: "target_peer".to_string(),
206 type_hint: "string".to_string(),
207 description: "Target peer ID on signaling server".to_string(),
208 required: true,
209 }],
210 example: json!({
211 "type": "send_offer",
212 "target_peer": "peer-bob"
213 }),
214 log_template: None,
215 },
216 ActionDefinition {
217 name: "disconnect".to_string(),
218 description: "Close all data channels and the peer connection".to_string(),
219 parameters: vec![],
220 example: json!({
221 "type": "disconnect"
222 }),
223 log_template: None,
224 },
225 ]
226 }
227
228 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
229 vec![
230 ActionDefinition {
231 name: "send_message".to_string(),
232 description:
233 "Send a message in response to received data (text or 'hex:' prefix for binary)"
234 .to_string(),
235 parameters: vec![
236 Parameter {
237 name: "channel".to_string(),
238 type_hint: "string".to_string(),
239 description: "Channel label (optional, uses receiving channel)".to_string(),
240 required: false,
241 },
242 Parameter {
243 name: "message".to_string(),
244 type_hint: "string".to_string(),
245 description: "Message text or 'hex:HEXDATA' for binary".to_string(),
246 required: true,
247 },
248 ],
249 example: json!({
250 "type": "send_message",
251 "message": "Reply message"
252 }),
253 log_template: None,
254 },
255 ActionDefinition {
256 name: "disconnect".to_string(),
257 description: "Close the connection".to_string(),
258 parameters: vec![],
259 example: json!({
260 "type": "disconnect"
261 }),
262 log_template: None,
263 },
264 ActionDefinition {
265 name: "wait_for_more".to_string(),
266 description: "Wait for more messages before responding".to_string(),
267 parameters: vec![],
268 example: json!({
269 "type": "wait_for_more"
270 }),
271 log_template: None,
272 },
273 ]
274 }
275
276 fn protocol_name(&self) -> &'static str {
277 "WebRTC"
278 }
279
280 fn get_event_types(&self) -> Vec<EventType> {
298 vec![
299 WEBRTC_CLIENT_CHANNEL_OPENED_EVENT.clone(),
300 WEBRTC_CLIENT_MESSAGE_RECEIVED_EVENT.clone(),
301 WEBRTC_CLIENT_SIGNALING_CONNECTED_EVENT.clone(),
302 ]
303 }
304
305 fn stack_name(&self) -> &'static str {
306 "ETH>IP>UDP>DTLS>SCTP>DataChannel"
307 }
308
309 fn keywords(&self) -> Vec<&'static str> {
310 vec![
311 "webrtc",
312 "webrtc client",
313 "data channel",
314 "peer to peer",
315 "p2p",
316 "signaling",
317 ]
318 }
319
320 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
321 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
322
323 ProtocolMetadataV2::builder()
324 .state(DevelopmentState::Experimental)
325 .implementation("webrtc-rs for data channels with WebSocket signaling support")
326 .llm_control("Full control over data channels, signaling, and binary/text messaging")
327 .e2e_testing("Manual SDP exchange or WebSocket signaling with local/remote peers")
328 .build()
329 }
330
331 fn description(&self) -> &'static str {
332 "WebRTC client for peer-to-peer data channels (text/binary, manual or WebSocket signaling)"
333 }
334
335 fn example_prompt(&self) -> &'static str {
336 "Open WebRTC client with automatic signaling or manual SDP exchange"
337 }
338
339 fn group_name(&self) -> &'static str {
340 "Real-time"
341 }
342 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
343 use crate::llm::actions::StartupExamples;
344 use serde_json::json;
345
346 StartupExamples::new(
347 json!({
349 "type": "open_client",
350 "remote_addr": "ws://localhost:8080/alice",
351 "base_stack": "webrtc",
352 "instruction": "Connect via signaling server and send hello message to peer bob"
353 }),
354 json!({
356 "type": "open_client",
357 "remote_addr": "ws://localhost:8080/alice",
358 "base_stack": "webrtc",
359 "event_handlers": [{
360 "event_pattern": "webrtc_message_received",
361 "handler": {
362 "type": "script",
363 "language": "python",
364 "code": "<webrtc_client_handler>"
365 }
366 }]
367 }),
368 json!({
370 "type": "open_client",
371 "remote_addr": "ws://localhost:8080/alice",
372 "base_stack": "webrtc",
373 "event_handlers": [
374 {
375 "event_pattern": "webrtc_channel_opened",
376 "handler": {
377 "type": "static",
378 "actions": [{
379 "type": "send_message",
380 "message": "Hello, peer!"
381 }]
382 }
383 },
384 {
385 "event_pattern": "webrtc_message_received",
386 "handler": {
387 "type": "static",
388 "actions": [{
389 "type": "disconnect"
390 }]
391 }
392 }
393 ]
394 }),
395 )
396 }
397}
398
399impl Client for WebRtcClientProtocol {
401 fn connect(
402 &self,
403 ctx: crate::protocol::ConnectContext,
404 ) -> std::pin::Pin<
405 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
406 > {
407 Box::pin(async move {
408 use crate::client::webrtc::WebRtcClient;
409 WebRtcClient::connect_with_llm_actions(
410 ctx.remote_addr,
411 ctx.llm_client,
412 ctx.state,
413 ctx.status_tx,
414 ctx.client_id,
415 ctx.startup_params,
416 )
417 .await
418 })
419 }
420
421 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
422 let action_type = action
423 .get("type")
424 .and_then(|v| v.as_str())
425 .context("Missing 'type' field in action")?;
426
427 match action_type {
428 "send_message" => {
429 let message = action
430 .get("message")
431 .and_then(|v| v.as_str())
432 .context("Missing 'message' field")?
433 .to_string();
434
435 let bytes = if let Some(hex_str) = message.strip_prefix("hex:") {
437 hex::decode(hex_str).context("Invalid hex data in message")?
439 } else {
440 message.into_bytes()
442 };
443
444 Ok(ClientActionResult::SendData(bytes))
445 }
446 "send_binary" => {
447 let hex_data = action
448 .get("hex_data")
449 .and_then(|v| v.as_str())
450 .context("Missing 'hex_data' field")?;
451
452 let bytes = hex::decode(hex_data).context("Invalid hex data")?;
454
455 Ok(ClientActionResult::SendData(bytes))
456 }
457 "apply_answer" => {
458 let answer_json = action
459 .get("answer_json")
460 .and_then(|v| v.as_str())
461 .context("Missing 'answer_json' field")?
462 .to_string();
463
464 Ok(ClientActionResult::Custom {
466 name: "apply_answer".to_string(),
467 data: json!({
468 "answer_json": answer_json,
469 }),
470 })
471 }
472 "create_channel" => {
473 let channel_label = action
474 .get("channel_label")
475 .and_then(|v| v.as_str())
476 .context("Missing 'channel_label' field")?
477 .to_string();
478
479 Ok(ClientActionResult::Custom {
481 name: "create_channel".to_string(),
482 data: json!({
483 "channel_label": channel_label,
484 }),
485 })
486 }
487 "send_offer" => {
488 let target_peer = action
489 .get("target_peer")
490 .and_then(|v| v.as_str())
491 .context("Missing 'target_peer' field")?
492 .to_string();
493
494 Ok(ClientActionResult::Custom {
496 name: "send_offer".to_string(),
497 data: json!({
498 "target_peer": target_peer,
499 }),
500 })
501 }
502 "disconnect" => Ok(ClientActionResult::Disconnect),
503 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
504 _ => Err(anyhow::anyhow!(
505 "Unknown WebRTC client action: {}",
506 action_type
507 )),
508 }
509 }
510}