From 22c1a940169dfd2004fdedfae9216837d8e67042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 18 Aug 2025 14:14:33 +0200 Subject: [PATCH 1/2] fix: Fix code block rendering in tw4 and put removed blocks back --- src/app/docs/examples/gossip-chat/page.mdx | 245 +++++++++++++++++++++ src/app/docs/tour/2-relays/page.mdx | 12 +- src/components/Code.jsx | 10 +- typography.js | 4 +- 4 files changed, 263 insertions(+), 8 deletions(-) diff --git a/src/app/docs/examples/gossip-chat/page.mdx b/src/app/docs/examples/gossip-chat/page.mdx index 09d95e94..cb8c7163 100644 --- a/src/app/docs/examples/gossip-chat/page.mdx +++ b/src/app/docs/examples/gossip-chat/page.mdx @@ -528,6 +528,251 @@ In either case, we still print a ticket to the terminal. The smallest change, but a very important one, is that we go from using the `subscribe` method to the `subscribe_and_join` method. The `subscribe` method would return a `Topic` immediately. The `subscribe_and_join` method takes the given topic, joins it, and waits for someone else to join the topic before returning. +```rust +use std::{collections::HashMap, fmt, str::FromStr}; + +use anyhow::Result; +use clap::Parser; +use futures_lite::StreamExt; +use iroh::{protocol::Router, Endpoint, NodeAddr, NodeId}; +use iroh_gossip::{ + net::{Event, Gossip, GossipEvent, GossipReceiver}, + proto::TopicId, +}; +use serde::{Deserialize, Serialize}; + +/// Chat over iroh-gossip +/// +/// This broadcasts unsigned messages over iroh-gossip. +/// +/// By default a new node id is created when starting the example. +/// +/// By default, we use the default n0 discovery services to dial by `NodeId`. +#[derive(Parser, Debug)] +struct Args { + /// Set your nickname. + #[clap(short, long)] + name: Option, + /// Set the bind port for our socket. By default, a random port will be used. + #[clap(short, long, default_value = "0")] + bind_port: u16, + #[clap(subcommand)] + command: Command, +} + +#[derive(Parser, Debug)] +enum Command { + /// Open a chat room for a topic and print a ticket for others to join. + Open, + /// Join a chat room from a ticket. + Join { + /// The ticket, as base32 string. + ticket: String, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + // parse the cli command + let (topic, nodes) = match &args.command { + Command::Open => { + let topic = TopicId::from_bytes(rand::random()); + println!("> opening chat room for topic {topic}"); + (topic, vec![]) + } + Command::Join { ticket } => { + let Ticket { topic, nodes } = Ticket::from_str(ticket)?; + println!("> joining chat room for topic {topic}"); + (topic, nodes) + } + }; + + let endpoint = Endpoint::builder().discovery_n0().bind().await?; + + println!("> our node id: {}", endpoint.node_id()); + let gossip = Gossip::builder().spawn(endpoint.clone()).await?; + + let router = Router::builder(endpoint.clone()) + .accept(iroh_gossip::ALPN, gossip.clone()) + .spawn(); + + // in our main file, after we create a topic `id`: + // print a ticket that includes our own node id and endpoint addresses + let ticket = { + // Get our address information, includes our + // `NodeId`, our `RelayUrl`, and any direct + // addresses. + let me = endpoint.node_addr().await?; + let nodes = vec![me]; + Ticket { topic, nodes } + }; + println!("> ticket to join us: {ticket}"); + + // join the gossip topic by connecting to known nodes, if any + let node_ids = nodes.iter().map(|p| p.node_id).collect(); + if nodes.is_empty() { + println!("> waiting for nodes to join us..."); + } else { + println!("> trying to connect to {} nodes...", nodes.len()); + // add the peer addrs from the ticket to our endpoint's addressbook so that they can be dialed + for node in nodes.into_iter() { + endpoint.add_node_addr(node)?; + } + }; + let (sender, receiver) = gossip.subscribe_and_join(topic, node_ids).await?.split(); + println!("> connected!"); + + // broadcast our name, if set + if let Some(name) = args.name { + let message = Message::new(MessageBody::AboutMe { + from: endpoint.node_id(), + name, + }); + sender.broadcast(message.to_vec().into()).await?; + } + + // subscribe and print loop + tokio::spawn(subscribe_loop(receiver)); + + // spawn an input thread that reads stdin + // create a multi-provider, single-consumer channel + let (line_tx, mut line_rx) = tokio::sync::mpsc::channel(1); + // and pass the `sender` portion to the `input_loop` + std::thread::spawn(move || input_loop(line_tx)); + + // broadcast each line we type + println!("> type a message and hit enter to broadcast..."); + // listen for lines that we have typed to be sent from `stdin` + while let Some(text) = line_rx.recv().await { + // create a message from the text + let message = Message::new(MessageBody::Message { + from: endpoint.node_id(), + text: text.clone(), + }); + // broadcast the encoded message + sender.broadcast(message.to_vec().into()).await?; + // print to ourselves the text that we sent + println!("> sent: {text}"); + } + router.shutdown().await?; + + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize)] +struct Message { + body: MessageBody, + nonce: [u8; 16], +} + +#[derive(Debug, Serialize, Deserialize)] +enum MessageBody { + AboutMe { from: NodeId, name: String }, + Message { from: NodeId, text: String }, +} + +impl Message { + fn from_bytes(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(Into::into) + } + + pub fn new(body: MessageBody) -> Self { + Self { + body, + nonce: rand::random(), + } + } + + pub fn to_vec(&self) -> Vec { + serde_json::to_vec(self).expect("serde_json::to_vec is infallible") + } +} + +// Handle incoming events +async fn subscribe_loop(mut receiver: GossipReceiver) -> Result<()> { + // keep track of the mapping between `NodeId`s and names + let mut names = HashMap::new(); + // iterate over all events + while let Some(event) = receiver.try_next().await? { + // if the Event is a `GossipEvent::Received`, let's deserialize the message: + if let Event::Gossip(GossipEvent::Received(msg)) = event { + // deserialize the message and match on the + // message type: + match Message::from_bytes(&msg.content)?.body { + MessageBody::AboutMe { from, name } => { + // if it's an `AboutMe` message + // add an entry into the map + // and print the name + names.insert(from, name.clone()); + println!("> {} is now known as {}", from.fmt_short(), name); + } + MessageBody::Message { from, text } => { + // if it's a `Message` message, + // get the name from the map + // and print the message + let name = names + .get(&from) + .map_or_else(|| from.fmt_short(), String::to_string); + println!("{}: {}", name, text); + } + } + } + } + Ok(()) +} + +fn input_loop(line_tx: tokio::sync::mpsc::Sender) -> Result<()> { + let mut buffer = String::new(); + let stdin = std::io::stdin(); // We get `Stdin` here. + loop { + stdin.read_line(&mut buffer)?; + line_tx.blocking_send(buffer.clone())?; + buffer.clear(); + } +} + +// add the `Ticket` code to the bottom of the main file +#[derive(Debug, Serialize, Deserialize)] +struct Ticket { + topic: TopicId, + nodes: Vec, +} + +impl Ticket { + /// Deserialize from a slice of bytes to a Ticket. + fn from_bytes(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(Into::into) + } + + /// Serialize from a `Ticket` to a `Vec` of bytes. + pub fn to_bytes(&self) -> Vec { + serde_json::to_vec(self).expect("serde_json::to_vec is infallible") + } +} + +// The `Display` trait allows us to use the `to_string` +// method on `Ticket`. +impl fmt::Display for Ticket { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut text = data_encoding::BASE32_NOPAD.encode(&self.to_bytes()[..]); + text.make_ascii_lowercase(); + write!(f, "{}", text) + } +} + +// The `FromStr` trait allows us to turn a `str` into +// a `Ticket` +impl FromStr for Ticket { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + let bytes = data_encoding::BASE32_NOPAD.decode(s.to_ascii_uppercase().as_bytes())?; + Self::from_bytes(&bytes) + } +} +``` + ## Running the Application ```bash diff --git a/src/app/docs/tour/2-relays/page.mdx b/src/app/docs/tour/2-relays/page.mdx index 465c70e0..cb78c1de 100644 --- a/src/app/docs/tour/2-relays/page.mdx +++ b/src/app/docs/tour/2-relays/page.mdx @@ -13,7 +13,17 @@ Keep in mind, connections are end-2-end encrypted, which means relays can’t re Coming back to our program, let’s add support for relays: -``` +```rust +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let builder = iroh::Endpoint::builder() + .relay_mode(iroh::RelayMode::Default); + + let endpoint = builder.bind().await?; + println!("node id: {:?}", endpoint.node_id()); + + Ok(()) +} ``` Here we've set the relay mode to `Default`, but this hasn't actually changed anything. Our prior code had `relay_mode` implicitly set to `Default`, and this works because iroh comes with a set of free-to-use public relays by default, run by the number 0 team. You’re more than welcome to run your own relays, use the number 0 hosted solution [n0des.iroh.computer](https://n0des.iroh.computer), run your own, or, ideally all of the above! The code for relay servers is in the main iroh repo, and we release compiled binaries for relays on each release of iroh. diff --git a/src/components/Code.jsx b/src/components/Code.jsx index 1fce75ce..84bc0497 100644 --- a/src/components/Code.jsx +++ b/src/components/Code.jsx @@ -174,17 +174,17 @@ function CodeGroupPanels({children, ...props}) { if (hasTabs) { return ( - + {Children.map(children, (child) => ( - + {child} - + ))} - + ); } - return {children}; + return <>{Children.map(children, child => {child})} } function usePreventLayoutShift() { diff --git a/typography.js b/typography.js index ca2ed697..fb0c2ba5 100644 --- a/typography.js +++ b/typography.js @@ -222,8 +222,8 @@ module.exports = ({ theme }) => ({ }, figcaption: { color: 'var(--tw-prose-captions)', - fontSize: theme('fontSize.xs')[0], - ...theme('fontSize.xs')[1], + fontSize: theme('fontSize.sm')[0], + ...theme('fontSize.sm')[1], marginTop: theme('spacing.2'), }, From 81f27cca0c69b36e0b45aeb6c62277d80dd8e2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 18 Aug 2025 14:17:29 +0200 Subject: [PATCH 2/2] Fix imports --- src/components/Code.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Code.jsx b/src/components/Code.jsx index 84bc0497..6cf5f60a 100644 --- a/src/components/Code.jsx +++ b/src/components/Code.jsx @@ -8,7 +8,7 @@ import { useRef, useState, } from 'react'; -import {Tab} from '@headlessui/react'; +import {Tab, TabList, TabPanel, TabPanels} from '@headlessui/react'; import clsx from 'clsx'; import {create} from 'zustand'; @@ -150,7 +150,7 @@ function CodeGroupHeader({title, children, selectedIndex}) { )} {hasTabs && ( - + {Children.map(children, (child, childIndex) => ( ))} - + )} );