add relay chat example

This commit is contained in:
Sasha 2023-08-30 00:53:31 +02:00
parent f0b204de3d
commit bb06af386b
No known key found for this signature in database
12 changed files with 602 additions and 175 deletions

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>JS-Waku light chat</title>
<link rel="stylesheet" href="./style.css" />
<link rel="apple-touch-icon" href="./favicon.png" />
<link rel="manifest" href="./manifest.json" />
<link rel="icon" href="./favicon.ico" />
</head>
<body>
<div class="content">
<div class="header">
<h3>Status: <span id="status"></span></h3>
<h4><label for="remoteNode">Remote node multiaddr</label></h4>
<div>
<input id="remoteNode" />
<button id="connectRemoteNode">Dial</button>
</div>
<h4><label for="webrtcPeer">WebRTC Peer</label></h4>
<div>
<input id="webrtcPeer" />
<button id="connectWebrtcPeer">Dial</button>
</div>
<div>
<button id="relayWebRTC">Ensure WebRTC Relay connection</button>
<button id="dropNonWebRTC">Drop non WebRTC connections</button>
</div>
<details open>
<summary>Peer's information</summary>
<h4>Content topic</h4>
<p id="contentTopic"></p>
<h4>Local Peer Id</h4>
<p id="localPeerId"></p>
<h4>Remote Peer Id</h4>
<p id="remotePeerId"></p>
<h4>Relay mesh's protocols</h4>
<p id="relayMeshInfo"></p>
</details>
</div>
<div id="messages"></div>
<div class="footer">
<div class="inputArea">
<input type="text" id="nickText" placeholder="Nickname" />
<textarea id="messageText" placeholder="Message"></textarea>
</div>
<div class="controls">
<button id="send">Send</button>
<button id="exit">Exit chat</button>
</div>
</div>
</div>
<script src="//cdn.jsdelivr.net/npm/protobufjs@7.X.X/dist/protobuf.min.js"></script>
<script type="module" src="./index.js"></script>
</body>
</html>

View File

