1pub mod actions;
3
4pub use actions::DcClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::io::{AsyncWriteExt, BufReader};
10use tokio::net::TcpStream;
11use tokio::sync::{mpsc, Mutex};
12use tokio_rustls::client::TlsStream;
13use tokio_rustls::rustls::pki_types::ServerName;
14use tokio_rustls::rustls::{ClientConfig, RootCertStore};
15use tokio_rustls::TlsConnector;
16use tracing::{debug, error, info, trace};
17
18use crate::client::dc::actions::{
19 DC_CLIENT_AUTHENTICATED_EVENT, DC_CLIENT_CONNECTED_EVENT, DC_CLIENT_HUBINFO_EVENT,
20 DC_CLIENT_KICKED_EVENT, DC_CLIENT_MESSAGE_RECEIVED_EVENT, DC_CLIENT_REDIRECT_EVENT,
21 DC_CLIENT_SEARCH_RESULT_EVENT, DC_CLIENT_USERLIST_EVENT,
22};
23use crate::client::llm_budget::call_llm_for_client;
24use crate::llm::actions::client_trait::Client;
25use crate::llm::ollama_client::OllamaClient;
26use crate::llm::ClientLlmResult;
27use crate::protocol::Event;
28use crate::state::app_state::AppState;
29use crate::state::{ClientId, ClientStatus};
30
31pub const MAX_RECONNECT_DELAY_SECS: u64 = 60;
33
34pub fn reconnect_delay_secs(initial_delay_secs: u64, attempt: u32) -> u64 {
40 let factor = 2u64
41 .checked_pow(attempt.saturating_sub(1))
42 .unwrap_or(u64::MAX);
43 initial_delay_secs
44 .saturating_mul(factor)
45 .min(MAX_RECONNECT_DELAY_SECS)
46}
47
48pub fn escape_nmdc_text(text: &str) -> String {
54 let mut out = String::with_capacity(text.len());
55 for (i, c) in text.char_indices() {
56 match c {
57 '$' => out.push_str("$"),
58 '|' => out.push_str("|"),
59 '&' if ["amp;", "#36;", "#124;"]
61 .iter()
62 .any(|entity| text[i + 1..].starts_with(entity)) =>
63 {
64 out.push_str("&")
65 }
66 _ => out.push(c),
67 }
68 }
69 out
70}
71
72pub fn validate_nmdc_nickname(nickname: &str) -> Result<&str> {
81 anyhow::ensure!(
82 !nickname.is_empty()
83 && !crate::utils::sanitize::has_controls(nickname)
84 && !nickname
85 .chars()
86 .any(|c| c.is_whitespace() || matches!(c, '$' | '|' | '<' | '>')),
87 "invalid NMDC nickname {:?}: it must be non-empty and contain no whitespace, control \
88 characters, '$', '|', '<' or '>'",
89 nickname
90 );
91 Ok(nickname)
92}
93
94enum DcWriteHalf {
96 Plain(tokio::io::WriteHalf<TcpStream>),
97 Tls(tokio::io::WriteHalf<TlsStream<TcpStream>>),
98}
99
100impl DcWriteHalf {
101 async fn write_all(&mut self, buf: &[u8]) -> Result<()> {
102 match self {
103 DcWriteHalf::Plain(w) => w.write_all(buf).await.map_err(Into::into),
104 DcWriteHalf::Tls(w) => w.write_all(buf).await.map_err(Into::into),
105 }
106 }
107
108 async fn flush(&mut self) -> Result<()> {
109 match self {
110 DcWriteHalf::Plain(w) => w.flush().await.map_err(Into::into),
111 DcWriteHalf::Tls(w) => w.flush().await.map_err(Into::into),
112 }
113 }
114
115 async fn shutdown(&mut self) -> Result<()> {
116 match self {
117 DcWriteHalf::Plain(w) => w.shutdown().await.map_err(Into::into),
118 DcWriteHalf::Tls(w) => w.shutdown().await.map_err(Into::into),
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq)]
125enum DcConnectionState {
126 AwaitingLock, AwaitingHello, Authenticated, }
130
131#[derive(Debug, Clone)]
133struct DcFileEntry {
134 name: String,
135 size: u64,
136 tth: Option<String>, }
138
139struct DcClientState {
141 auth_state: DcConnectionState,
142 nickname: String,
143 description: String,
144 email: String,
145 share_size: u64,
146 memory: String,
147 file_list: Vec<DcFileEntry>, quit_requested: bool,
151}
152
153pub struct DcClient;
155
156impl DcClient {
157 pub async fn connect_with_llm_actions(
159 remote_addr: String,
160 llm_client: OllamaClient,
161 app_state: Arc<AppState>,
162 status_tx: mpsc::UnboundedSender<String>,
163 client_id: ClientId,
164 startup_params: Option<crate::protocol::StartupParams>,
165 ) -> Result<SocketAddr> {
166 let nickname = startup_params
168 .as_ref()
169 .map(|p| p.get_optional_string("nickname"))
170 .transpose()?
171 .flatten()
172 .unwrap_or_else(|| "NetGetUser".to_string());
173 validate_nmdc_nickname(&nickname)?;
174 let description = startup_params
175 .as_ref()
176 .map(|p| p.get_optional_string("description"))
177 .transpose()?
178 .flatten()
179 .unwrap_or_else(|| "NetGet DC Client".to_string());
180 let email = startup_params
181 .as_ref()
182 .map(|p| p.get_optional_string("email"))
183 .transpose()?
184 .flatten()
185 .unwrap_or_else(|| String::new());
186 let share_size = startup_params
187 .as_ref()
188 .map(|p| p.get_optional_u64("share_size"))
189 .transpose()?
190 .flatten()
191 .unwrap_or(0);
192 let use_tls = startup_params
193 .as_ref()
194 .map(|p| p.get_optional_bool("use_tls"))
195 .transpose()?
196 .flatten()
197 .unwrap_or(false);
198 let auto_reconnect = startup_params
199 .as_ref()
200 .map(|p| p.get_optional_bool("auto_reconnect"))
201 .transpose()?
202 .flatten()
203 .unwrap_or(false);
204 let max_reconnect_attempts = startup_params
205 .as_ref()
206 .map(|p| p.get_optional_u64("max_reconnect_attempts"))
207 .transpose()?
208 .flatten()
209 .unwrap_or(5);
210 let max_reconnect_attempts =
211 u32::try_from(max_reconnect_attempts).context("max_reconnect_attempts exceeds u32")?;
212 let initial_reconnect_delay_secs = startup_params
213 .as_ref()
214 .map(|p| p.get_optional_u64("initial_reconnect_delay_secs"))
215 .transpose()?
216 .flatten()
217 .unwrap_or(2);
218
219 Self::connect_with_reconnect(
221 remote_addr,
222 llm_client,
223 app_state,
224 status_tx,
225 client_id,
226 nickname,
227 description,
228 email,
229 share_size,
230 use_tls,
231 auto_reconnect,
232 max_reconnect_attempts,
233 initial_reconnect_delay_secs,
234 )
235 .await
236 }
237
238 async fn connect_with_reconnect(
240 remote_addr: String,
241 llm_client: OllamaClient,
242 app_state: Arc<AppState>,
243 status_tx: mpsc::UnboundedSender<String>,
244 client_id: ClientId,
245 nickname: String,
246 description: String,
247 email: String,
248 share_size: u64,
249 use_tls: bool,
250 auto_reconnect: bool,
251 max_reconnect_attempts: u32,
252 initial_reconnect_delay_secs: u64,
253 ) -> Result<SocketAddr> {
254 let mut reconnect_attempt = 0u32;
255
256 loop {
257 let connect_result = Self::connect_once(
259 remote_addr.clone(),
260 llm_client.clone(),
261 app_state.clone(),
262 status_tx.clone(),
263 client_id,
264 nickname.clone(),
265 description.clone(),
266 email.clone(),
267 share_size,
268 use_tls,
269 auto_reconnect,
270 max_reconnect_attempts,
271 initial_reconnect_delay_secs,
272 reconnect_attempt,
273 )
274 .await;
275
276 match connect_result {
277 Ok(local_addr) => return Ok(local_addr),
278 Err(e) if !auto_reconnect => {
279 return Err(e);
281 }
282 Err(e)
283 if max_reconnect_attempts > 0
284 && reconnect_attempt >= max_reconnect_attempts =>
285 {
286 error!(
288 "DC client {} exhausted reconnection attempts ({}/{}): {}",
289 client_id, reconnect_attempt, max_reconnect_attempts, e
290 );
291 return Err(anyhow::anyhow!(
292 "Failed to connect after {} attempts: {}",
293 max_reconnect_attempts,
294 e
295 ));
296 }
297 Err(e) => {
298 reconnect_attempt = reconnect_attempt.saturating_add(1);
300
301 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await
309 {
310 let event = Event::new(
311 &crate::client::dc::actions::DC_CLIENT_DISCONNECTED_EVENT,
312 serde_json::json!({
313 "reason": e.to_string(),
314 "will_reconnect": true,
315 "reconnect_attempt": reconnect_attempt,
316 }),
317 );
318 let memory = app_state
319 .get_memory_for_client(client_id)
320 .await
321 .unwrap_or_default();
322 match call_llm_for_client(
336 &llm_client,
337 &app_state,
338 client_id.to_string(),
339 &instruction,
340 &memory,
341 Some(&event),
342 &DcClientProtocol::new(),
343 &status_tx,
344 )
345 .await
346 {
347 Ok(result) => {
348 if let Some(mem) = result.memory_updates {
349 app_state.set_memory_for_client(client_id, mem).await;
350 }
351 if !result.actions.is_empty() {
352 let _ = status_tx.send(format!(
353 "[CLIENT] DC client {} is disconnected; {} action(s) \
354 from dc_client_disconnected were not sent",
355 client_id,
356 result.actions.len()
357 ));
358 }
359 }
360 Err(le) => error!(
361 "DC client {} LLM error on dc_client_disconnected: {}",
362 client_id, le
363 ),
364 }
365 }
366 let delay_secs =
367 reconnect_delay_secs(initial_reconnect_delay_secs, reconnect_attempt);
368
369 info!(
370 "DC client {} connection failed (attempt {}/{}), retrying in {}s: {}",
371 client_id,
372 reconnect_attempt,
373 if max_reconnect_attempts == 0 {
374 "∞".to_string()
375 } else {
376 max_reconnect_attempts.to_string()
377 },
378 delay_secs,
379 e
380 );
381
382 let _ = status_tx.send(format!(
383 "[CLIENT] DC client {} reconnecting in {}s (attempt {}/{})",
384 client_id,
385 delay_secs,
386 reconnect_attempt,
387 if max_reconnect_attempts == 0 {
388 "∞".to_string()
389 } else {
390 max_reconnect_attempts.to_string()
391 }
392 ));
393
394 tokio::time::sleep(tokio::time::Duration::from_secs(delay_secs)).await;
396 }
397 }
398 }
399 }
400
401 async fn connect_once(
403 remote_addr: String,
404 llm_client: OllamaClient,
405 app_state: Arc<AppState>,
406 status_tx: mpsc::UnboundedSender<String>,
407 client_id: ClientId,
408 nickname: String,
409 description: String,
410 email: String,
411 share_size: u64,
412 use_tls: bool,
413 _auto_reconnect: bool,
414 _max_reconnect_attempts: u32,
415 _initial_reconnect_delay_secs: u64,
416 _reconnect_attempt: u32,
417 ) -> Result<SocketAddr> {
418 let tcp_stream = TcpStream::connect(&remote_addr)
420 .await
421 .context(format!("Failed to connect to DC hub at {}", remote_addr))?;
422
423 let local_addr = tcp_stream.local_addr()?;
424 let remote_sock_addr = tcp_stream.peer_addr()?;
425
426 if use_tls {
428 info!(
429 "DC client {} establishing TLS connection to {}",
430 client_id, remote_sock_addr
431 );
432
433 let server_name_str = remote_addr.split(':').next().unwrap_or("dc.hub");
435
436 let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default();
440
441 let root_store = RootCertStore {
443 roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
444 };
445
446 let config = ClientConfig::builder()
447 .with_root_certificates(root_store)
448 .with_no_client_auth();
449
450 let connector = TlsConnector::from(Arc::new(config));
451
452 let server_name = match ServerName::try_from(server_name_str.to_string()) {
454 Ok(name) => name,
455 Err(_) => {
456 debug!("Failed to parse server name, using IP");
457 ServerName::try_from(remote_sock_addr.ip().to_string())
458 .map_err(|e| anyhow::anyhow!("Invalid server name: {}", e))?
459 }
460 };
461
462 let tls_stream = connector
464 .connect(server_name, tcp_stream)
465 .await
466 .context("TLS handshake failed")?;
467
468 info!(
469 "DC client {} connected via TLS to {} (local: {})",
470 client_id, remote_sock_addr, local_addr
471 );
472
473 app_state
475 .update_client_status(client_id, ClientStatus::Connected)
476 .await;
477 let _ = status_tx.send(format!("[CLIENT] DC client {} connected (TLS)", client_id));
478 let _ = status_tx.send("__UPDATE_UI__".to_string());
479
480 let (read_half, write_half) = tokio::io::split(tls_stream);
482 let write_half_arc = Arc::new(Mutex::new(DcWriteHalf::Tls(write_half)));
483
484 return run_dc_client_loop(
486 read_half,
487 write_half_arc,
488 client_id,
489 app_state,
490 llm_client,
491 status_tx,
492 nickname,
493 description,
494 email,
495 share_size,
496 local_addr,
497 )
498 .await;
499 }
500
501 info!(
502 "DC client {} connected to {} (local: {})",
503 client_id, remote_sock_addr, local_addr
504 );
505
506 app_state
508 .update_client_status(client_id, ClientStatus::Connected)
509 .await;
510 let _ = status_tx.send(format!("[CLIENT] DC client {} connected", client_id));
511 let _ = status_tx.send("__UPDATE_UI__".to_string());
512
513 let (read_half, write_half) = tokio::io::split(tcp_stream);
515 let write_half_arc = Arc::new(Mutex::new(DcWriteHalf::Plain(write_half)));
516
517 run_dc_client_loop(
519 read_half,
520 write_half_arc,
521 client_id,
522 app_state,
523 llm_client,
524 status_tx,
525 nickname,
526 description,
527 email,
528 share_size,
529 local_addr,
530 )
531 .await
532 }
533}
534
535async fn run_dc_client_loop<R>(
537 read_half: R,
538 write_half_arc: Arc<Mutex<DcWriteHalf>>,
539 client_id: ClientId,
540 app_state: Arc<AppState>,
541 llm_client: OllamaClient,
542 status_tx: mpsc::UnboundedSender<String>,
543 nickname: String,
544 description: String,
545 email: String,
546 share_size: u64,
547 local_addr: SocketAddr,
548) -> Result<SocketAddr>
549where
550 R: tokio::io::AsyncRead + Unpin + Send + 'static,
551{
552 let mut reader = BufReader::new(read_half);
554 let client_state = Arc::new(Mutex::new(DcClientState {
556 auth_state: DcConnectionState::AwaitingLock,
557 nickname: nickname.clone(),
558 description,
559 email,
560 share_size,
561 memory: String::new(),
562 file_list: Vec::new(), quit_requested: false,
564 }));
565
566 let command_rx =
571 crate::client::command_support::register_command_channel(&app_state, client_id).await;
572
573 let cmd_task = tokio::spawn(command_loop(
577 command_rx,
578 client_state.clone(),
579 write_half_arc.clone(),
580 client_id,
581 app_state.clone(),
582 status_tx.clone(),
583 ));
584 app_state.register_client_task(client_id, cmd_task).await;
585
586 let task_registrar = app_state.clone();
590 let task_handle = tokio::spawn(async move {
591 info!("DC client {} read loop started", client_id);
592
593 loop {
599 let framed = tokio::time::timeout(
600 std::time::Duration::from_secs(600),
601 crate::utils::line_reader::read_bounded_delimited(&mut reader, b'|', 64 * 1024),
602 )
603 .await
604 .unwrap_or_else(|_| {
605 Err(std::io::Error::new(
606 std::io::ErrorKind::TimedOut,
607 "DC frame deadline",
608 ))
609 });
610 match framed {
611 Ok(None) => {
612 info!("DC client {} disconnected from hub", client_id);
613 app_state
614 .update_client_status(client_id, ClientStatus::Disconnected)
615 .await;
616 let _ =
617 status_tx.send(format!("[CLIENT] DC client {} disconnected", client_id));
618 let _ = status_tx.send("__UPDATE_UI__".to_string());
619 break;
620 }
621 Ok(Some(frame)) => {
622 let buf = frame;
623 let message = String::from_utf8_lossy(&buf);
627 let segment = message.trim_end_matches('|').trim();
628 if segment.is_empty() {
629 continue;
630 }
631
632 trace!("DC client {} received: {}", client_id, segment);
633
634 if session_end_requested(&client_state).await {
637 end_session(&app_state, &status_tx, client_id).await;
638 break;
639 }
640
641 if let Err(e) = process_dc_message(
643 segment,
644 &client_state,
645 &write_half_arc,
646 &llm_client,
647 &app_state,
648 &status_tx,
649 client_id,
650 )
651 .await
652 {
653 error!("Error processing DC message: {}", e);
654 }
655
656 if session_end_requested(&client_state).await {
659 end_session(&app_state, &status_tx, client_id).await;
660 break;
661 }
662 }
663 Err(e) => {
664 error!("DC client {} read error: {}", client_id, e);
665 app_state
666 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
667 .await;
668 let _ = status_tx.send("__UPDATE_UI__".to_string());
669 break;
670 }
671 }
672 }
673
674 app_state.remove_client_handle(client_id).await;
678 });
679 task_registrar
680 .register_client_task(client_id, task_handle)
681 .await;
682
683 Ok(local_addr)
684}
685
686#[derive(Debug, Clone, PartialEq, Eq)]
688pub struct PrivateMessageParts {
689 pub target: String,
690 pub source: String,
691 pub message: String,
692}
693
694pub fn parse_private_message(message: &str) -> Result<PrivateMessageParts> {
700 let malformed = |what: &str| anyhow::anyhow!("Malformed private message: {}", what);
701
702 if !message.starts_with("$To:") {
703 return Err(anyhow::anyhow!("Not a private message"));
704 }
705
706 let from_pos = message
708 .find(" From:")
709 .ok_or_else(|| malformed("missing 'From:'"))?;
710
711 let target = message
713 .get(5..from_pos)
714 .ok_or_else(|| malformed("no target"))?
715 .trim()
716 .to_string();
717
718 let msg_start = message
720 .get(from_pos..)
721 .and_then(|rest| rest.find('$'))
722 .map(|p| p + from_pos)
723 .ok_or_else(|| malformed("missing message delimiter '$'"))?;
724
725 let source = message
727 .get(from_pos + 6..msg_start)
728 .ok_or_else(|| malformed("no source"))?
729 .trim()
730 .to_string();
731
732 let body = message
735 .get(msg_start + 1..)
736 .ok_or_else(|| malformed("no body"))?;
737 let text = body
738 .strip_prefix('<')
739 .and_then(|rest| rest.split_once('>'))
740 .map(|(_, after)| after.strip_prefix(' ').unwrap_or(after))
741 .unwrap_or(body);
742
743 Ok(PrivateMessageParts {
744 target,
745 source,
746 message: text.to_string(),
747 })
748}
749
750fn parse_chat_message(message: &str) -> Result<(String, String)> {
752 if !message.starts_with('<') {
753 return Err(anyhow::anyhow!("Not a chat message"));
754 }
755
756 let close_bracket_pos = message
758 .char_indices()
759 .find(|(_, c)| *c == '>')
760 .map(|(i, _)| i)
761 .ok_or_else(|| anyhow::anyhow!("Malformed chat message: missing '>'"))?;
762
763 let source = message[1..close_bracket_pos].to_string();
764
765 let msg_start = close_bracket_pos + 1;
767 let msg = if msg_start < message.len() {
768 let msg_slice = &message[msg_start..];
769 msg_slice.trim_start().to_string()
771 } else {
772 String::new()
773 };
774
775 Ok((source, msg))
776}
777
778async fn process_dc_message(
780 message: &str,
781 client_state: &Arc<Mutex<DcClientState>>,
782 write_half: &Arc<Mutex<DcWriteHalf>>,
783 llm_client: &OllamaClient,
784 app_state: &Arc<AppState>,
785 status_tx: &mpsc::UnboundedSender<String>,
786 client_id: ClientId,
787) -> Result<()> {
788 let state = client_state.lock().await;
789 let current_auth_state = state.auth_state.clone();
790 let nickname = state.nickname.clone();
791 let description = state.description.clone();
792 let email = state.email.clone();
793 let share_size = state.share_size;
794 let memory = state.memory.clone();
795 drop(state);
796
797 if message.starts_with("$Lock ") {
799 handle_lock_message(
801 message,
802 client_state,
803 write_half,
804 llm_client,
805 app_state,
806 status_tx,
807 client_id,
808 &nickname,
809 &description,
810 &email,
811 share_size,
812 )
813 .await?;
814 } else if message.starts_with("$Hello ") {
815 handle_hello_message(
817 message,
818 client_state,
819 write_half,
820 llm_client,
821 app_state,
822 status_tx,
823 client_id,
824 &memory,
825 )
826 .await?;
827 } else if message.starts_with("<") {
828 handle_chat_message(
830 message,
831 client_state,
832 write_half,
833 llm_client,
834 app_state,
835 status_tx,
836 client_id,
837 &memory,
838 current_auth_state,
839 )
840 .await?;
841 } else if message.starts_with("$To:") {
842 handle_private_message(
844 message,
845 client_state,
846 write_half,
847 llm_client,
848 app_state,
849 status_tx,
850 client_id,
851 &memory,
852 current_auth_state,
853 )
854 .await?;
855 } else if message.starts_with("$SR ") {
856 handle_search_result(
858 message,
859 client_state,
860 write_half,
861 llm_client,
862 app_state,
863 status_tx,
864 client_id,
865 &memory,
866 current_auth_state,
867 )
868 .await?;
869 } else if message.starts_with("$NickList ") {
870 handle_nicklist(
872 message,
873 client_state,
874 write_half,
875 llm_client,
876 app_state,
877 status_tx,
878 client_id,
879 &memory,
880 current_auth_state,
881 )
882 .await?;
883 } else if message.starts_with("$HubName ") {
884 handle_hub_name(
886 message,
887 client_state,
888 write_half,
889 llm_client,
890 app_state,
891 status_tx,
892 client_id,
893 &memory,
894 current_auth_state,
895 )
896 .await?;
897 } else if message.starts_with("$HubTopic ") {
898 handle_hub_topic(
900 message,
901 client_state,
902 write_half,
903 llm_client,
904 app_state,
905 status_tx,
906 client_id,
907 &memory,
908 current_auth_state,
909 )
910 .await?;
911 } else if message.starts_with("$Kick ") {
912 handle_kick(
914 message,
915 client_state,
916 llm_client,
917 app_state,
918 status_tx,
919 client_id,
920 &memory,
921 )
922 .await?;
923 } else if message.starts_with("$ForceMove ") {
924 handle_redirect(
926 message,
927 client_state,
928 llm_client,
929 app_state,
930 status_tx,
931 client_id,
932 &memory,
933 )
934 .await?;
935 } else if message.starts_with("$GetListLen") || message.starts_with("$UGetBlock") {
936 let state = client_state.lock().await;
939 let files = state.file_list.clone();
940 let nick = state.nickname.clone();
941 drop(state);
942
943 let xml = generate_xml_filelist(&files, &nick);
944 let xml_bytes = xml.as_bytes();
945
946 debug!(
947 "DC client {} responding to file list request with {} files ({} bytes)",
948 client_id,
949 files.len(),
950 xml_bytes.len()
951 );
952
953 info!(
959 "DC client {} received file list request (not fully implemented - use send_dc_raw_command to respond)",
960 client_id
961 );
962 } else {
963 debug!(
965 "DC client {} received unknown message: {}",
966 client_id, message
967 );
968 }
969
970 Ok(())
971}
972
973#[allow(clippy::too_many_arguments)]
975async fn handle_lock_message(
976 message: &str,
977 client_state: &Arc<Mutex<DcClientState>>,
978 write_half: &Arc<Mutex<DcWriteHalf>>,
979 llm_client: &OllamaClient,
980 app_state: &Arc<AppState>,
981 status_tx: &mpsc::UnboundedSender<String>,
982 client_id: ClientId,
983 nickname: &str,
984 description: &str,
985 email: &str,
986 share_size: u64,
987) -> Result<()> {
988 let parts: Vec<&str> = message.split_whitespace().collect();
990 if parts.len() < 2 {
991 return Err(anyhow::anyhow!("Invalid Lock format"));
992 }
993
994 let lock_str = parts[1];
995 let pk = parts
996 .get(2)
997 .and_then(|s| s.strip_prefix("Pk="))
998 .unwrap_or("UnknownHub");
999
1000 info!("DC client {} received Lock from hub '{}'", client_id, pk);
1001
1002 let event = Event::new(
1004 &DC_CLIENT_CONNECTED_EVENT,
1005 serde_json::json!({
1006 "lock": lock_str,
1007 "pk": pk,
1008 }),
1009 );
1010
1011 let instruction = app_state
1012 .get_instruction_for_client(client_id)
1013 .await
1014 .unwrap_or_default();
1015
1016 let memory = client_state.lock().await.memory.clone();
1019
1020 match call_llm_for_client(
1021 llm_client,
1022 app_state,
1023 client_id.to_string(),
1024 &instruction,
1025 &memory,
1026 Some(&event),
1027 &DcClientProtocol::new(),
1028 status_tx,
1029 )
1030 .await
1031 {
1032 Ok(result) => {
1033 if execute_dc_actions(result, client_state, write_half, status_tx, client_id).await {
1037 return Ok(());
1038 }
1039 }
1040 Err(e) => {
1041 error!("LLM error on dc_connected event: {}", e);
1042 }
1043 }
1044
1045 let key = calculate_dc_key(lock_str.as_bytes())?;
1047
1048 let mut key_cmd = b"$Key ".to_vec();
1050 key_cmd.extend(key);
1051 key_cmd.push(b'|');
1052 {
1053 let mut writer = write_half.lock().await;
1054 writer.write_all(&key_cmd).await?;
1055 writer.flush().await?;
1056 }
1057 info!("DC client {} sent Key", client_id);
1058
1059 let validate_cmd = format!("$ValidateNick {}|", nickname);
1061 send_dc_command(write_half, &validate_cmd).await?;
1062 info!("DC client {} sent ValidateNick: {}", client_id, nickname);
1063
1064 let version_cmd = "$Version 1,0091|".to_string();
1066 send_dc_command(write_half, &version_cmd).await?;
1067
1068 let myinfo_cmd = format!(
1070 "$MyINFO $ALL {} {}$ $LAN(T3)A${}${}$|",
1071 nickname,
1072 escape_nmdc_text(description),
1073 escape_nmdc_text(email),
1074 share_size
1075 );
1076 send_dc_command(write_half, &myinfo_cmd).await?;
1077 info!("DC client {} sent MyINFO", client_id);
1078
1079 client_state.lock().await.auth_state = DcConnectionState::AwaitingHello;
1081
1082 Ok(())
1083}
1084
1085async fn handle_hello_message(
1087 message: &str,
1088 client_state: &Arc<Mutex<DcClientState>>,
1089 write_half: &Arc<Mutex<DcWriteHalf>>,
1090 llm_client: &OllamaClient,
1091 app_state: &Arc<AppState>,
1092 status_tx: &mpsc::UnboundedSender<String>,
1093 client_id: ClientId,
1094 memory: &str,
1095) -> Result<()> {
1096 let nickname = message.strip_prefix("$Hello ").unwrap_or("").trim();
1098
1099 info!("DC client {} authenticated as '{}'", client_id, nickname);
1100
1101 client_state.lock().await.auth_state = DcConnectionState::Authenticated;
1103
1104 let event = Event::new(
1106 &DC_CLIENT_AUTHENTICATED_EVENT,
1107 serde_json::json!({
1108 "nickname": nickname,
1109 }),
1110 );
1111
1112 let instruction = app_state
1113 .get_instruction_for_client(client_id)
1114 .await
1115 .unwrap_or_default();
1116
1117 match call_llm_for_client(
1118 llm_client,
1119 app_state,
1120 client_id.to_string(),
1121 &instruction,
1122 memory,
1123 Some(&event),
1124 &DcClientProtocol::new(),
1125 status_tx,
1126 )
1127 .await
1128 {
1129 Ok(result) => {
1130 execute_dc_actions(result, client_state, write_half, status_tx, client_id).await;
1133 }
1134 Err(e) => {
1135 error!("LLM error on dc_authenticated event: {}", e);
1136 }
1137 }
1138
1139 Ok(())
1140}
1141
1142#[allow(clippy::too_many_arguments)]
1144async fn handle_chat_message(
1145 message: &str,
1146 client_state: &Arc<Mutex<DcClientState>>,
1147 write_half: &Arc<Mutex<DcWriteHalf>>,
1148 llm_client: &OllamaClient,
1149 app_state: &Arc<AppState>,
1150 status_tx: &mpsc::UnboundedSender<String>,
1151 client_id: ClientId,
1152 memory: &str,
1153 auth_state: DcConnectionState,
1154) -> Result<()> {
1155 if auth_state != DcConnectionState::Authenticated {
1156 return Ok(()); }
1158
1159 let (source, msg) = match parse_chat_message(message) {
1161 Ok((s, m)) => (s, m),
1162 Err(e) => {
1163 debug!("Failed to parse chat message '{}': {}", message, e);
1164 return Ok(());
1165 }
1166 };
1167
1168 debug!(
1169 "DC client {} received chat from {}: {}",
1170 client_id, source, msg
1171 );
1172
1173 let event = Event::new(
1175 &DC_CLIENT_MESSAGE_RECEIVED_EVENT,
1176 serde_json::json!({
1177 "source": source,
1178 "message": msg,
1179 "is_private": false,
1180 }),
1181 );
1182
1183 let instruction = app_state
1184 .get_instruction_for_client(client_id)
1185 .await
1186 .unwrap_or_default();
1187
1188 match call_llm_for_client(
1189 llm_client,
1190 app_state,
1191 client_id.to_string(),
1192 &instruction,
1193 memory,
1194 Some(&event),
1195 &DcClientProtocol::new(),
1196 status_tx,
1197 )
1198 .await
1199 {
1200 Ok(result) => {
1201 execute_dc_actions(result, client_state, write_half, status_tx, client_id).await;
1202 }
1203 Err(e) => {
1204 error!("LLM error on dc_message_received: {}", e);
1205 }
1206 }
1207
1208 Ok(())
1209}
1210
1211#[allow(clippy::too_many_arguments)]
1213async fn handle_private_message(
1214 message: &str,
1215 client_state: &Arc<Mutex<DcClientState>>,
1216 write_half: &Arc<Mutex<DcWriteHalf>>,
1217 llm_client: &OllamaClient,
1218 app_state: &Arc<AppState>,
1219 status_tx: &mpsc::UnboundedSender<String>,
1220 client_id: ClientId,
1221 memory: &str,
1222 auth_state: DcConnectionState,
1223) -> Result<()> {
1224 if auth_state != DcConnectionState::Authenticated {
1225 return Ok(());
1226 }
1227
1228 let parts = match parse_private_message(message) {
1230 Ok(p) => p,
1231 Err(e) => {
1232 debug!("Failed to parse private message '{}': {}", message, e);
1233 return Ok(());
1234 }
1235 };
1236
1237 let source = parts.source;
1238 let target = parts.target;
1239 let msg = parts.message;
1240
1241 debug!(
1242 "DC client {} received private message from {} (to {}): {}",
1243 client_id, source, target, msg
1244 );
1245
1246 let event = Event::new(
1248 &DC_CLIENT_MESSAGE_RECEIVED_EVENT,
1249 serde_json::json!({
1250 "source": source,
1251 "message": msg,
1252 "is_private": true,
1253 "target": target,
1254 }),
1255 );
1256
1257 let instruction = app_state
1258 .get_instruction_for_client(client_id)
1259 .await
1260 .unwrap_or_default();
1261
1262 match call_llm_for_client(
1263 llm_client,
1264 app_state,
1265 client_id.to_string(),
1266 &instruction,
1267 memory,
1268 Some(&event),
1269 &DcClientProtocol::new(),
1270 status_tx,
1271 )
1272 .await
1273 {
1274 Ok(result) => {
1275 execute_dc_actions(result, client_state, write_half, status_tx, client_id).await;
1276 }
1277 Err(e) => {
1278 error!("LLM error on private message: {}", e);
1279 }
1280 }
1281
1282 Ok(())
1283}
1284
1285#[allow(clippy::too_many_arguments)]
1287async fn handle_search_result(
1288 message: &str,
1289 client_state: &Arc<Mutex<DcClientState>>,
1290 write_half: &Arc<Mutex<DcWriteHalf>>,
1291 llm_client: &OllamaClient,
1292 app_state: &Arc<AppState>,
1293 status_tx: &mpsc::UnboundedSender<String>,
1294 client_id: ClientId,
1295 memory: &str,
1296 auth_state: DcConnectionState,
1297) -> Result<()> {
1298 if auth_state != DcConnectionState::Authenticated {
1299 return Ok(());
1300 }
1301
1302 let without_prefix = message.strip_prefix("$SR ").unwrap_or("");
1305 let parts: Vec<&str> = without_prefix.split('\x05').collect();
1306
1307 if parts.is_empty() {
1308 return Ok(());
1309 }
1310
1311 let first_parts: Vec<&str> = parts[0].split_whitespace().collect();
1312 let source = first_parts.first().unwrap_or(&"").to_string();
1313 let filename = first_parts.get(1).unwrap_or(&"").to_string();
1314 let size = parts
1315 .get(1)
1316 .and_then(|s| s.parse::<u64>().ok())
1317 .unwrap_or(0);
1318
1319 let slots_str = parts.get(2).unwrap_or(&"");
1320 let slots_parts: Vec<&str> = slots_str.split('/').collect();
1321 let free_slots = slots_parts
1322 .first()
1323 .and_then(|s| s.trim().parse::<u64>().ok())
1324 .unwrap_or(0);
1325 let total_slots = slots_parts
1326 .get(1)
1327 .and_then(|s| s.trim().parse::<u64>().ok())
1328 .unwrap_or(0);
1329
1330 debug!(
1331 "DC client {} received search result: {} from {} ({} bytes, {}/{} slots)",
1332 client_id, filename, source, size, free_slots, total_slots
1333 );
1334
1335 let event = Event::new(
1337 &DC_CLIENT_SEARCH_RESULT_EVENT,
1338 serde_json::json!({
1339 "source": source,
1340 "filename": filename,
1341 "size": size,
1342 "free_slots": free_slots,
1343 "total_slots": total_slots,
1344 }),
1345 );
1346
1347 let instruction = app_state
1348 .get_instruction_for_client(client_id)
1349 .await
1350 .unwrap_or_default();
1351
1352 match call_llm_for_client(
1353 llm_client,
1354 app_state,
1355 client_id.to_string(),
1356 &instruction,
1357 memory,
1358 Some(&event),
1359 &DcClientProtocol::new(),
1360 status_tx,
1361 )
1362 .await
1363 {
1364 Ok(result) => {
1365 execute_dc_actions(result, client_state, write_half, status_tx, client_id).await;
1366 }
1367 Err(e) => {
1368 error!("LLM error on search result: {}", e);
1369 }
1370 }
1371
1372 Ok(())
1373}
1374
1375#[allow(clippy::too_many_arguments)]
1377async fn handle_nicklist(
1378 message: &str,
1379 client_state: &Arc<Mutex<DcClientState>>,
1380 write_half: &Arc<Mutex<DcWriteHalf>>,
1381 llm_client: &OllamaClient,
1382 app_state: &Arc<AppState>,
1383 status_tx: &mpsc::UnboundedSender<String>,
1384 client_id: ClientId,
1385 memory: &str,
1386 auth_state: DcConnectionState,
1387) -> Result<()> {
1388 if auth_state != DcConnectionState::Authenticated {
1389 return Ok(());
1390 }
1391
1392 let without_prefix = message.strip_prefix("$NickList ").unwrap_or("");
1394 let users: Vec<String> = without_prefix
1395 .split("$$")
1396 .filter(|s| !s.is_empty())
1397 .map(|s| s.to_string())
1398 .collect();
1399
1400 debug!(
1401 "DC client {} received user list: {} users",
1402 client_id,
1403 users.len()
1404 );
1405
1406 let event = Event::new(
1408 &DC_CLIENT_USERLIST_EVENT,
1409 serde_json::json!({
1410 "users": users,
1411 }),
1412 );
1413
1414 let instruction = app_state
1415 .get_instruction_for_client(client_id)
1416 .await
1417 .unwrap_or_default();
1418
1419 match call_llm_for_client(
1420 llm_client,
1421 app_state,
1422 client_id.to_string(),
1423 &instruction,
1424 memory,
1425 Some(&event),
1426 &DcClientProtocol::new(),
1427 status_tx,
1428 )
1429 .await
1430 {
1431 Ok(result) => {
1432 execute_dc_actions(result, client_state, write_half, status_tx, client_id).await;
1433 }
1434 Err(e) => {
1435 error!("LLM error on userlist: {}", e);
1436 }
1437 }
1438
1439 Ok(())
1440}
1441
1442#[allow(clippy::too_many_arguments)]
1444async fn handle_hub_name(
1445 message: &str,
1446 client_state: &Arc<Mutex<DcClientState>>,
1447 write_half: &Arc<Mutex<DcWriteHalf>>,
1448 llm_client: &OllamaClient,
1449 app_state: &Arc<AppState>,
1450 status_tx: &mpsc::UnboundedSender<String>,
1451 client_id: ClientId,
1452 memory: &str,
1453 auth_state: DcConnectionState,
1454) -> Result<()> {
1455 if auth_state != DcConnectionState::Authenticated {
1456 return Ok(());
1457 }
1458
1459 let hub_name = message.strip_prefix("$HubName ").unwrap_or("").to_string();
1460
1461 debug!("DC client {} hub name: {}", client_id, hub_name);
1462
1463 let event = Event::new(
1465 &DC_CLIENT_HUBINFO_EVENT,
1466 serde_json::json!({
1467 "hub_name": hub_name,
1468 }),
1469 );
1470
1471 let instruction = app_state
1472 .get_instruction_for_client(client_id)
1473 .await
1474 .unwrap_or_default();
1475
1476 match call_llm_for_client(
1477 llm_client,
1478 app_state,
1479 client_id.to_string(),
1480 &instruction,
1481 memory,
1482 Some(&event),
1483 &DcClientProtocol::new(),
1484 status_tx,
1485 )
1486 .await
1487 {
1488 Ok(result) => {
1489 execute_dc_actions(result, client_state, write_half, status_tx, client_id).await;
1490 }
1491 Err(e) => {
1492 error!("LLM error on hub name: {}", e);
1493 }
1494 }
1495
1496 Ok(())
1497}
1498
1499#[allow(clippy::too_many_arguments)]
1501async fn handle_hub_topic(
1502 message: &str,
1503 client_state: &Arc<Mutex<DcClientState>>,
1504 write_half: &Arc<Mutex<DcWriteHalf>>,
1505 llm_client: &OllamaClient,
1506 app_state: &Arc<AppState>,
1507 status_tx: &mpsc::UnboundedSender<String>,
1508 client_id: ClientId,
1509 memory: &str,
1510 auth_state: DcConnectionState,
1511) -> Result<()> {
1512 if auth_state != DcConnectionState::Authenticated {
1513 return Ok(());
1514 }
1515
1516 let hub_topic = message.strip_prefix("$HubTopic ").unwrap_or("").to_string();
1517
1518 debug!("DC client {} hub topic: {}", client_id, hub_topic);
1519
1520 let event = Event::new(
1522 &DC_CLIENT_HUBINFO_EVENT,
1523 serde_json::json!({
1524 "hub_topic": hub_topic,
1525 }),
1526 );
1527
1528 let instruction = app_state
1529 .get_instruction_for_client(client_id)
1530 .await
1531 .unwrap_or_default();
1532
1533 match call_llm_for_client(
1534 llm_client,
1535 app_state,
1536 client_id.to_string(),
1537 &instruction,
1538 memory,
1539 Some(&event),
1540 &DcClientProtocol::new(),
1541 status_tx,
1542 )
1543 .await
1544 {
1545 Ok(result) => {
1546 execute_dc_actions(result, client_state, write_half, status_tx, client_id).await;
1547 }
1548 Err(e) => {
1549 error!("LLM error on hub topic: {}", e);
1550 }
1551 }
1552
1553 Ok(())
1554}
1555
1556async fn handle_kick(
1558 message: &str,
1559 _client_state: &Arc<Mutex<DcClientState>>,
1560 llm_client: &OllamaClient,
1561 app_state: &Arc<AppState>,
1562 status_tx: &mpsc::UnboundedSender<String>,
1563 client_id: ClientId,
1564 memory: &str,
1565) -> Result<()> {
1566 let nickname = message.strip_prefix("$Kick ").unwrap_or("").to_string();
1567
1568 info!("DC client {} was kicked: {}", client_id, nickname);
1569
1570 let event = Event::new(
1572 &DC_CLIENT_KICKED_EVENT,
1573 serde_json::json!({
1574 "nickname": nickname,
1575 }),
1576 );
1577
1578 let instruction = app_state
1579 .get_instruction_for_client(client_id)
1580 .await
1581 .unwrap_or_default();
1582
1583 match call_llm_for_client(
1590 llm_client,
1591 app_state,
1592 client_id.to_string(),
1593 &instruction,
1594 memory,
1595 Some(&event),
1596 &DcClientProtocol::new(),
1597 status_tx,
1598 )
1599 .await
1600 {
1601 Ok(result) => {
1602 if let Some(mem) = result.memory_updates {
1603 app_state.set_memory_for_client(client_id, mem).await;
1604 }
1605 if !result.actions.is_empty() {
1606 info!(
1607 "DC client {}: {} action(s) answered at kicked cannot be sent \
1608 -- the connection is closing",
1609 client_id,
1610 result.actions.len()
1611 );
1612 }
1613 }
1614 Err(e) => error!("DC client {} LLM error on kicked: {}", client_id, e),
1615 }
1616
1617 Ok(())
1618}
1619
1620async fn handle_redirect(
1622 message: &str,
1623 _client_state: &Arc<Mutex<DcClientState>>,
1624 llm_client: &OllamaClient,
1625 app_state: &Arc<AppState>,
1626 status_tx: &mpsc::UnboundedSender<String>,
1627 client_id: ClientId,
1628 memory: &str,
1629) -> Result<()> {
1630 let address = message
1631 .strip_prefix("$ForceMove ")
1632 .unwrap_or("")
1633 .to_string();
1634
1635 info!("DC client {} redirected to {}", client_id, address);
1636
1637 let event = Event::new(
1639 &DC_CLIENT_REDIRECT_EVENT,
1640 serde_json::json!({
1641 "address": address,
1642 }),
1643 );
1644
1645 let instruction = app_state
1646 .get_instruction_for_client(client_id)
1647 .await
1648 .unwrap_or_default();
1649
1650 match call_llm_for_client(
1657 llm_client,
1658 app_state,
1659 client_id.to_string(),
1660 &instruction,
1661 memory,
1662 Some(&event),
1663 &DcClientProtocol::new(),
1664 status_tx,
1665 )
1666 .await
1667 {
1668 Ok(result) => {
1669 if let Some(mem) = result.memory_updates {
1670 app_state.set_memory_for_client(client_id, mem).await;
1671 }
1672 if !result.actions.is_empty() {
1673 info!(
1674 "DC client {}: {} action(s) answered at redirect cannot be sent \
1675 -- the connection is closing",
1676 client_id,
1677 result.actions.len()
1678 );
1679 }
1680 }
1681 Err(e) => error!("DC client {} LLM error on redirect: {}", client_id, e),
1682 }
1683
1684 Ok(())
1685}
1686
1687async fn execute_dc_actions(
1696 result: ClientLlmResult,
1697 client_state: &Arc<Mutex<DcClientState>>,
1698 write_half: &Arc<Mutex<DcWriteHalf>>,
1699 status_tx: &mpsc::UnboundedSender<String>,
1700 client_id: ClientId,
1701) -> bool {
1702 if let Some(mem) = result.memory_updates {
1703 client_state.lock().await.memory = mem;
1704 }
1705
1706 let protocol = DcClientProtocol::new();
1707
1708 for action in result.actions {
1709 let action_type = action
1710 .get("type")
1711 .and_then(|v| v.as_str())
1712 .unwrap_or("<missing type>")
1713 .to_string();
1714 let outcome = match protocol.execute_action(action) {
1715 Ok(action_result) => {
1716 apply_dc_action(action_result, client_state, write_half, client_id).await
1717 }
1718 Err(e) => Err(e),
1719 };
1720 match outcome {
1721 Ok(Applied::Sent(_)) => {}
1722 Ok(Applied::Disconnect) => {
1723 client_state.lock().await.quit_requested = true;
1724 let _ = write_half.lock().await.shutdown().await;
1725 info!(
1726 "DC client {} ending the session: decision=model_disconnect",
1727 client_id
1728 );
1729 return true;
1730 }
1731 Err(e) => {
1732 error!(
1733 "DC client {} action '{}' failed: {}",
1734 client_id, action_type, e
1735 );
1736 let _ = status_tx.send(format!(
1737 "[WARN] DC client {} action '{}' failed: {}",
1738 client_id, action_type, e
1739 ));
1740 }
1741 }
1742 }
1743
1744 false
1745}
1746
1747async fn session_end_requested(client_state: &Arc<Mutex<DcClientState>>) -> bool {
1749 client_state.lock().await.quit_requested
1750}
1751
1752async fn end_session(
1754 app_state: &Arc<AppState>,
1755 status_tx: &mpsc::UnboundedSender<String>,
1756 client_id: ClientId,
1757) {
1758 info!("DC client {} session ended by disconnect", client_id);
1759 app_state
1760 .update_client_status(client_id, ClientStatus::Disconnected)
1761 .await;
1762 let _ = status_tx.send(format!("[CLIENT] DC client {} disconnected", client_id));
1763 let _ = status_tx.send("__UPDATE_UI__".to_string());
1764}
1765
1766enum Applied {
1768 Sent(usize),
1770 Disconnect,
1772}
1773
1774async fn apply_dc_action(
1782 action_result: crate::llm::actions::client_trait::ClientActionResult,
1783 client_state: &Arc<Mutex<DcClientState>>,
1784 write_half: &Arc<Mutex<DcWriteHalf>>,
1785 client_id: ClientId,
1786) -> Result<Applied> {
1787 use crate::llm::actions::client_trait::ClientActionResult;
1788
1789 let nickname = client_state.lock().await.nickname.clone();
1790 let mut sent = 0usize;
1791
1792 {
1793 match action_result {
1794 ClientActionResult::Custom { name, data } => match name.as_str() {
1795 "dc_chat" => {
1796 if let Some(message) = data.get("message").and_then(|v| v.as_str()) {
1797 let cmd = format!("<{}> {}|", nickname, escape_nmdc_text(message));
1798 send_dc_command(write_half, &cmd).await?;
1799 sent = cmd.len();
1800 info!("DC client {} sent chat: {}", client_id, message);
1801 }
1802 }
1803 "dc_private_message" => {
1804 if let (Some(target), Some(message)) = (
1805 data.get("target").and_then(|v| v.as_str()),
1806 data.get("message").and_then(|v| v.as_str()),
1807 ) {
1808 let target = validate_nmdc_nickname(target)
1809 .context("send_dc_private_message target")?;
1810 let cmd = format!(
1811 "$To: {} From: {} $<{}> {}|",
1812 target,
1813 nickname,
1814 nickname,
1815 escape_nmdc_text(message)
1816 );
1817 send_dc_command(write_half, &cmd).await?;
1818 sent = cmd.len();
1819 info!(
1820 "DC client {} sent private message to {}: {}",
1821 client_id, target, message
1822 );
1823 }
1824 }
1825 "dc_search" => {
1826 if let Some(query) = data.get("query").and_then(|v| v.as_str()) {
1827 let cmd = format!(
1829 "$Search Hub:{} F?F?0?1?{}|",
1830 nickname,
1831 escape_nmdc_text(query)
1832 );
1833 send_dc_command(write_half, &cmd).await?;
1834 sent = cmd.len();
1835 info!("DC client {} sent search: {}", client_id, query);
1836 }
1837 }
1838 "dc_myinfo" => {
1839 if let (Some(description), Some(email), Some(share_size)) = (
1840 data.get("description").and_then(|v| v.as_str()),
1841 data.get("email").and_then(|v| v.as_str()),
1842 data.get("share_size").and_then(|v| v.as_u64()),
1843 ) {
1844 let cmd = format!(
1845 "$MyINFO $ALL {} {}$ $LAN(T3)A${}${}$|",
1846 nickname,
1847 escape_nmdc_text(description),
1848 escape_nmdc_text(email),
1849 share_size
1850 );
1851 send_dc_command(write_half, &cmd).await?;
1852 sent = cmd.len();
1853 info!("DC client {} sent MyINFO", client_id);
1854 }
1855 }
1856 "dc_get_nicklist" => {
1857 let cmd = "$GetNickList|".to_string();
1858 send_dc_command(write_half, &cmd).await?;
1859 sent = cmd.len();
1860 info!("DC client {} requested user list", client_id);
1861 }
1862 "dc_filelist" => {
1863 if let Some(files_array) = data.get("files").and_then(|v| v.as_array()) {
1864 let mut file_list = Vec::new();
1866 for file in files_array {
1867 if let (Some(name), Some(size)) = (
1868 file.get("name").and_then(|v| v.as_str()),
1869 file.get("size").and_then(|v| v.as_u64()),
1870 ) {
1871 let tth = file
1872 .get("tth")
1873 .and_then(|v| v.as_str())
1874 .map(|s| s.to_string());
1875
1876 file_list.push(DcFileEntry {
1877 name: name.to_string(),
1878 size,
1879 tth,
1880 });
1881 }
1882 }
1883
1884 client_state.lock().await.file_list = file_list.clone();
1886 info!(
1887 "DC client {} configured file list with {} files",
1888 client_id,
1889 file_list.len()
1890 );
1891 }
1892 }
1893 "dc_raw_command" => {
1894 if let Some(command) = data.get("command").and_then(|v| v.as_str()) {
1895 let mut cmd = command.to_string();
1896 if !cmd.ends_with('|') {
1897 cmd.push('|');
1898 }
1899 send_dc_command(write_half, &cmd).await?;
1900 sent = cmd.len();
1901 info!("DC client {} sent raw command: {}", client_id, command);
1902 }
1903 }
1904 _ => {}
1905 },
1906 ClientActionResult::Disconnect => {
1907 info!("DC client {} disconnecting", client_id);
1908 let cmd = "$Quit|".to_string();
1910 let _ = send_dc_command(write_half, &cmd).await;
1911 return Ok(Applied::Disconnect);
1912 }
1913 ClientActionResult::WaitForMore => {
1914 }
1916 _ => {}
1917 }
1918 }
1919
1920 Ok(Applied::Sent(sent))
1921}
1922
1923async fn command_loop(
1931 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
1932 client_state: Arc<Mutex<DcClientState>>,
1933 write_half: Arc<Mutex<DcWriteHalf>>,
1934 client_id: ClientId,
1935 app_state: Arc<AppState>,
1936 status_tx: mpsc::UnboundedSender<String>,
1937) {
1938 use crate::llm::actions::protocol_trait::Protocol;
1939 use crate::state::client_handles::ClientSendOutcome;
1940 use crate::state::AccessLogOwner;
1941
1942 let protocol = DcClientProtocol::new();
1943
1944 while let Some(command) = command_rx.recv().await {
1945 let action = command.action.clone();
1946 let outcome = match protocol.execute_action(action.clone()) {
1947 Err(e) => Ok(ClientSendOutcome::Rejected {
1948 error: e.to_string(),
1949 }),
1950 Ok(result) => apply_dc_action(result, &client_state, &write_half, client_id)
1951 .await
1952 .map(|applied| match applied {
1953 Applied::Disconnect => ClientSendOutcome::Disconnected,
1954 Applied::Sent(0) => ClientSendOutcome::Executed {
1955 detail: "executed (nothing to write)".to_string(),
1956 },
1957 Applied::Sent(bytes_sent) => ClientSendOutcome::Sent { bytes_sent },
1958 }),
1959 };
1960
1961 let outcome_json = match &outcome {
1962 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
1963 Err(e) => serde_json::json!({"error": e.to_string()}),
1964 };
1965 app_state
1966 .record_access_log(
1967 AccessLogOwner::Client(client_id.as_u32()),
1968 protocol.protocol_name(),
1969 None,
1970 "injected_action",
1971 action,
1972 vec![outcome_json],
1973 )
1974 .await;
1975
1976 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
1977 if let Err(e) = &outcome {
1978 error!("DC client {} injected action failed: {}", client_id, e);
1979 let _ = status_tx.send(format!(
1980 "[WARN] Client {} injected action failed: {}",
1981 client_id, e
1982 ));
1983 }
1984 let _ = status_tx.send("__UPDATE_UI__".to_string());
1985 crate::client::command_support::reply(command, outcome);
1986
1987 if disconnect {
1988 client_state.lock().await.quit_requested = true;
1991 let _ = write_half.lock().await.shutdown().await;
1992 break;
1993 }
1994 }
1995}
1996
1997async fn send_dc_command(write_half: &Arc<Mutex<DcWriteHalf>>, command: &str) -> Result<()> {
1999 let mut write_guard = write_half.lock().await;
2000 write_guard.write_all(command.as_bytes()).await?;
2001 write_guard.flush().await?;
2002 Ok(())
2003}
2004
2005fn generate_xml_filelist(files: &[DcFileEntry], nickname: &str) -> String {
2007 let mut xml = format!("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
2008 xml.push_str(&format!(
2009 "<FileListing Version=\"1\" CID=\"{}\" Base=\"/\" Generator=\"NetGet DC Client\">\n",
2010 nickname
2011 ));
2012
2013 for file in files {
2014 let tth_attr = file
2015 .tth
2016 .as_ref()
2017 .map(|t| format!(" TTH=\"{}\"", t))
2018 .unwrap_or_default();
2019
2020 xml.push_str(&format!(
2021 " <File Name=\"{}\" Size=\"{}\"{}/>\n",
2022 escape_xml(&file.name),
2023 file.size,
2024 tth_attr
2025 ));
2026 }
2027
2028 xml.push_str("</FileListing>\n");
2029 xml
2030}
2031
2032fn escape_xml(s: &str) -> String {
2034 s.replace('&', "&")
2035 .replace('<', "<")
2036 .replace('>', ">")
2037 .replace('"', """)
2038 .replace('\'', "'")
2039}
2040
2041pub fn calculate_dc_key(lock_bytes: &[u8]) -> Result<Vec<u8>> {
2043 let len = lock_bytes.len();
2044
2045 anyhow::ensure!(
2046 (2..=64 * 1024).contains(&len),
2047 "NMDC Lock must contain 2..65536 bytes"
2048 );
2049
2050 let mut key = vec![0u8; len];
2051
2052 for i in 1..len {
2054 key[i] = lock_bytes[i] ^ lock_bytes[i - 1];
2055 }
2056
2057 key[0] = lock_bytes[0] ^ lock_bytes[len - 1] ^ lock_bytes[len - 2] ^ 5;
2058
2059 for byte in &mut key {
2061 *byte = ((*byte << 4) & 0xF0) | ((*byte >> 4) & 0x0F);
2063 }
2064
2065 let mut escaped = Vec::with_capacity(len);
2067 for byte in key {
2068 if matches!(byte, 0 | 5 | 36 | 96 | 124 | 126) {
2069 escaped.extend_from_slice(format!("/%DCN{byte:03}%/").as_bytes());
2070 } else {
2071 escaped.push(byte);
2072 }
2073 }
2074 Ok(escaped)
2075}