1use anyhow::{Context as _, Result, anyhow, bail};
27use data_encoding::BASE32_NOPAD;
28use futures_lite::StreamExt;
29use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
30use iroh_gossip::net::{Event, GOSSIP_ALPN, Gossip, GossipEvent, JoinOptions};
31use iroh_gossip::proto::TopicId;
32use parking_lot::Mutex;
33use std::collections::{BTreeSet, HashMap};
34use std::env;
35use tokio::sync::{RwLock, oneshot};
36use tokio::task::JoinHandle;
37use url::Url;
38
39use crate::EventType;
40use crate::chat::send_msg;
41use crate::config::Config;
42use crate::context::Context;
43use crate::log::warn;
44use crate::message::{Message, MsgId, Viewtype};
45use crate::mimeparser::SystemMessage;
46
47const PUBLIC_KEY_LENGTH: usize = 32;
49const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
50
51#[derive(Debug)]
53pub struct Iroh {
54 pub(crate) router: iroh::protocol::Router,
56
57 pub(crate) gossip: Gossip,
59
60 pub(crate) sequence_numbers: Mutex<HashMap<TopicId, i32>>,
62
63 pub(crate) iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
65
66 pub(crate) public_key: PublicKey,
70}
71
72impl Iroh {
73 pub(crate) async fn network_change(&self) {
75 self.router.endpoint().network_change().await
76 }
77
78 pub(crate) async fn close(self) -> Result<()> {
80 self.router.shutdown().await.context("Closing iroh failed")
81 }
82
83 async fn join_and_subscribe_gossip(
89 &self,
90 ctx: &Context,
91 msg_id: MsgId,
92 ) -> Result<Option<oneshot::Receiver<()>>> {
93 let topic = get_iroh_topic_for_msg(ctx, msg_id)
94 .await?
95 .with_context(|| format!("Message {msg_id} has no gossip topic"))?;
96
97 let mut iroh_channels = self.iroh_channels.write().await;
102
103 if iroh_channels.contains_key(&topic) {
104 return Ok(None);
105 }
106
107 let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
108 let node_ids = peers.iter().map(|p| p.node_id).collect::<Vec<_>>();
109
110 info!(
111 ctx,
112 "IROH_REALTIME: Joining gossip {topic} with peers: {:?}.", node_ids,
113 );
114
115 for node_addr in &peers {
117 if !node_addr.is_empty() {
118 self.router.endpoint().add_node_addr(node_addr.clone())?;
119 }
120 }
121
122 let (join_tx, join_rx) = oneshot::channel();
123
124 let (gossip_sender, gossip_receiver) = self
125 .gossip
126 .subscribe_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
127 .split();
128
129 let ctx = ctx.clone();
130 let subscribe_loop = tokio::spawn(async move {
131 if let Err(e) = subscribe_loop(&ctx, gossip_receiver, topic, msg_id, join_tx).await {
132 warn!(ctx, "subscribe_loop failed: {e}")
133 }
134 });
135
136 iroh_channels.insert(topic, ChannelState::new(subscribe_loop, gossip_sender));
137
138 Ok(Some(join_rx))
139 }
140
141 pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
143 if self.iroh_channels.read().await.get(&topic).is_some() {
144 self.router.endpoint().add_node_addr(peer.clone())?;
145 self.gossip.subscribe(topic, vec![peer.node_id])?;
146 }
147 Ok(())
148 }
149
150 pub async fn send_webxdc_realtime_data(
152 &self,
153 ctx: &Context,
154 msg_id: MsgId,
155 mut data: Vec<u8>,
156 ) -> Result<()> {
157 let topic = get_iroh_topic_for_msg(ctx, msg_id)
158 .await?
159 .with_context(|| format!("Message {msg_id} has no gossip topic"))?;
160 self.join_and_subscribe_gossip(ctx, msg_id).await?;
161
162 let seq_num = self.get_and_incr(&topic);
163
164 let mut iroh_channels = self.iroh_channels.write().await;
165 let state = iroh_channels
166 .get_mut(&topic)
167 .context("Just created state does not exist")?;
168 data.extend(seq_num.to_le_bytes());
169 data.extend(self.public_key.as_bytes());
170
171 state.sender.broadcast(data.into()).await?;
172
173 if env::var("REALTIME_DEBUG").is_ok() {
174 info!(ctx, "Sent realtime data");
175 }
176
177 Ok(())
178 }
179
180 fn get_and_incr(&self, topic: &TopicId) -> i32 {
181 let mut sequence_numbers = self.sequence_numbers.lock();
182 let entry = sequence_numbers.entry(*topic).or_default();
183 *entry = entry.wrapping_add(1);
184 *entry
185 }
186
187 pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
193 let mut addr = self.router.endpoint().node_addr().await?;
194 addr.direct_addresses = BTreeSet::new();
195 debug_assert!(addr.relay_url().is_some());
196 Ok(addr)
197 }
198
199 pub(crate) async fn leave_realtime(&self, topic: TopicId) -> Result<()> {
201 if let Some(channel) = self.iroh_channels.write().await.remove(&topic) {
202 channel.subscribe_loop.abort();
210 let _ = channel.subscribe_loop.await;
211 }
212 Ok(())
213 }
214}
215
216#[derive(Debug)]
218pub(crate) struct ChannelState {
219 subscribe_loop: JoinHandle<()>,
221
222 sender: iroh_gossip::net::GossipSender,
223}
224
225impl ChannelState {
226 fn new(subscribe_loop: JoinHandle<()>, sender: iroh_gossip::net::GossipSender) -> Self {
227 Self {
228 subscribe_loop,
229 sender,
230 }
231 }
232}
233
234impl Context {
235 async fn init_peer_channels(&self) -> Result<Iroh> {
237 info!(self, "Initializing peer channels.");
238 let secret_key = SecretKey::generate(rand_old::rngs::OsRng);
239 let public_key = secret_key.public();
240
241 let relay_mode = if let Some(relay_url) = self
242 .metadata
243 .read()
244 .await
245 .as_ref()
246 .and_then(|conf| conf.iroh_relay.clone())
247 {
248 RelayMode::Custom(RelayUrl::from(relay_url).into())
249 } else {
250 RelayMode::Default
253 };
254
255 let endpoint = Endpoint::builder()
256 .tls_x509() .secret_key(secret_key)
258 .alpns(vec![GOSSIP_ALPN.to_vec()])
259 .relay_mode(relay_mode)
260 .bind()
261 .await?;
262
263 let gossip = Gossip::builder()
269 .max_message_size(128 * 1024)
270 .spawn(endpoint.clone())
271 .await?;
272
273 let router = iroh::protocol::Router::builder(endpoint)
274 .accept(GOSSIP_ALPN, gossip.clone())
275 .spawn();
276
277 Ok(Iroh {
278 router,
279 gossip,
280 sequence_numbers: Mutex::new(HashMap::new()),
281 iroh_channels: RwLock::new(HashMap::new()),
282 public_key,
283 })
284 }
285
286 pub async fn get_peer_channels(&self) -> Option<tokio::sync::RwLockReadGuard<'_, Iroh>> {
288 tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
289 self.iroh.read().await,
290 |opt_iroh| opt_iroh.as_ref(),
291 )
292 .ok()
293 }
294
295 pub async fn get_or_try_init_peer_channel(
297 &self,
298 ) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
299 if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
300 bail!("Attempt to initialize Iroh when realtime is disabled");
301 }
302
303 if let Some(lock) = self.get_peer_channels().await {
304 return Ok(lock);
305 }
306
307 let lock = self.iroh.write().await;
308 match tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
309 lock,
310 |opt_iroh| opt_iroh.as_ref(),
311 ) {
312 Ok(lock) => Ok(lock),
313 Err(mut lock) => {
314 let iroh = self.init_peer_channels().await?;
315 *lock = Some(iroh);
316 tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
317 lock,
318 |opt_iroh| opt_iroh.as_ref(),
319 )
320 .map_err(|_| anyhow!("Downgrade should succeed as we just stored `Some` value"))
321 }
322 }
323 }
324
325 pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
326 if let Some(iroh) = &*self.iroh.read().await {
327 info!(
328 self,
329 "Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
330 );
331 iroh.maybe_add_gossip_peer(topic, peer).await?;
332 }
333 Ok(())
334 }
335}
336
337pub(crate) async fn iroh_add_peer_for_topic(
339 ctx: &Context,
340 msg_id: MsgId,
341 topic: TopicId,
342 peer: NodeId,
343 relay_server: Option<&str>,
344) -> Result<()> {
345 ctx.sql
346 .execute(
347 "INSERT OR REPLACE INTO iroh_gossip_peers (msg_id, public_key, topic, relay_server) VALUES (?, ?, ?, ?)",
348 (msg_id, peer.as_bytes(), topic.as_bytes(), relay_server),
349 )
350 .await?;
351 Ok(())
352}
353
354pub async fn add_gossip_peer_from_header(
356 context: &Context,
357 instance_id: MsgId,
358 node_addr: &str,
359) -> Result<()> {
360 if !context
361 .get_config_bool(Config::WebxdcRealtimeEnabled)
362 .await?
363 {
364 return Ok(());
365 }
366
367 let node_addr =
368 serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
369
370 info!(
371 context,
372 "Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
373 );
374
375 context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
376 msg_id: instance_id,
377 });
378
379 let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? else {
380 warn!(
381 context,
382 "Could not add iroh peer because {instance_id} has no topic."
383 );
384 return Ok(());
385 };
386
387 let node_id = node_addr.node_id;
388 let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
389 iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
390
391 context.maybe_add_gossip_peer(topic, node_addr).await?;
392 Ok(())
393}
394
395pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: TopicId) -> Result<()> {
397 ctx.sql
398 .execute(
399 "INSERT OR REPLACE INTO iroh_gossip_peers (msg_id, public_key, topic, relay_server) VALUES (?, ?, ?, ?)",
400 (msg_id, PUBLIC_KEY_STUB, topic.as_bytes(), Option::<&str>::None),
401 )
402 .await?;
403 Ok(())
404}
405
406async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeAddr>> {
408 ctx.sql
409 .query_map(
410 "SELECT public_key, relay_server FROM iroh_gossip_peers WHERE msg_id = ? AND public_key != ?",
411 (msg_id, PUBLIC_KEY_STUB),
412 |row| {
413 let key: Vec<u8> = row.get(0)?;
414 let server: Option<String> = row.get(1)?;
415 Ok((key, server))
416 },
417 |g| {
418 g.map(|data| {
419 let (key, server) = data?;
420 let server = server.map(|data| Ok::<_, url::ParseError>(RelayUrl::from(Url::parse(&data)?))).transpose()?;
421 let id = NodeId::from_bytes(&key.try_into()
422 .map_err(|_| anyhow!("Can't convert sql data to [u8; 32]"))?)?;
423 Ok::<_, anyhow::Error>(NodeAddr::from_parts(
424 id, server, vec![]
425 ))
426 })
427 .collect::<std::result::Result<Vec<_>, _>>()
428 },
429 )
430 .await
431}
432
433pub(crate) async fn get_iroh_topic_for_msg(
435 ctx: &Context,
436 msg_id: MsgId,
437) -> Result<Option<TopicId>> {
438 if let Some(bytes) = ctx
439 .sql
440 .query_get_value::<Vec<u8>>(
441 "SELECT topic FROM iroh_gossip_peers WHERE msg_id = ? LIMIT 1",
442 (msg_id,),
443 )
444 .await
445 .context("Couldn't restore topic from db")?
446 {
447 let topic_id = TopicId::from_bytes(
448 bytes
449 .try_into()
450 .map_err(|_| anyhow!("Could not convert stored topic ID"))?,
451 );
452 Ok(Some(topic_id))
453 } else {
454 Ok(None)
455 }
456}
457
458pub async fn send_webxdc_realtime_advertisement(
461 ctx: &Context,
462 msg_id: MsgId,
463) -> Result<Option<oneshot::Receiver<()>>> {
464 if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
465 return Ok(None);
466 }
467
468 let iroh = ctx.get_or_try_init_peer_channel().await?;
469 let conn = iroh.join_and_subscribe_gossip(ctx, msg_id).await?;
470
471 let webxdc = Message::load_from_db(ctx, msg_id).await?;
472 let mut msg = Message::new(Viewtype::Text);
473 msg.hidden = true;
474 msg.param.set_cmd(SystemMessage::IrohNodeAddr);
475 msg.in_reply_to = Some(webxdc.rfc724_mid.clone());
476 send_msg(ctx, webxdc.chat_id, &mut msg).await?;
477 info!(ctx, "IROH_REALTIME: Sent realtime advertisement");
478 Ok(conn)
479}
480
481pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u8>) -> Result<()> {
483 if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
484 return Ok(());
485 }
486
487 let iroh = ctx.get_or_try_init_peer_channel().await?;
488 iroh.send_webxdc_realtime_data(ctx, msg_id, data).await?;
489 Ok(())
490}
491
492pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
498 let Some(iroh) = ctx.get_peer_channels().await else {
499 return Ok(());
500 };
501 let Some(topic) = get_iroh_topic_for_msg(ctx, msg_id).await? else {
502 return Ok(());
503 };
504 iroh.leave_realtime(topic).await?;
505 info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}");
506
507 Ok(())
508}
509
510fn create_random_topic() -> TopicId {
512 TopicId::from_bytes(rand::random())
513}
514
515pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result<String> {
518 let topic = create_random_topic();
519 insert_topic_stub(ctx, msg_id, topic).await?;
520 let topic_string = BASE32_NOPAD.encode(topic.as_bytes()).to_ascii_lowercase();
521 Ok(topic_string)
522}
523
524pub(crate) fn iroh_topic_from_str(topic: &str) -> Result<TopicId> {
526 let mut topic_raw = [0u8; 32];
527 BASE32_NOPAD
528 .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
529 .map_err(|e| e.error)
530 .context("Wrong gossip topic header")?;
531
532 let topic = TopicId::from_bytes(topic_raw);
533 Ok(topic)
534}
535
536#[expect(clippy::arithmetic_side_effects)]
537async fn subscribe_loop(
538 context: &Context,
539 mut stream: iroh_gossip::net::GossipReceiver,
540 topic: TopicId,
541 msg_id: MsgId,
542 join_tx: oneshot::Sender<()>,
543) -> Result<()> {
544 let mut join_tx = Some(join_tx);
545
546 while let Some(event) = stream.try_next().await? {
547 match event {
548 Event::Gossip(event) => match event {
549 GossipEvent::Joined(nodes) => {
550 if let Some(join_tx) = join_tx.take() {
551 join_tx.send(()).ok();
554 }
555
556 for node in nodes {
557 iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
558 }
559 }
560 GossipEvent::NeighborUp(node) => {
561 info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
562 iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
563 }
564 GossipEvent::NeighborDown(_node) => {}
565 GossipEvent::Received(message) => {
566 info!(context, "IROH_REALTIME: Received realtime data");
567 context.emit_event(EventType::WebxdcRealtimeData {
568 msg_id,
569 data: message
570 .content
571 .get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
572 .context("too few bytes in iroh message")?
573 .into(),
574 });
575 }
576 },
577 Event::Lagged => {
578 warn!(context, "Gossip lost some messages");
579 }
580 };
581 }
582 Ok(())
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use crate::{
589 EventType,
590 chat::{self, ChatId, add_contact_to_chat, resend_msgs, send_msg},
591 message::{Message, Viewtype},
592 receive_imf::receive_imf,
593 test_utils::{TestContext, TestContextManager},
594 };
595
596 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
597 async fn test_can_communicate() {
598 let mut tcm = TestContextManager::new();
599 let alice = &mut tcm.alice().await;
600 let bob = &mut tcm.bob().await;
601
602 let alice_chat = alice.create_chat(bob).await;
604 let mut instance = Message::new(Viewtype::File);
605 instance
606 .set_file_from_bytes(
607 alice,
608 "minimal.xdc",
609 include_bytes!("../test-data/webxdc/minimal.xdc"),
610 None,
611 )
612 .unwrap();
613
614 send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
615 let alice_webxdc = alice.get_last_msg().await;
616 assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
617
618 let webxdc = alice.pop_sent_msg().await;
619 let bob_webxdc = bob.recv_msg(&webxdc).await;
620 assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
621
622 bob_webxdc.chat_id.accept(bob).await.unwrap();
623
624 send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
626 .await
627 .unwrap();
628
629 bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
630 loop {
631 let event = bob.evtracker.recv().await.unwrap();
632 if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
633 assert!(msg_id == bob_webxdc.id);
634 break;
635 }
636 }
637
638 let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
640 .await
641 .unwrap()
642 .into_iter()
643 .map(|addr| addr.node_id)
644 .collect::<Vec<_>>();
645
646 assert_eq!(
647 members,
648 vec![
649 alice
650 .get_or_try_init_peer_channel()
651 .await
652 .unwrap()
653 .get_node_addr()
654 .await
655 .unwrap()
656 .node_id
657 ]
658 );
659
660 bob.get_or_try_init_peer_channel()
661 .await
662 .unwrap()
663 .join_and_subscribe_gossip(bob, bob_webxdc.id)
664 .await
665 .unwrap()
666 .unwrap()
667 .await
668 .unwrap();
669
670 alice
672 .get_or_try_init_peer_channel()
673 .await
674 .unwrap()
675 .send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
676 .await
677 .unwrap();
678
679 loop {
680 let event = bob.evtracker.recv().await.unwrap();
681 if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
682 if data == "alice -> bob".as_bytes() {
683 break;
684 } else {
685 panic!(
686 "Unexpected status update: {}",
687 String::from_utf8_lossy(&data)
688 );
689 }
690 }
691 }
692 bob.get_or_try_init_peer_channel()
694 .await
695 .unwrap()
696 .send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
697 .await
698 .unwrap();
699
700 loop {
701 let event = alice.evtracker.recv().await.unwrap();
702 if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
703 if data == "bob -> alice".as_bytes() {
704 break;
705 } else {
706 panic!(
707 "Unexpected status update: {}",
708 String::from_utf8_lossy(&data)
709 );
710 }
711 }
712 }
713
714 let members = get_iroh_gossip_peers(alice, alice_webxdc.id)
716 .await
717 .unwrap()
718 .into_iter()
719 .map(|addr| addr.node_id)
720 .collect::<Vec<_>>();
721
722 assert_eq!(
723 members,
724 vec![
725 bob.get_or_try_init_peer_channel()
726 .await
727 .unwrap()
728 .get_node_addr()
729 .await
730 .unwrap()
731 .node_id
732 ]
733 );
734
735 bob.get_or_try_init_peer_channel()
736 .await
737 .unwrap()
738 .send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
739 .await
740 .unwrap();
741
742 loop {
743 let event = alice.evtracker.recv().await.unwrap();
744 if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
745 if data == "bob -> alice 2".as_bytes() {
746 break;
747 } else {
748 panic!(
749 "Unexpected status update: {}",
750 String::from_utf8_lossy(&data)
751 );
752 }
753 }
754 }
755
756 assert!(alice.iroh.read().await.is_some());
759 alice.stop_io().await;
760 assert!(alice.iroh.read().await.is_none());
761 }
762
763 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
764 async fn test_duplicated_out_of_order_advertisement() -> Result<()> {
765 let mut tcm = TestContextManager::new();
766 let alice = &mut tcm.alice().await;
767 let bob = &mut tcm.bob().await;
768
769 let alice_chat = alice.create_chat(bob).await;
770 let mut instance = Message::new(Viewtype::File);
771 instance.set_file_from_bytes(
772 alice,
773 "minimal.xdc",
774 include_bytes!("../test-data/webxdc/minimal.xdc"),
775 None,
776 )?;
777
778 send_msg(alice, alice_chat.id, &mut instance).await?;
779 let alice_webxdc = alice.get_last_msg().await;
780 assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
781
782 let webxdc = alice.pop_sent_msg().await;
783 send_webxdc_realtime_advertisement(alice, alice_webxdc.id).await?;
785 let advertisement = alice.pop_sent_msg().await;
786
787 receive_imf(bob, advertisement.payload().as_bytes(), false).await?;
789
790 let bob_webxdc = bob.recv_msg(&webxdc).await;
791 assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
792
793 bob_webxdc.chat_id.accept(bob).await?;
794
795 bob.recv_msg_trash(&advertisement).await;
796 loop {
797 let event = bob.evtracker.recv().await.unwrap();
798 if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
799 assert!(msg_id == bob_webxdc.id);
800 break;
801 }
802 }
803 let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
804 .await?
805 .into_iter()
806 .map(|addr| addr.node_id)
807 .collect::<Vec<_>>();
808 assert_eq!(
809 members,
810 vec![
811 alice
812 .get_or_try_init_peer_channel()
813 .await
814 .unwrap()
815 .get_node_addr()
816 .await
817 .unwrap()
818 .node_id
819 ]
820 );
821 Ok(())
822 }
823
824 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
825 async fn test_can_reconnect() {
826 let mut tcm = TestContextManager::new();
827 let alice = &mut tcm.alice().await;
828 let bob = &mut tcm.bob().await;
829
830 assert!(
831 alice
832 .get_config_bool(Config::WebxdcRealtimeEnabled)
833 .await
834 .unwrap()
835 );
836 let alice_chat = alice.create_chat(bob).await;
838 let mut instance = Message::new(Viewtype::File);
839 instance
840 .set_file_from_bytes(
841 alice,
842 "minimal.xdc",
843 include_bytes!("../test-data/webxdc/minimal.xdc"),
844 None,
845 )
846 .unwrap();
847
848 send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
849 let alice_webxdc = alice.get_last_msg().await;
850 assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
851
852 let webxdc = alice.pop_sent_msg().await;
853 let bob_webxdc = bob.recv_msg(&webxdc).await;
854 assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
855
856 bob_webxdc.chat_id.accept(bob).await.unwrap();
857
858 send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
860 .await
861 .unwrap();
862
863 bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
864
865 let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
867 .await
868 .unwrap()
869 .into_iter()
870 .map(|addr| addr.node_id)
871 .collect::<Vec<_>>();
872
873 assert_eq!(
874 members,
875 vec![
876 alice
877 .get_or_try_init_peer_channel()
878 .await
879 .unwrap()
880 .get_node_addr()
881 .await
882 .unwrap()
883 .node_id
884 ]
885 );
886
887 bob.get_or_try_init_peer_channel()
888 .await
889 .unwrap()
890 .join_and_subscribe_gossip(bob, bob_webxdc.id)
891 .await
892 .unwrap()
893 .unwrap()
894 .await
895 .unwrap();
896
897 alice
899 .get_or_try_init_peer_channel()
900 .await
901 .unwrap()
902 .send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
903 .await
904 .unwrap();
905
906 loop {
907 let event = bob.evtracker.recv().await.unwrap();
908 if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
909 if data == "alice -> bob".as_bytes() {
910 break;
911 } else {
912 panic!(
913 "Unexpected status update: {}",
914 String::from_utf8_lossy(&data)
915 );
916 }
917 }
918 }
919
920 let bob_topic = get_iroh_topic_for_msg(bob, bob_webxdc.id)
921 .await
922 .unwrap()
923 .unwrap();
924 let bob_sequence_number = bob
925 .iroh
926 .read()
927 .await
928 .as_ref()
929 .unwrap()
930 .sequence_numbers
931 .lock()
932 .get(&bob_topic)
933 .copied();
934 leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
935 let bob_sequence_number_after = bob
936 .iroh
937 .read()
938 .await
939 .as_ref()
940 .unwrap()
941 .sequence_numbers
942 .lock()
943 .get(&bob_topic)
944 .copied();
945 assert_eq!(bob_sequence_number, bob_sequence_number_after);
947
948 bob.get_or_try_init_peer_channel()
949 .await
950 .unwrap()
951 .join_and_subscribe_gossip(bob, bob_webxdc.id)
952 .await
953 .unwrap()
954 .unwrap()
955 .await
956 .unwrap();
957
958 bob.get_or_try_init_peer_channel()
959 .await
960 .unwrap()
961 .send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
962 .await
963 .unwrap();
964
965 loop {
966 let event = alice.evtracker.recv().await.unwrap();
967 if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
968 if data == "bob -> alice".as_bytes() {
969 break;
970 } else {
971 panic!(
972 "Unexpected status update: {}",
973 String::from_utf8_lossy(&data)
974 );
975 }
976 }
977 }
978
979 assert_eq!(
983 alice
984 .iroh
985 .read()
986 .await
987 .as_ref()
988 .unwrap()
989 .iroh_channels
990 .read()
991 .await
992 .len(),
993 1
994 );
995 leave_webxdc_realtime(alice, alice_webxdc.id).await.unwrap();
996 let topic = get_iroh_topic_for_msg(alice, alice_webxdc.id)
997 .await
998 .unwrap()
999 .unwrap();
1000 assert!(
1001 alice
1002 .iroh
1003 .read()
1004 .await
1005 .as_ref()
1006 .unwrap()
1007 .iroh_channels
1008 .read()
1009 .await
1010 .get(&topic)
1011 .is_none()
1012 );
1013 }
1014
1015 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1016 async fn test_parallel_connect() {
1017 let mut tcm = TestContextManager::new();
1018 let alice = &mut tcm.alice().await;
1019 let bob = &mut tcm.bob().await;
1020
1021 let chat = alice.create_chat(bob).await.id;
1022
1023 let mut instance = Message::new(Viewtype::File);
1024 instance
1025 .set_file_from_bytes(
1026 alice,
1027 "minimal.xdc",
1028 include_bytes!("../test-data/webxdc/minimal.xdc"),
1029 None,
1030 )
1031 .unwrap();
1032 connect_alice_bob(alice, chat, &mut instance, bob).await
1033 }
1034
1035 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1036 async fn test_webxdc_resend() {
1037 let mut tcm = TestContextManager::new();
1038 let alice = &mut tcm.alice().await;
1039 let bob = &mut tcm.bob().await;
1040 let group = chat::create_group(alice, "group chat").await.unwrap();
1041
1042 let mut instance = Message::new(Viewtype::File);
1044 instance
1045 .set_file_from_bytes(
1046 alice,
1047 "minimal.xdc",
1048 include_bytes!("../test-data/webxdc/minimal.xdc"),
1049 None,
1050 )
1051 .unwrap();
1052
1053 add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(bob).await)
1054 .await
1055 .unwrap();
1056
1057 connect_alice_bob(alice, group, &mut instance, bob).await;
1058
1059 let fiona = &mut tcm.fiona().await;
1061
1062 add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(fiona).await)
1063 .await
1064 .unwrap();
1065
1066 resend_msgs(alice, &[instance.id]).await.unwrap();
1067 let msg = alice.pop_sent_msg().await;
1068 let fiona_instance = fiona.recv_msg(&msg).await;
1069 fiona_instance.chat_id.accept(fiona).await.unwrap();
1070 assert!(fiona.ctx.iroh.read().await.is_none());
1071
1072 let fiona_connect_future = send_webxdc_realtime_advertisement(fiona, fiona_instance.id)
1073 .await
1074 .unwrap()
1075 .unwrap();
1076 let fiona_advert = fiona.pop_sent_msg().await;
1077 alice.recv_msg_trash(&fiona_advert).await;
1078
1079 fiona_connect_future.await.unwrap();
1080
1081 let realtime_send_loop = async {
1082 loop {
1085 send_webxdc_realtime_data(alice, instance.id, b"alice -> bob & fiona".into())
1086 .await
1087 .unwrap();
1088 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
1089 }
1090 };
1091
1092 let realtime_receive_loop = async {
1093 loop {
1094 let event = fiona.evtracker.recv().await.unwrap();
1095 if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
1096 if data == b"alice -> bob & fiona" {
1097 break;
1098 } else {
1099 panic!(
1100 "Unexpected status update: {}",
1101 String::from_utf8_lossy(&data)
1102 );
1103 }
1104 }
1105 }
1106 };
1107 tokio::select!(
1108 _ = realtime_send_loop => {
1109 panic!("Send loop should never finish");
1110 },
1111 _ = realtime_receive_loop => {
1112 return;
1113 }
1114 );
1115 }
1116
1117 async fn connect_alice_bob(
1118 alice: &mut TestContext,
1119 alice_chat_id: ChatId,
1120 instance: &mut Message,
1121 bob: &mut TestContext,
1122 ) {
1123 send_msg(alice, alice_chat_id, instance).await.unwrap();
1124 let alice_webxdc = alice.get_last_msg().await;
1125
1126 let webxdc = alice.pop_sent_msg().await;
1127 let bob_webxdc = bob.recv_msg(&webxdc).await;
1128 assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
1129
1130 bob_webxdc.chat_id.accept(bob).await.unwrap();
1131
1132 eprintln!("Sending advertisements");
1133 let alice_advertisement_future = send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
1135 .await
1136 .unwrap()
1137 .unwrap();
1138 let alice_advertisement = alice.pop_sent_msg().await;
1139
1140 let bob_advertisement_future = send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
1141 .await
1142 .unwrap()
1143 .unwrap();
1144 let bob_advertisement = bob.pop_sent_msg().await;
1145
1146 eprintln!("Receiving advertisements");
1147 bob.recv_msg_trash(&alice_advertisement).await;
1148 alice.recv_msg_trash(&bob_advertisement).await;
1149
1150 eprintln!("Alice and Bob wait for connection");
1151 alice_advertisement_future.await.unwrap();
1152 bob_advertisement_future.await.unwrap();
1153
1154 eprintln!("Sending ephemeral message");
1156 send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".into())
1157 .await
1158 .unwrap();
1159
1160 eprintln!("Waiting for ephemeral message");
1161 loop {
1162 let event = bob.evtracker.recv().await.unwrap();
1163 if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
1164 if data == b"alice -> bob" {
1165 break;
1166 } else {
1167 panic!(
1168 "Unexpected status update: {}",
1169 String::from_utf8_lossy(&data)
1170 );
1171 }
1172 }
1173 }
1174 }
1175
1176 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1177 async fn test_peer_channels_disabled() {
1178 let mut tcm = TestContextManager::new();
1179 let alice = &mut tcm.alice().await;
1180
1181 alice
1182 .set_config_bool(Config::WebxdcRealtimeEnabled, false)
1183 .await
1184 .unwrap();
1185
1186 send_webxdc_realtime_advertisement(alice, MsgId::new(1))
1188 .await
1189 .unwrap();
1190
1191 assert!(alice.ctx.iroh.read().await.is_none());
1192
1193 send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
1195 .await
1196 .unwrap();
1197
1198 assert!(alice.ctx.iroh.read().await.is_none());
1199
1200 leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
1201
1202 assert!(alice.ctx.iroh.read().await.is_none());
1203
1204 assert!(alice.ctx.get_or_try_init_peer_channel().await.is_err());
1207 }
1208
1209 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1210 async fn test_leave_webxdc_realtime_uninitialized() {
1211 let mut tcm = TestContextManager::new();
1212 let alice = &mut tcm.alice().await;
1213
1214 alice
1215 .set_config_bool(Config::WebxdcRealtimeEnabled, true)
1216 .await
1217 .unwrap();
1218
1219 assert!(alice.ctx.iroh.read().await.is_none());
1220 leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
1221 assert!(alice.ctx.iroh.read().await.is_none());
1222 }
1223}