import { createLightNode } from "js-waku/lib/create_waku"; import { utils } from "js-waku"; import { waitForRemotePeer } from "js-waku/lib/wait_for_remote_peer"; import { Fleet, getPredefinedBootstrapNodes } from "js-waku/lib/predefined_bootstrap_nodes"; import { PeerDiscoveryStaticPeers } from "js-waku/lib/peer_discovery_static_list"; import { Protocols } from "js-waku"; import * as noise from "@waku/noise"; import protobuf from "protobufjs"; import QRCode from "qrcode"; // TODO: Get rid of these import hexToArrayBuffer from "hex-to-array-buffer"; import arrayBufferToHex from "array-buffer-to-hex"; const messagesDiv = document.getElementById("messages"); const nicknameInput = document.getElementById("nick-input"); const textInput = document.getElementById("text-input"); const sendButton = document.getElementById("send-btn"); const sendingStatusSpan = document.getElementById("sending-status"); const chatArea = document.getElementById("chat-area"); const qrCanvas = document.getElementById("qr-canvas"); const qrUrl = document.getElementById("qr-url"); const wakuStatusSpan = document.getElementById("waku-status"); const handshakeStatusSpan = document.getElementById("handshake-status"); function getPairingInfofromUrl() { const urlParts = window.location.href.split("?"); if (urlParts.length < 2) return undefined; const pairingParts = decodeURIComponent(urlParts[1]).split(":"); if (pairingParts.length < 6) throw new Error("invalid pairing information format"); const qrMessageNameTag = new Uint8Array(hexToArrayBuffer(pairingParts.shift())); return new noise.InitiatorParameters(pairingParts.join(":"), qrMessageNameTag); } function getSenderAndResponder(node) { const sender = { async publish(encoder, msg) { await node.lightPush.push(encoder, msg); }, }; const msgQueue = new Array(); const subscriptions = new Map(); const intervals = new Map(); const responder = { async subscribe(decoder) { const subscription = await node.filter.subscribe([decoder], (wakuMessage) => { msgQueue.push(wakuMessage); }); subscriptions.set(decoder.contentTopic, subscription); }, async nextMessage(contentTopic) { if (msgQueue.length != 0) { const oldestMsg = msgQueue.shift(); if (oldestMsg.contentTopic === contentTopic) { return oldestMsg; } } return new Promise((resolve) => { const interval = setInterval(() => { if (msgQueue.length != 0) { clearInterval(interval); const oldestMsg = msgQueue.shift(); if (oldestMsg.contentTopic === contentTopic) { resolve(oldestMsg); } } }, 100); intervals.set(contentTopic, interval); }); }, async stop(contentTopic) { if (intervals.has(contentTopic)) { clearInterval(intervals.get(contentTopic)); intervals.delete(contentTopic); } if (subscriptions.has(contentTopic)) { await subscriptions.get(contentTopic)(); subscriptions.delete(contentTopic); } else { console.log("Subscriptipon doesnt exist") } }, }; return [sender, responder]; } async function confirmAuthCodeFlow(pairingObj) { const authCode = await pairingObj.getAuthCode(); pairingObj.validateAuthCode(confirm("Confirm that authcode is: " + authCode)); } async function hideQR() { qrCanvas.remove(); qrUrl.remove(); } async function disableUI() { hideQR(); chatArea.remove(); } // Function to update the fields to guide the user by disabling buttons. const updateFields = () => { const readyToSend = nicknameInput.value !== ""; textInput.disabled = !readyToSend; sendButton.disabled = !readyToSend; }; // Protobuf const ProtoChatMessage = 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")); let messages = []; const updateMessages = () => { messagesDiv.innerHTML = ""; }; const onMessage = (wakuMessage) => { const { timestamp, nick, text } = ProtoChatMessage.decode(wakuMessage.payload); const time = new Date(); time.setTime(Number(timestamp) * 1000); messages.push(`(${nick}) ${utils.bytesToUtf8(text)} [${time.toISOString()}]`); updateMessages(); }; async function main() { // Starting the node const node = await createLightNode({ libp2p: { peerDiscovery: [new PeerDiscoveryStaticPeers(getPredefinedBootstrapNodes(Fleet.Test))], }, }); try { await node.start(); await waitForRemotePeer(node, [Protocols.Filter, Protocols.LightPush]); wakuStatusSpan.innerHTML = "connected"; const [sender, responder] = getSenderAndResponder(node); const myStaticKey = noise.generateX25519KeyPair(); const pairingParameters = getPairingInfofromUrl(); const initiator = pairingParameters ? true : false; let encoder; let decoder; if (initiator) { console.log("Initiator"); qrCanvas.remove(); // Initiator does not require a QR code const pairingObj = new noise.WakuPairing(sender, responder, myStaticKey, pairingParameters); const pExecute = pairingObj.execute(120000); // timeout after 2m confirmAuthCodeFlow(pairingObj); try { handshakeStatusSpan.innerHTML = "executing handshake..."; [encoder, decoder] = await pExecute; handshakeStatusSpan.innerHTML = "handshake completed!"; } catch (err) { handshakeStatusSpan.innerHTML = err.message; disableUI(); console.error(err); } /* // The information needs to be backed up to decrypt messages sent with // codecs generated with the handshake. The `handshakeResult` variable // contains private information that needs to be stored safely const contentTopic = pairingObj.contentTopic; const handshakeResult = pairingObj.getHandshakeResult(); // To restore the codecs for decrypting older messages, or continuing an existing // session, use this: [encoder, decoder] = WakuPairing.getSecureCodec(contentTopic, handshakeResult); */ } else { console.log("Responder"); const pairingObj = new noise.WakuPairing(sender, responder, myStaticKey, new noise.ResponderParameters()); const pExecute = pairingObj.execute(120000); // timeout after 2m confirmAuthCodeFlow(pairingObj); const pInfo = pairingObj.getPairingInfo(); // Data to encode in the QR code. The qrMessageNametag too to the QR string (separated by ) const qrString = arrayBufferToHex(pInfo.qrMessageNameTag) + ":" + pInfo.qrCode; const qrURLString = window.location.href + "?" + encodeURIComponent(qrString); handshakeStatusSpan.innerHTML = "generating QR code..."; console.log("Generating QR..."); QRCode.toCanvas(qrCanvas, qrURLString, (err) => { if (err) { handshakeStatusSpan.innerHTML = err.message; disableUI(); console.error(err); } else { handshakeStatusSpan.innerHTML = "waiting for handshake to start"; qrUrl.href = qrURLString; qrUrl.style.display = "block"; } }); try { handshakeStatusSpan.innerHTML = "executing handshake..."; [encoder, decoder] = await pExecute; handshakeStatusSpan.innerHTML = "handshake completed!"; hideQR(); } catch (err) { handshakeStatusSpan.innerHTML = err.message; disableUI(); console.error(err); } /* // The information needs to be backed up to decrypt messages sent with // codecs generated with the handshake. The `handshakeResult` variable // contains private information that needs to be stored safely const contentTopic = pairingObj.contentTopic; const handshakeResult = pairingObj.getHandshakeResult(); // To restore the codecs for decrypting older messages, or continuing an existing // session, use this: [encoder, decoder] = WakuPairing.getSecureCodec(contentTopic, handshakeResult); */ } nicknameInput.onchange = updateFields; nicknameInput.onblur = updateFields; sendButton.onclick = async () => { const text = utils.utf8ToBytes(textInput.value); const timestamp = new Date(); const msg = ProtoChatMessage.create({ text, nick: nicknameInput.value, timestamp: Math.floor(timestamp.valueOf() / 1000), }); const payload = ProtoChatMessage.encode(msg).finish(); sendingStatusSpan.innerText = "sending..."; await node.lightPush.push(encoder, { payload, timestamp }); sendingStatusSpan.innerText = "sent!"; onMessage({ payload }); textInput.value = null; setTimeout(() => { sendingStatusSpan.innerText = ""; }, 5000); }; await node.filter.subscribe([decoder], onMessage); chatArea.style.display = "block"; } catch (err) { wakuStatusSpan.innerHTML = err.message; disableUI(); return; } } main();