@ -0,0 +1,340 @@
import {
createRelayNode,
bytesToUtf8,
utf8ToBytes,
createDecoder,
createEncoder,
} from "@waku/sdk";
import { webSockets } from "@libp2p/websockets";
import { all as filterAll } from "@libp2p/websockets/filters";
import { webRTC } from "@libp2p/webrtc";
import { circuitRelayTransport } from "libp2p/circuit-relay";
const CONTENT_TOPIC = "/toy-chat/2/huilong/proto";
const ui = initUI();
runApp(ui).catch((err) => {
console.error(err);
ui.setStatus(`error: ${err.message}`, "error");
});
async function runApp(ui) {
const {
info,
sendMessage,
unsubscribeFromMessages,
dial,
dialWebRTCpeer,
dropNetworkConnections,
ensureWebRTCconnectionInRelayMesh,
} = await initWakuContext({
ui,
contentTopic: CONTENT_TOPIC,
});
ui.setLocalPeer(info.localPeerId);
ui.setContentTopic(info.contentTopic);
ui.onSendMessage(sendMessage);
ui.onRemoteNodeConnect(dial);
ui.onWebrtcConnect(dialWebRTCpeer);
ui.onRelayWebRTC(ensureWebRTCconnectionInRelayMesh);
ui.onDropNonWebRTC(dropNetworkConnections);
ui.onExit(async () => {
ui.setStatus("disconnecting...", "progress");
await unsubscribeFromMessages();
ui.setStatus("disconnected", "terminated");
ui.resetMessages();
});
}
async function initWakuContext({ ui, contentTopic }) {
const Decoder = createDecoder(contentTopic);
const Encoder = createEncoder({ contentTopic });
const ChatMessage = new protobuf.Type("ChatMessage")
.add(new protobuf.Field("timestamp", 1, "uint64"))
.add(new protobuf.Field("nick", 2, "string"))
.add(new protobuf.Field("text", 3, "bytes"));
ui.setStatus("starting...", "progress");
const node = await createRelayNode({
libp2p: {
addresses: {
listen: ["/webrtc"],
},
connectionGater: {
denyDialMultiaddr: () => {
// refuse to deny localhost addresses
return false;
},
},
transports: [
webRTC({}),
circuitRelayTransport({
discoverRelays: 1,
}),
webSockets({ filter: filterAll }),
],
},
});
await node.start();
// Set a filter by using Decoder for a given ContentTopic
const unsubscribeFromMessages = await node.relay.subscribe(
[Decoder],
(wakuMessage) => {
const messageObj = ChatMessage.decode(wakuMessage.payload);
ui.renderMessage({
...messageObj,
text: bytesToUtf8(messageObj.text),
});
}
);
ui.setStatus("started", "success");
const localPeerId = node.libp2p.peerId.toString();
const remotePeers = await node.libp2p.peerStore.all();
const remotePeerIds = new Set(remotePeers.map((peer) => peer.id.toString()));
ui.setRemotePeer(Array.from(remotePeerIds.keys()));
node.libp2p.addEventListener("peer:connect", async (event) => {
remotePeerIds.add(event.detail.toString());
ui.setRemotePeer(Array.from(remotePeerIds.keys()));
ui.setRelayMeshInfo(node.relay.gossipSub);
});
node.libp2p.addEventListener("peer:disconnect", (event) => {
remotePeerIds.delete(event.detail.toString());
ui.setRemotePeer(Array.from(remotePeerIds.keys()));
ui.setRelayMeshInfo(node.relay.gossipSub);
});
node.libp2p.addEventListener("peer:identify", (event) => {
const peer = event.detail;
if (!peer.protocols.includes("/webrtc-signaling/0.0.1")) {
return;
}
ui.setWebrtcPeer(peer.peerId.toString());
ui.setRelayMeshInfo(node.relay.gossipSub);
});
window.node = node;
return {
unsubscribeFromMessages,
info: {
contentTopic,
localPeerId,
},
sendMessage: async ({ text, nick }) => {
if (!text || !nick) {
return;
}
const protoMessage = ChatMessage.create({
nick,
timestamp: Date.now(),
text: utf8ToBytes(text),
});
await node.relay.send(Encoder, {
payload: ChatMessage.encode(protoMessage).finish(),
});
},
dial: async (multiaddr) => {
ui.setStatus("connecting...", "progress");
await node.dial(multiaddr);
ui.setStatus("connected", "success");
},
dialWebRTCpeer: async (peerId) => {
const peers = await node.libp2p.peerStore.all();
const circuitPeer = peers.filter(
(p) =>
p.protocols.includes("/libp2p/circuit/relay/0.2.0/hop") &&
p.protocols.includes("/libp2p/circuit/relay/0.2.0/stop")
)[0];
if (!circuitPeer) {
throw Error("No Circuit peer is found");
}
let multiaddr = circuitPeer.addresses.pop().multiaddr;
multiaddr = `${multiaddr}/p2p/${circuitPeer.id.toString()}/p2p-circuit/webrtc/p2p/${peerId}`;
await node.dial(multiaddr);
ui.setRelayMeshInfo(node.relay.gossipSub);
},
ensureWebRTCconnectionInRelayMesh: async () => {
const promises = node.libp2p
.getConnections()
.filter((c) => c.stat.multiplexer === "/webrtc")
.map(async (c) => {
const outboundStream = node.relay.gossipSub.streamsOutbound.get(
c.remotePeer.toString()
);
const isWebRTCOutbound =
outboundStream.rawStream.constructor.name === "WebRTCStream";
if (isWebRTCOutbound) {
return;
}
node.relay.gossipSub.streamsOutbound.delete(c.remotePeer.toString());
await node.relay.gossipSub.createOutboundStream(
c.remotePeer.toString(),
c
);
});
await Promise.all(promises);
ui.setRelayMeshInfo(node.relay.gossipSub);
},
dropNetworkConnections: async () => {
const promises = node.libp2p
.getConnections()
.filter((c) => c.stat.multiplexer !== "/webrtc")
.map(async (c) => {
const peerId = c.remotePeer.toString();
node.relay.gossipSub.peers.delete(peerId);
node.relay.gossipSub.streamsInbound.delete(peerId);
node.relay.gossipSub.streamsOutbound.delete(peerId);
await node.libp2p.peerStore.delete(c.remotePeer);
await c.close();
});
await Promise.all(promises);
ui.setRelayMeshInfo(node.relay.gossipSub);
},
};
}
// UI adapter
function initUI() {
const exitButton = document.getElementById("exit");
const sendButton = document.getElementById("send");
const statusBlock = document.getElementById("status");
const localPeerBlock = document.getElementById("localPeerId");
const remotePeerId = document.getElementById("remotePeerId");
const contentTopicBlock = document.getElementById("contentTopic");
const messagesBlock = document.getElementById("messages");
const nickText = document.getElementById("nickText");
const messageText = document.getElementById("messageText");
const remoteNode = document.getElementById("remoteNode");
const connectRemoteNode = document.getElementById("connectRemoteNode");
const webrtcPeer = document.getElementById("webrtcPeer");
const connectWebrtcPeer = document.getElementById("connectWebrtcPeer");
const relayWebRTCbutton = document.getElementById("relayWebRTC");
const dropNonWebRTCbutton = document.getElementById("dropNonWebRTC");
const relayMeshInfo = document.getElementById("relayMeshInfo");
return {
// UI events
onExit: (cb) => {
exitButton.addEventListener("click", cb);
},
onSendMessage: (cb) => {
sendButton.addEventListener("click", async () => {
await cb({
nick: nickText.value,
text: messageText.value,
});
messageText.value = "";
});
},
// UI renderers
setStatus: (value, className) => {
statusBlock.innerHTML = `<span class=${className || ""}>${value}</span>`;
},
setLocalPeer: (id) => {
localPeerBlock.innerText = id.toString();
},
setRemotePeer: (ids) => {
remotePeerId.innerText = ids.join("\n");
},
setContentTopic: (topic) => {
contentTopicBlock.innerText = topic.toString();
},
renderMessage: (messageObj) => {
const { nick, text, timestamp } = messageObj;
const date = new Date(timestamp);
// WARNING: XSS vulnerable
messagesBlock.innerHTML += `
<div class="message">
<p>${nick} <span>(${date.toDateString()})</span>:</p>
<p>${text}</p>
<div>
`;
},
resetMessages: () => {
messagesBlock.innerHTML = "";
},
setWebrtcPeer: (peerId) => {
webrtcPeer.value = peerId;
},
onRemoteNodeConnect: (cb) => {
connectRemoteNode.addEventListener("click", () => {
const multiaddr = remoteNode.value;
if (!multiaddr) {
throw Error("No multiaddr set to dial");
}
cb(multiaddr);
});
},
onWebrtcConnect: (cb) => {
connectWebrtcPeer.addEventListener("click", () => {
const multiaddr = webrtcPeer.value;
if (!multiaddr) {
throw Error("No multiaddr to dial webrtc");
}
cb(multiaddr);
});
},
onRelayWebRTC: (cb) => {
relayWebRTCbutton.addEventListener("click", cb);
},
onDropNonWebRTC: (cb) => {
dropNonWebRTCbutton.addEventListener("click", cb);
},
setRelayMeshInfo: (gossipSub) => {
relayMeshInfo.innerHTML = "";
Array.from(gossipSub.peers)
.map((peerId) => {
let inbound = gossipSub.streamsInbound.get(peerId);
inbound = inbound ? inbound.rawStream.constructor.name : "none";
let outbound = gossipSub.streamsOutbound.get(peerId);
outbound = outbound ? outbound.rawStream.constructor.name : "none";
return [peerId, inbound, outbound];
})
.map(([peerId, inbound, outbound]) => {
relayMeshInfo.innerHTML += `${peerId}<br /><b>inbound</b>: ${inbound}\t<b>outbound</b>: ${outbound}`;
relayMeshInfo.innerHTML += "<br /><br />";
});
},
};
}

