Skip to content

Commit

Permalink
Merge #9
Browse files Browse the repository at this point in the history
9: chat example r=D4nte a=D4nte

Resolves #15

Co-authored-by: Franck Royer <franck@royer.one>
  • Loading branch information
bors[bot] and D4nte authored Apr 1, 2021
2 parents fdff7c4 + 5a967ec commit 62b27fd
Show file tree
Hide file tree
Showing 15 changed files with 452 additions and 51 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,24 @@

A JavaScript implementation of the [Waku v2 protocol](https://specs.vac.dev/specs/waku/v2/waku-v2).

**This repo is a Work In Progress**
## This is a Work In Progress

You can track progress on the [project board](https://github.com/status-im/js-waku/projects/1).

## Examples

## Chat app

A node chat app is provided as a working example of the library.
It is interoperable with the [nim-waku chat app example](https://github.com/status-im/nim-waku/blob/master/examples/v2/chat2.nim).
To run the chat app:

```shell
npm install
npm run chat:app -- --staticNode /ip4/134.209.139.210/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ --listenAddr /ip4/0.0.0.0/tcp/55123
```

The `--listenAddr` parameter is optional, however [NAT passthrough](https://github.com/status-im/js-waku/issues/12) is not yet supported, so you'll need the listening port to be open to receive messages from the fleet.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ version: v1beta1
plugins:
- name: ts_proto
out: ./src/proto
opt: grpc_js
opt: grpc_js,esModuleInterop=true
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "waku-js-chat",
"name": "js-waku",
"version": "1.0.0",
"description": "A chat application running on node and waku",
"main": "build/main/index.js",
Expand All @@ -19,6 +19,7 @@
"pretest": "run-s pretest:*",
"pretest:1-init-git-submodules": "[ -f './nim-waku/build/wakunode2' ] || git submodule update --init --recursive",
"pretest:2-build-nim-waku": "cd nim-waku; [ -f './build/wakunode2' ] || make -j$(nproc --all 2>/dev/null || echo 2) wakunode2",
"chat:start": "ts-node src/chat/index.ts",
"test": "run-s build test:*",
"test:lint": "eslint src --ext .ts",
"test:prettier": "prettier \"src/**/*.ts\" --list-different",
Expand Down
9 changes: 9 additions & 0 deletions proto/chat/v2/chat_message.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
syntax = "proto3";

package chat.v2;

message ChatMessageProto {
uint64 timestamp = 1;
string nick = 2;
bytes payload = 3;
}
2 changes: 1 addition & 1 deletion proto/waku/v2/waku.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ syntax = "proto3";

package waku.v2;

message WakuMessage {
message WakuMessageProto {
optional bytes payload = 1;
optional uint32 content_topic = 2;
optional uint32 version = 3;
Expand Down
26 changes: 26 additions & 0 deletions src/chat/chat_message.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect } from 'chai';
import fc from 'fast-check';

import { ChatMessage } from './chat_message';

describe('Chat Message', function () {
it('Chat message round trip binary serialization', function () {
fc.assert(
fc.property(
fc.date({ min: new Date(0) }),
fc.string(),
fc.string(),
(timestamp, nick, message) => {
const msg = new ChatMessage(timestamp, nick, message);
const buf = msg.encode();
const actual = ChatMessage.decode(buf);

// Date.toString does not include ms, as we loose this precision by design
expect(actual.timestamp.toString()).to.eq(timestamp.toString());
expect(actual.nick).to.eq(nick);
expect(actual.message).to.eq(message);
}
)
);
});
});
35 changes: 35 additions & 0 deletions src/chat/chat_message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Reader } from 'protobufjs/minimal';

import { ChatMessageProto } from '../proto/chat/v2/chat_message';

export class ChatMessage {
public constructor(
public timestamp: Date,
public nick: string,
public message: string
) {}

static decode(bytes: Uint8Array): ChatMessage {
const protoMsg = ChatMessageProto.decode(Reader.create(bytes));
const timestamp = new Date(protoMsg.timestamp * 1000);
const message = protoMsg.payload
? Array.from(protoMsg.payload)
.map((char) => {
return String.fromCharCode(char);
})
.join('')
: '';
return new ChatMessage(timestamp, protoMsg.nick, message);
}

encode(): Uint8Array {
const timestamp = Math.floor(this.timestamp.valueOf() / 1000);
const payload = Buffer.from(this.message, 'utf-8');

return ChatMessageProto.encode({
timestamp,
nick: this.nick,
payload,
}).finish();
}
}
108 changes: 108 additions & 0 deletions src/chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import readline from 'readline';
import util from 'util';

import Waku from '../lib/waku';
import { WakuMessage } from '../lib/waku_message';
import { TOPIC } from '../lib/waku_relay';
import { delay } from '../test_utils/delay';

import { ChatMessage } from './chat_message';

(async function () {
const opts = processArguments();

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const question = util.promisify(rl.question).bind(rl);

// Looks like wrong type definition of promisify is picked.
// May be related to https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20497
const nick = ((await question(
'Please choose a nickname: '
)) as unknown) as string;
console.log(`Hi ${nick}!`);

const waku = await Waku.create({ listenAddresses: [opts.listenAddr] });

// TODO: Bubble event to waku, infer topic, decode msg
// Tracked with https://github.com/status-im/js-waku/issues/19
waku.libp2p.pubsub.on(TOPIC, (event) => {
const wakuMsg = WakuMessage.decode(event.data);
if (wakuMsg.payload) {
const chatMsg = ChatMessage.decode(wakuMsg.payload);
const timestamp = chatMsg.timestamp.toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: false,
});
console.log(`<${timestamp}> ${chatMsg.nick}: ${chatMsg.message}`);
}
});

console.log('Waku started');

if (opts.staticNode) {
console.log(`dialing ${opts.staticNode}`);
await waku.dial(opts.staticNode);
}

await new Promise((resolve) =>
waku.libp2p.pubsub.once('gossipsub:heartbeat', resolve)
);

// TODO: identify if it is possible to listen to an event to confirm dial
// finished instead of an arbitrary delay. Tracked with
// https://github.com/status-im/js-waku/issues/18
await delay(2000);
// TODO: Automatically subscribe, tracked with
// https://github.com/status-im/js-waku/issues/17
await waku.relay.subscribe();
console.log('Subscribed to waku relay');

await new Promise((resolve) =>
waku.libp2p.pubsub.once('gossipsub:heartbeat', resolve)
);

console.log('Ready to chat!');
rl.prompt();
for await (const line of rl) {
rl.prompt();
const chatMessage = new ChatMessage(new Date(), nick, line);

const msg = WakuMessage.fromBytes(chatMessage.encode());
await waku.relay.publish(msg);
}
})();

interface Options {
staticNode?: string;
listenAddr: string;
}

function processArguments(): Options {
const passedArgs = process.argv.slice(2);

let opts: Options = { listenAddr: '/ip4/0.0.0.0/tcp/0' };

while (passedArgs.length) {
const arg = passedArgs.shift();
switch (arg) {
case '--staticNode':
opts = Object.assign(opts, { staticNode: passedArgs.shift() });
break;
case '--listenAddr':
opts = Object.assign(opts, { listenAddr: passedArgs.shift() });
break;
default:
console.log(`Unsupported argument: ${arg}`);
process.exit(1);
}
}

return opts;
}
2 changes: 1 addition & 1 deletion src/lib/waku.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Waku', function () {
describe('Interop: Nim', function () {
it('nim connects to js', async function () {
this.timeout(10_000);
const waku = await Waku.create(NOISE_KEY_1);
const waku = await Waku.create({ staticNoiseKey: NOISE_KEY_1 });

const peerId = waku.libp2p.peerId.toB58String();

Expand Down
28 changes: 25 additions & 3 deletions src/lib/waku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,38 @@ import PeerId from 'peer-id';

import { CODEC, WakuRelay, WakuRelayPubsub } from './waku_relay';

export interface CreateOptions {
listenAddresses: string[];
staticNoiseKey: bytes | undefined;
}

export default class Waku {
private constructor(public libp2p: Libp2p, public relay: WakuRelay) {}

/**
* Create new waku node
* @param listenAddresses: Array of Multiaddrs on which the node should listen. If not present, defaults to ['/ip4/0.0.0.0/tcp/0'].
* @param staticNoiseKey: A static key to use for noise,
* mainly used for test to reduce entropy usage.
* @returns {Promise<Waku>}
*/
static async create(staticNoiseKey?: bytes): Promise<Waku> {
static async create(options: Partial<CreateOptions>): Promise<Waku> {
const opts = Object.assign(
{
listenAddresses: ['/ip4/0.0.0.0/tcp/0'],
staticNoiseKey: undefined,
},
options
);

const libp2p = await Libp2p.create({
addresses: {
listen: ['/ip4/0.0.0.0/tcp/0'],
listen: opts.listenAddresses,
},
modules: {
transport: [TCP],
streamMuxer: [Mplex],
connEncryption: [new Noise(staticNoiseKey)],
connEncryption: [new Noise(opts.staticNoiseKey)],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Type needs update
pubsub: WakuRelayPubsub,
Expand All @@ -37,6 +51,14 @@ export default class Waku {
return new Waku(libp2p, new WakuRelay(libp2p.pubsub));
}

/**
* Dials to the provided peer. If successful, the known metadata of the peer will be added to the nodes peerStore, and the Connection will be returned
* @param peer The peer to dial
*/
async dial(peer: PeerId | Multiaddr | string) {
return this.libp2p.dialProtocol(peer, CODEC);
}

async dialWithMultiAddr(peerId: PeerId, multiaddr: Multiaddr[]) {
this.libp2p.peerStore.addressBook.set(peerId, multiaddr);
await this.libp2p.dialProtocol(peerId, CODEC);
Expand Down
17 changes: 14 additions & 3 deletions src/lib/waku_message.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import fc from 'fast-check';

import { Message } from './waku_message';
import { WakuMessage } from './waku_message';

describe('Waku Message', function () {
it('Waku message round trip binary serialization', function () {
fc.assert(
fc.property(fc.string(), (s) => {
const msg = Message.fromUtf8String(s);
const msg = WakuMessage.fromUtf8String(s);
const binary = msg.toBinary();
const actual = Message.fromBinary(binary);
const actual = WakuMessage.decode(binary);

return actual.isEqualTo(msg);
})
);
});

it('Payload to utf-8', function () {
fc.assert(
fc.property(fc.string(), (s) => {
const msg = WakuMessage.fromUtf8String(s);
const utf8 = msg.utf8Payload();

return utf8 === s;
})
);
});
});
Loading

0 comments on commit 62b27fd

Please sign in to comment.