Current section

Files

Jump to
iroh_ex native iroh_ex src main.rs.txt
Raw

native/iroh_ex/src/main.rs.txt

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()
.await?;
// 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::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::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)]
enum Message {
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 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)? {
Message::AboutMe { from, name } => {
// if it's an `AboutMe` message
// add and entry into the map
// and print the name
names.insert(from, name.clone());
println!("> {} is now known as {}", from.fmt_short(), name);
}
Message::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)
}
}