Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions src/app/docs/examples/gossip-chat/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// 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<Self> {
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<u8> {
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<String>) -> 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<NodeAddr>,
}

impl Ticket {
/// Deserialize from a slice of bytes to a Ticket.
fn from_bytes(bytes: &[u8]) -> Result<Self> {
serde_json::from_slice(bytes).map_err(Into::into)
}

/// Serialize from a `Ticket` to a `Vec` of bytes.
pub fn to_bytes(&self) -> Vec<u8> {
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<Self, Self::Err> {
let bytes = data_encoding::BASE32_NOPAD.decode(s.to_ascii_uppercase().as_bytes())?;
Self::from_bytes(&bytes)
}
}
```

## Running the Application

```bash
Expand Down
12 changes: 11 additions & 1 deletion src/app/docs/tour/2-relays/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 8 additions & 8 deletions src/components/Code.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -150,7 +150,7 @@ function CodeGroupHeader({title, children, selectedIndex}) {
</h3>
)}
{hasTabs && (
<Tab.List className="-mb-px flex gap-4 text-xs font-medium">
<TabList className="-mb-px flex gap-4 text-xs font-medium">
{Children.map(children, (child, childIndex) => (
<Tab
className={clsx(
Expand All @@ -163,7 +163,7 @@ function CodeGroupHeader({title, children, selectedIndex}) {
{getPanelTitle(child.props)}
</Tab>
))}
</Tab.List>
</TabList>
)}
</div>
);
Expand All @@ -174,17 +174,17 @@ function CodeGroupPanels({children, ...props}) {

if (hasTabs) {
return (
<Tab.Panels>
<TabPanels>
{Children.map(children, (child) => (
<Tab.Panel>
<TabPanel>
<CodePanel {...props}>{child}</CodePanel>
</Tab.Panel>
</TabPanel>
))}
</Tab.Panels>
</TabPanels>
);
}

return <CodePanel {...props}>{children}</CodePanel>;
return <>{Children.map(children, child => <CodePanel {...props}>{child}</CodePanel>)}</>
}

function usePreventLayoutShift() {
Expand Down
4 changes: 2 additions & 2 deletions typography.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
},

Expand Down
Loading