Skip to content

Commit 9fece9d

Browse files
committed
feat: implement interactive/echo modes, implement release automation
1 parent bc56f6a commit 9fece9d

File tree

4 files changed

+214
-22
lines changed

4 files changed

+214
-22
lines changed
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Build and release chat example
2+
on:
3+
push:
4+
branches:
5+
- master
6+
- feat/chat-example
7+
permissions: write-all
8+
jobs:
9+
metadata:
10+
name: Get release metadata
11+
runs-on: ubuntu-latest
12+
outputs:
13+
version: ${{ steps.get_version.outputs.version }}
14+
release_exists: ${{ steps.check_release.outputs.exists }}
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Get version
20+
id: get_version
21+
run: echo "version=chat-example-$(cargo read-manifest --manifest-path examples/chat/Cargo.toml | jq -r '.version')" >> $GITHUB_OUTPUT
22+
23+
- name: Check if release exists
24+
id: check_release
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
run: |
28+
RELEASE_URL=$(curl --silent "https://api.github.com/repos/calimero-network/relay-server/releases/tags/${{ steps.get_version.outputs.version }}" \
29+
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
30+
-H "Accept: application/vnd.github.v3+json" | jq -r '.url')
31+
if [[ "$RELEASE_URL" != "null" ]]; then
32+
echo "exists=true" >> $GITHUB_OUTPUT
33+
else
34+
echo "exists=false" >> $GITHUB_OUTPUT
35+
fi
36+
37+
release:
38+
name: Build and release
39+
runs-on: ubuntu-latest
40+
needs: metadata
41+
if: needs.metadata.outputs.release_exists == 'false'
42+
steps:
43+
- name: Checkout code
44+
uses: actions/checkout@v4
45+
46+
- name: Setup rust toolchain
47+
run: rustup toolchain install stable --profile minimal
48+
49+
- name: Setup rust cache
50+
uses: Swatinem/rust-cache@v2
51+
52+
- name: Build for Intel Linux
53+
run: cargo build -p chat-example --release --target=x86_64-unknown-linux-gnu
54+
55+
- name: Build for Aarch Linux
56+
run: cross build -p chat-example --release --target=aarch64-unknown-linux-gnu
57+
58+
- name: Create artifacts directory
59+
run: |
60+
mkdir -p artifacts
61+
cp target/x86_64-unknown-linux-gnu/release/chat-example artifacts/chat-example-x86_64-unknown-linux
62+
cp target/aarch64-unknown-linux-gnu/release/chat-example artifacts/chat-example-aarch64-unknown-linux
63+
64+
- name: Create GitHub Release
65+
uses: softprops/action-gh-release@v2
66+
with:
67+
tag_name: ${{ needs.metadata.outputs.version }}
68+
files: |
69+
examples/chat/README.md
70+
artifacts/*
71+
env:
72+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

examples/chat/README.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Chat
2+
This examples show cases how to manually dial (connect to) either local peer or remote peer has a reservation on relay-server.
3+
4+
## Run local only
5+
This examples shows how to run two sessions locally and connect sessions by manually dialing local peer.
6+
7+
Run first chat session in echo mode.
8+
```
9+
cargo run -p chat-example -- --mode echo --port 4002 --secret-key-seed 102 --gossip-topic-names calimero-network/examples/chat/v0.0.1 --relay-address /ip4/3.71.239.80/udp/4001/quic-v1/p2p/12D3KooWAgFah4EZtWnMMGMUddGdJpb5cq2NubNCAD2jA5AZgbXF
10+
```
11+
12+
Run second chat session in interactive mode with local peer dial.
13+
```
14+
cargo run -p chat-example -- --mode interactive --port 4003 --secret-key-seed 103 --gossip-topic-names calimero-network/examples/chat/v0.0.1 --dial-peer-addrs /ip4/127.0.0.1/udp/4002/quic-v1/p2p/12D3KooWMpeKAbMK4BTPsQY3rG7XwtdstseHGcq7kffY8LToYYKK --relay-address /ip4/3.71.239.80/udp/4001/quic-v1/p2p/12D3KooWAgFah4EZtWnMMGMUddGdJpb5cq2NubNCAD2jA5AZgbXF
15+
```
16+
17+
In the interactive session publish new message manually:
18+
```
19+
publish calimero-network/examples/chat/v0.0.1 ola
20+
```
21+
22+
## Run locally with remote peer dial in
23+
This examples shows how to run two sessions locally and connect sessions manually by dialing private remote peer from each session. For the gossip message to pass from one local session to second local session it needs to go "the long way" around (local -> remote -> local).
24+
25+
Run first chat session in interactive mode with remote peer dial.
26+
```
27+
```
28+
29+
Run second chat session in interactive mode with remote peer dial.
30+
```
31+
```
32+
33+
In any interactive session publish new message manually:
34+
```
35+
publish calimero-network/examples/chat/v0.0.1 ola
36+
```
37+
38+
## Debugging and known issues
39+
- If multiple people are running the same example, some will fail to get reservation on relay server because the same PeerId already exists.
40+
- Fix: change `secret-key-seed` to something else
41+

examples/chat/src/main.rs

+98-20
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,63 @@ use libp2p::gossipsub;
55
use libp2p::identity;
66
use multiaddr::Multiaddr;
77
use tokio::io::AsyncBufReadExt;
8-
use tracing::info;
8+
use tracing::{error, info};
99
use tracing_subscriber::prelude::*;
1010
use tracing_subscriber::EnvFilter;
1111

1212
mod network;
1313

1414
#[derive(Debug, Parser)]
15-
#[clap(name = "DCUtR client example")]
15+
#[clap(name = "Chat example")]
1616
struct Opt {
17+
/// The mode (interactive, echo).
18+
#[clap(long)]
19+
mode: Mode,
20+
21+
/// The port used to listen on all interfaces
22+
#[clap(long)]
23+
port: u16,
24+
1725
/// Fixed value to generate deterministic peer id.
1826
#[clap(long)]
1927
secret_key_seed: u8,
2028

21-
/// The listening address
29+
/// The listening address of a relay server to connect to.
2230
#[clap(long)]
2331
relay_address: Multiaddr,
32+
33+
/// Optional list of peer addresses to dial immediately after network bootstrap.
34+
#[clap(long)]
35+
dial_peer_addrs: Option<Vec<Multiaddr>>,
36+
37+
/// Optional list of gossip topic names to subscribe immediately after network bootstrap.
38+
#[clap(long)]
39+
gossip_topic_names: Option<Vec<String>>,
40+
}
41+
42+
#[derive(Clone, Debug, PartialEq, Parser)]
43+
enum Mode {
44+
Interactive,
45+
Echo,
46+
}
47+
48+
impl FromStr for Mode {
49+
type Err = String;
50+
fn from_str(mode: &str) -> Result<Self, Self::Err> {
51+
match mode {
52+
"interactive" => Ok(Mode::Interactive),
53+
"echo" => Ok(Mode::Echo),
54+
_ => Err("Expected either 'dial' or 'listen'".to_string()),
55+
}
56+
}
2457
}
2558

2659
#[tokio::main]
2760
async fn main() -> eyre::Result<()> {
2861
tracing_subscriber::registry()
29-
// "info,chat_example::network=debug,{}",
62+
// "info,chat_example=debug,{}",
3063
.with(EnvFilter::builder().parse(format!(
31-
"info,chat_example::network=debug,{}",
64+
"info,{}",
3265
std::env::var("RUST_LOG").unwrap_or_default()
3366
))?)
3467
.with(tracing_subscriber::fmt::layer())
@@ -39,24 +72,46 @@ async fn main() -> eyre::Result<()> {
3972
let keypair = generate_ed25519(opt.secret_key_seed);
4073

4174
let (network_client, mut network_events) =
42-
network::run(keypair, opt.relay_address.clone()).await?;
75+
network::run(keypair, opt.port, opt.relay_address.clone()).await?;
76+
77+
if let Some(peer_addrs) = opt.dial_peer_addrs {
78+
for addr in peer_addrs {
79+
network_client.dial(addr).await?;
80+
}
81+
}
82+
83+
if let Some(topic_names) = opt.gossip_topic_names {
84+
for topic_name in topic_names {
85+
let topic = gossipsub::IdentTopic::new(topic_name);
86+
network_client.subscribe(topic).await?;
87+
}
88+
}
4389

44-
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();
90+
match opt.mode {
91+
Mode::Interactive => {
92+
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();
4593

46-
loop {
47-
tokio::select! {
48-
event = network_events.recv() => {
49-
let Some(event) = event else {
50-
break;
51-
};
52-
handle_event(network_client.clone(), event).await?;
53-
}
54-
line = stdin.next_line() => {
55-
if let Some(line) = line? {
56-
handle_line(network_client.clone(), line).await?;
94+
loop {
95+
tokio::select! {
96+
event = network_events.recv() => {
97+
let Some(event) = event else {
98+
break;
99+
};
100+
handle_network_event(Mode::Interactive, network_client.clone(), event).await?;
101+
}
102+
line = stdin.next_line() => {
103+
if let Some(line) = line? {
104+
handle_line(network_client.clone(), line).await?;
105+
}
106+
}
57107
}
58108
}
59109
}
110+
Mode::Echo => {
111+
while let Some(event) = network_events.recv().await {
112+
handle_network_event(Mode::Echo, network_client.clone(), event).await?;
113+
}
114+
}
60115
}
61116

62117
Ok(())
@@ -69,8 +124,9 @@ fn generate_ed25519(secret_key_seed: u8) -> identity::Keypair {
69124
identity::Keypair::ed25519_from_bytes(bytes).expect("only errors on wrong length")
70125
}
71126

72-
async fn handle_event(
73-
_: network::client::NetworkClient,
127+
async fn handle_network_event(
128+
mode: Mode,
129+
network_client: network::client::NetworkClient,
74130
event: network::types::NetworkEvent,
75131
) -> eyre::Result<()> {
76132
match event {
@@ -86,6 +142,28 @@ async fn handle_event(
86142
peer_id, observed_addr
87143
);
88144
}
145+
network::types::NetworkEvent::Message { id, message } => {
146+
let text = String::from_utf8_lossy(&message.data);
147+
info!("Message from {:?}: {:?}", id, text);
148+
149+
match mode {
150+
Mode::Echo => {
151+
let text = format!("Echo, original: '{}'", text);
152+
153+
match network_client
154+
.publish(message.topic, text.into_bytes())
155+
.await
156+
{
157+
Ok(_) => info!("Echoed message back"),
158+
Err(err) => error!(%err, "Failed to echo message back"),
159+
};
160+
}
161+
_ => {}
162+
}
163+
}
164+
network::types::NetworkEvent::ListeningOn { address, .. } => {
165+
info!("Listening on: {}", address);
166+
}
89167
event => {
90168
info!("Unhandled event: {:?}", event);
91169
}

examples/chat/src/network.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@ struct Behaviour {
2828

2929
pub async fn run(
3030
keypair: identity::Keypair,
31+
port: u16,
3132
relay_address: Multiaddr,
3233
) -> eyre::Result<(NetworkClient, mpsc::Receiver<types::NetworkEvent>)> {
3334
let (client, mut event_receiver, event_loop) = init(keypair).await?;
3435

3536
tokio::spawn(event_loop.run());
3637

3738
let swarm_listen: Vec<Multiaddr> = vec![
38-
"/ip4/0.0.0.0/udp/0/quic-v1".parse()?,
39-
"/ip4/0.0.0.0/tcp/0".parse()?,
39+
format!("/ip4/0.0.0.0/udp/{}/quic-v1", port).parse()?,
40+
format!("/ip4/0.0.0.0/tcp/{}", port).parse()?,
4041
];
4142
for addr in swarm_listen {
4243
client.listen_on(addr.clone()).await?;

0 commit comments

Comments
 (0)