From bb06af386b98e45c74fb1ba1e10f45cef00b99e2 Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 30 Aug 2023 00:53:31 +0200 Subject: [PATCH] add relay chat example --- examples/{relay-rtc => relay-chat}/README.md | 0 .../{relay-rtc => relay-chat}/favicon.ico | Bin .../{relay-rtc => relay-chat}/favicon.png | Bin examples/relay-chat/index.html | 70 ++++ examples/relay-chat/index.js | 340 ++++++++++++++++++ .../{relay-rtc => relay-chat}/manifest.json | 0 .../package-lock.json | 0 .../{relay-rtc => relay-chat}/package.json | 0 examples/relay-chat/style.css | 185 ++++++++++ .../webpack.config.js | 8 +- examples/relay-rtc/index.html | 42 --- examples/relay-rtc/index.js | 132 ------- 12 files changed, 602 insertions(+), 175 deletions(-) rename examples/{relay-rtc => relay-chat}/README.md (100%) rename examples/{relay-rtc => relay-chat}/favicon.ico (100%) rename examples/{relay-rtc => relay-chat}/favicon.png (100%) create mode 100644 examples/relay-chat/index.html create mode 100644 examples/relay-chat/index.js rename examples/{relay-rtc => relay-chat}/manifest.json (100%) rename examples/{relay-rtc => relay-chat}/package-lock.json (100%) rename examples/{relay-rtc => relay-chat}/package.json (100%) create mode 100644 examples/relay-chat/style.css rename examples/{relay-rtc => relay-chat}/webpack.config.js (71%) delete mode 100644 examples/relay-rtc/index.html delete mode 100644 examples/relay-rtc/index.js diff --git a/examples/relay-rtc/README.md b/examples/relay-chat/README.md similarity index 100% rename from examples/relay-rtc/README.md rename to examples/relay-chat/README.md diff --git a/examples/relay-rtc/favicon.ico b/examples/relay-chat/favicon.ico similarity index 100% rename from examples/relay-rtc/favicon.ico rename to examples/relay-chat/favicon.ico diff --git a/examples/relay-rtc/favicon.png b/examples/relay-chat/favicon.png similarity index 100% rename from examples/relay-rtc/favicon.png rename to examples/relay-chat/favicon.png diff --git a/examples/relay-chat/index.html b/examples/relay-chat/index.html new file mode 100644 index 0000000..25b6de9 --- /dev/null +++ b/examples/relay-chat/index.html @@ -0,0 +1,70 @@ + + + + + + JS-Waku light chat + + + + + + + +
+
+

Status:

+ +

+
+ + +
+ +

+
+ + +
+ +
+ + +
+ +
+ Peer's information + +

Content topic

+

+ +

Local Peer Id

+

+ +

Remote Peer Id

+

+ +

Relay mesh's protocols

+

+
+
+ +
+ + +
+ + + + + diff --git a/examples/relay-chat/index.js b/examples/relay-chat/index.js new file mode 100644 index 0000000..48c019b --- /dev/null +++ b/examples/relay-chat/index.js @@ -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 = `${value}`; + }, + 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 += ` +
+

${nick} (${date.toDateString()}):

+

${text}

+
+ `; + }, + 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}
inbound: ${inbound}\toutbound: ${outbound}`; + relayMeshInfo.innerHTML += "

"; + }); + }, + }; +} diff --git a/examples/relay-rtc/manifest.json b/examples/relay-chat/manifest.json similarity index 100% rename from examples/relay-rtc/manifest.json rename to examples/relay-chat/manifest.json diff --git a/examples/relay-rtc/package-lock.json b/examples/relay-chat/package-lock.json similarity index 100% rename from examples/relay-rtc/package-lock.json rename to examples/relay-chat/package-lock.json diff --git a/examples/relay-rtc/package.json b/examples/relay-chat/package.json similarity index 100% rename from examples/relay-rtc/package.json rename to examples/relay-chat/package.json diff --git a/examples/relay-chat/style.css b/examples/relay-chat/style.css new file mode 100644 index 0000000..77212d7 --- /dev/null +++ b/examples/relay-chat/style.css @@ -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; +} diff --git a/examples/relay-rtc/webpack.config.js b/examples/relay-chat/webpack.config.js similarity index 71% rename from examples/relay-rtc/webpack.config.js rename to examples/relay-chat/webpack.config.js index aa2eb4e..9fb628f 100644 --- a/examples/relay-rtc/webpack.config.js +++ b/examples/relay-chat/webpack.config.js @@ -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", + ], }), ], }; diff --git a/examples/relay-rtc/index.html b/examples/relay-rtc/index.html deleted file mode 100644 index 2d099c3..0000000 --- a/examples/relay-rtc/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - JS-Waku light node example - - - - - - -

Status

-
- -

Local Peer Id

-
- -

Remote Peer Id

-
- - - - -
- - -
- - - -
-
- - - - - diff --git a/examples/relay-rtc/index.js b/examples/relay-rtc/index.js deleted file mode 100644 index a523b56..0000000 --- a/examples/relay-rtc/index.js +++ /dev/null @@ -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 = "
    "; - messages.forEach((msg) => (div.innerHTML += "
  • " + msg + "
  • ")); - div.innerHTML += "
"; -}; - -statusDiv.innerHTML = "

Creating Waku node.

"; -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 = "

Starting Waku node.

"; -await node.start(); -statusDiv.innerHTML = "

Waku node started.

"; -peerIdDiv.innerHTML = "

" + node.libp2p.peerId.toString() + "

"; -dialButton.disabled = false; - -dialButton.onclick = async () => { - const ma = remoteMultiAddrDiv.value; - if (!ma) { - statusDiv.innerHTML = "

Error: No multiaddr provided.

"; - return; - } - statusDiv.innerHTML = "

Dialing peer.

"; - const multiaddr = MultiformatsMultiaddr.multiaddr(ma); - await node.dial(multiaddr, ["relay"]); - await waitForRemotePeer(node, ["relay"]); - const peers = await node.libp2p.peerStore.all(); - statusDiv.innerHTML = "

Peer dialed.

"; - remotePeerIdDiv.innerHTML = "

" + peers[0].id.toString() + "

"; - 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 - */