View File

@ -0,0 +1,185 @@
* {
margin: 0;
padding: 0;
word-wrap: break-word;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
html {
font-size: 16px;
overflow: hidden;
}
body {
display: flex;
align-items: center;
padding: 10px;
justify-content: center;
}
h3 + h4,
div + h4,
div + details,
div + div {
margin-top: 20px;
}
.header div {
display: flex;
justify-content: space-between;
}
.header div input {
min-width: 300px;
min-height: 30px;
width: 80%;
margin-right: 10px;
}
.header div button {
min-width: 50px;
min-height: 30px;
width: 10%;
flex-grow: 1;
}
.header div button + button {
margin-left: 10px;
}
details {
margin-bottom: 15px;
}
details p {
margin-bottom: 10px;
}
summary {
cursor: pointer;
max-width: 100%;
margin-bottom: 5px;
}
span {
font-weight: 300;
}
input,
textarea {
line-height: 1rem;
padding: 5px;
}
textarea {
min-height: 3rem;
}
h3 {
margin-bottom: 5px;
}
.content {
width: 800px;
min-width: 300px;
max-width: 800px;
height: 100%;
display: flex;
flex-direction: column;
align-content: space-between;
}
#messages {
overflow-y: scroll;
overflow-x: hidden;
}
.message + .message {
margin-top: 15px;
}
.message :first-child {
font-weight: bold;
}
.message p + p {
margin-top: 5px;
}
.message span {
font-size: 0.8rem;
}
.inputArea {
display: flex;
gap: 10px;
flex-direction: column;
margin-top: 20px;
}
.controls {
margin-top: 10px;
display: flex;
gap: 10px;
}
.controls button {
flex-grow: 1;
cursor: pointer;
padding: 10px;
}
#send {
background-color: #32d1a0;
border: none;
color: white;
}
#send:hover {
background-color: #3abd96;
}
#send:active {
background-color: #3ba183;
}
#exit {
color: white;
border: none;
background-color: #ff3a31;
}
#exit:hover {
background-color: #e4423a;
}
#exit:active {
background-color: #c84740;
}
.success {
color: #3ba183;
}
.progress {
color: #9ea13b;
}
.terminated {
color: black;
}
.error {
color: #c84740;
}
.footer {
display: flex;
width: 100%;
flex-direction: column;
align-self: flex-end;
}

