diff --git a/buf.gen.yaml b/buf.gen.yaml index d04c651fb3..40edf9635d 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -3,4 +3,4 @@ version: v1beta1 plugins: - name: ts_proto out: ./src/proto - opt: grpc_js + opt: grpc_js,esModuleInterop=true diff --git a/proto/chat/v2/chat_message.proto b/proto/chat/v2/chat_message.proto new file mode 100644 index 0000000000..9d33bec91f --- /dev/null +++ b/proto/chat/v2/chat_message.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package chat.v2; + +message ChatMessageProto { + uint64 timestamp = 1; + string nick = 2; + bytes payload = 3; +} diff --git a/src/chat/chat_message.spec.ts b/src/chat/chat_message.spec.ts new file mode 100644 index 0000000000..a33d08c22c --- /dev/null +++ b/src/chat/chat_message.spec.ts @@ -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); + } + ) + ); + }); +}); diff --git a/src/chat/chat_message.ts b/src/chat/chat_message.ts new file mode 100644 index 0000000000..855245aca1 --- /dev/null +++ b/src/chat/chat_message.ts @@ -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(); + } +} diff --git a/src/chat/index.ts b/src/chat/index.ts index 6e26a75f56..f9f83058d1 100644 --- a/src/chat/index.ts +++ b/src/chat/index.ts @@ -5,6 +5,8 @@ import { Message } 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(); @@ -12,8 +14,18 @@ import { delay } from '../test_utils/delay'; // TODO: Bubble event to waku, infer topic, decode msg waku.libp2p.pubsub.on(TOPIC, (event) => { - const msg = Message.fromBinary(event.data); - console.log(msg.utf8Payload()); + const wakuMsg = Message.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'); @@ -47,7 +59,9 @@ import { delay } from '../test_utils/delay'; rl.prompt(); rl.on('line', async (line) => { rl.prompt(); - const msg = Message.fromUtf8String('(js-chat) ' + line); + const chatMessage = new ChatMessage(new Date(), 'js-chat', line); + + const msg = Message.fromBytes(chatMessage.encode()); await waku.relay.publish(msg); }); })(); diff --git a/src/lib/waku_message.spec.ts b/src/lib/waku_message.spec.ts index 5865f7fe18..53bbeed709 100644 --- a/src/lib/waku_message.spec.ts +++ b/src/lib/waku_message.spec.ts @@ -8,7 +8,7 @@ describe('Waku Message', function () { fc.property(fc.string(), (s) => { const msg = Message.fromUtf8String(s); const binary = msg.toBinary(); - const actual = Message.fromBinary(binary); + const actual = Message.decode(binary); return actual.isEqualTo(msg); }) diff --git a/src/lib/waku_message.ts b/src/lib/waku_message.ts index 1b46978fb0..07bf32221a 100644 --- a/src/lib/waku_message.ts +++ b/src/lib/waku_message.ts @@ -15,16 +15,25 @@ export class Message { ) {} /** - * Create Message from utf-8 string - * @param message + * Create Message with a utf-8 string as payload + * @param payload * @returns {Message} */ - static fromUtf8String(message: string): Message { - const payload = Buffer.from(message, 'utf-8'); + static fromUtf8String(payload: string): Message { + const buf = Buffer.from(payload, 'utf-8'); + return new Message(buf, DEFAULT_CONTENT_TOPIC, DEFAULT_VERSION); + } + + /** + * Create Message with a byte array as payload + * @param payload + * @returns {Message} + */ + static fromBytes(payload: Uint8Array): Message { return new Message(payload, DEFAULT_CONTENT_TOPIC, DEFAULT_VERSION); } - static fromBinary(bytes: Uint8Array): Message { + static decode(bytes: Uint8Array): Message { const wakuMsg = WakuMessage.decode(Reader.create(bytes)); return new Message(wakuMsg.payload, wakuMsg.contentTopic, wakuMsg.version); } diff --git a/src/lib/waku_relay.spec.ts b/src/lib/waku_relay.spec.ts index 24b9d465fa..cfbf011c72 100644 --- a/src/lib/waku_relay.spec.ts +++ b/src/lib/waku_relay.spec.ts @@ -392,6 +392,6 @@ function waitForNextData(pubsub: Pubsub): Promise { return new Promise((resolve) => { pubsub.once(TOPIC, resolve); }).then((msg: any) => { - return Message.fromBinary(msg.data); + return Message.decode(msg.data); }); }