View File

@ -13,7 +13,13 @@ module.exports = {
mode: "development",
plugins: [
new CopyWebpackPlugin({
patterns: ["index.html", "favicon.ico", "favicon.png", "manifest.json"],
patterns: [
"index.html",
"favicon.ico",
"favicon.png",
"manifest.json",
"style.css",
],
}),
],
};

View File

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>JS-Waku light node example</title>
<link rel="apple-touch-icon" href="./favicon.png" />
<link rel="manifest" href="./manifest.json" />
<link rel="icon" href="./favicon.ico" />
</head>
<body>
<div><h2>Status</h2></div>
<div id="status"></div>
<div><h2>Local Peer Id</h2></div>
<div id="peer-id"></div>
<div><h2>Remote Peer Id</h2></div>
<div id="remote-peer-id"></div>
<label for="remote-multiaddr">Remote peer's multiaddr</label>
<input id="remote-multiaddr" type="text" value="" />
<button disabled id="dial" type="button">Dial</button>
<br />
<button disabled id="subscribe" type="button">Subscribe with Filter</button>
<button disabled id="unsubscribe" type="button">
Unsubscribe with Filter
</button>
<br />
<label for="textInput">Message text</label>
<input id="textInput" placeholder="Type your message here" type="text" />
<button disabled id="sendButton" type="button">
Send message using Light Push
</button>
<br />
<div id="messages"></div>
<script src="https://unpkg.com/@multiformats/multiaddr@12.1.1/dist/index.min.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@ -1,132 +0,0 @@
import {
createRelayNode,
waitForRemotePeer,
createEncoder,
createDecoder,
utf8ToBytes,
bytesToUtf8,
} from "@waku/sdk";
import { webSockets } from "@libp2p/websockets";
import { all as filterAll } from "@libp2p/websockets/filters";
import { webRTC } from "@libp2p/webrtc";
import { circuitRelayTransport } from "libp2p/circuit-relay";
const peerIdDiv = document.getElementById("peer-id");
const remotePeerIdDiv = document.getElementById("remote-peer-id");
const statusDiv = document.getElementById("status");
const remoteMultiAddrDiv = document.getElementById("remote-multiaddr");
const dialButton = document.getElementById("dial");
const subscribeButton = document.getElementById("subscribe");
const unsubscribeButton = document.getElementById("unsubscribe");
const messagesDiv = document.getElementById("messages");
const textInput = document.getElementById("textInput");
const sendButton = document.getElementById("sendButton");
const ContentTopic = "/js-waku-examples/1/chat/utf8";
const decoder = createDecoder(ContentTopic);
const encoder = createEncoder({ contentTopic: ContentTopic });
let messages = [];
let unsubscribe;
const updateMessages = (msgs, div) => {
div.innerHTML = "<ul>";
messages.forEach((msg) => (div.innerHTML += "<li>" + msg + "</li>"));
div.innerHTML += "</ul>";
};
statusDiv.innerHTML = "<p>Creating Waku node.</p>";
const node = await createRelayNode({
libp2p: {
addresses: {
listen: ["/webrtc"],
},
connectionGater: {
denyDialMultiaddr: () => {
// by default we refuse to dial local addresses from the browser since they
// are usually sent by remote peers broadcasting undialable multiaddrs but
// here we are explicitly connecting to a local node so do not deny dialing
// any discovered address
return false;
},
},
transports: [
webRTC({}),
circuitRelayTransport({
discoverRelays: 1,
}),
webSockets({ filter: filterAll }),
],
},
});
window.node = node;
statusDiv.innerHTML = "<p>Starting Waku node.</p>";
await node.start();
statusDiv.innerHTML = "<p>Waku node started.</p>";
peerIdDiv.innerHTML = "<p>" + node.libp2p.peerId.toString() + "</p>";
dialButton.disabled = false;
dialButton.onclick = async () => {
const ma = remoteMultiAddrDiv.value;
if (!ma) {
statusDiv.innerHTML = "<p>Error: No multiaddr provided.</p>";
return;
}
statusDiv.innerHTML = "<p>Dialing peer.</p>";
const multiaddr = MultiformatsMultiaddr.multiaddr(ma);
await node.dial(multiaddr, ["relay"]);
await waitForRemotePeer(node, ["relay"]);
const peers = await node.libp2p.peerStore.all();
statusDiv.innerHTML = "<p>Peer dialed.</p>";
remotePeerIdDiv.innerHTML = "<p>" + peers[0].id.toString() + "</p>";
textInput.disabled = false;
sendButton.disabled = false;
subscribeButton.disabled = false;
};
const callback = (wakuMessage) => {
const text = bytesToUtf8(wakuMessage.payload);
const timestamp = wakuMessage.timestamp.toString();
messages.push(text + " - " + timestamp);
updateMessages(messages, messagesDiv);
};
subscribeButton.onclick = async () => {
unsubscribe = await node.relay.subscribe([decoder], callback);
unsubscribeButton.disabled = false;
subscribeButton.disabled = true;
};
unsubscribeButton.onclick = async () => {
await unsubscribe();
unsubscribe = undefined;
unsubscribeButton.disabled = true;
subscribeButton.disabled = false;
};
sendButton.onclick = async () => {
const text = textInput.value;
await node.relay.send(encoder, {
payload: utf8ToBytes(text),
});
console.log("Message sent!");
textInput.value = null;
};
const GONODE = "16Uiu2HAmUdyH4P2UhgX3hTCbeeJHpxxfsn8EdyHkMnPyb54a92jZ";
const root = `/ip4/192.168.0.101/tcp/60001/ws/p2p/${GONODE}`;
remoteMultiAddrDiv.value = root;
window.dial = (id) => node.dial(`${root}/p2p-circuit/webrtc/p2p/${id}`);
window.drop = () =>
Array.from(node.libp2p.components.connectionManager.connections.entries())
.filter((c) => c[0].toString() === GONODE)
.map((c) => console.log(c[1][0].close()));
/*
/ip4/127.0.0.1/tcp/60001/ws/p2p/16Uiu2HAm3s9fFHbcVrKQz2h5fMAsUzm3AeCUxg52e3SBFjt7q4Gg
/ip4/127.0.0.1/tcp/60001/ws/p2p/16Uiu2HAm3s9fFHbcVrKQz2h5fMAsUzm3AeCUxg52e3SBFjt7q4Gg/p2p-circuit/p2p/12D3KooWMghpY8592CnQQTyg4fiSJcUuA7Pu8n8YzUPYaLjzH7Yo/p2p-circuit/webrtc/p2p/12D3KooWMghpY8592CnQQTyg4fiSJcUuA7Pu8n8YzUPYaLjzH7Yo
*/