diff --git a/packages/status-communities/src/chat.ts b/packages/status-communities/src/chat.ts index e08e3a22..b3403f54 100644 --- a/packages/status-communities/src/chat.ts +++ b/packages/status-communities/src/chat.ts @@ -1,4 +1,4 @@ -import { ChatMessage } from "./chat_message"; +import { ChatMessage, MediaContent } from "./chat_message"; import { chatIdToContentTopic } from "./contentTopic"; import { createSymKeyFromPassword } from "./encryption"; @@ -24,10 +24,16 @@ export class Chat { return chatIdToContentTopic(this.id); } - public createMessage(text: string): ChatMessage { + public createMessage(text: string, mediaContent?: MediaContent): ChatMessage { const { timestamp, clock } = this._nextClockAndTimestamp(); - const message = ChatMessage.createMessage(clock, timestamp, text, this.id); + const message = ChatMessage.createMessage( + clock, + timestamp, + text, + this.id, + mediaContent + ); this._updateClockFromMessage(message); diff --git a/packages/status-communities/src/chat_message.spec.ts b/packages/status-communities/src/chat_message.spec.ts new file mode 100644 index 00000000..451e3d88 --- /dev/null +++ b/packages/status-communities/src/chat_message.spec.ts @@ -0,0 +1,95 @@ +import { expect } from "chai"; + +import { + AudioContent, + ChatMessage, + ContentType, + ImageContent, + StickerContent, +} from "./chat_message"; +import { + AudioMessage_AudioType, + ChatMessage_ContentType, +} from "./proto/communities/v1/chat_message"; +import { ImageType } from "./proto/communities/v1/enums"; + +describe("Chat Message", () => { + it("Encode & decode Image message", () => { + const payload = Buffer.from([1, 1]); + + const imageContent: ImageContent = { + image: payload, + imageType: ImageType.IMAGE_TYPE_PNG, + contentType: ContentType.Image, + }; + + const message = ChatMessage.createMessage( + 1, + 1, + "Some text", + "chat-id", + imageContent + ); + + const buf = message.encode(); + const dec = ChatMessage.decode(buf); + + expect(dec.contentType).eq(ChatMessage_ContentType.CONTENT_TYPE_IMAGE); + expect(dec.image?.payload?.toString()).eq(payload.toString()); + expect(dec.image?.type).eq(ImageType.IMAGE_TYPE_PNG); + }); + + it("Encode & decode Audio message", () => { + const payload = Buffer.from([1, 1]); + const durationMs = 12345; + + const audioContent: AudioContent = { + audio: payload, + audioType: AudioMessage_AudioType.AUDIO_TYPE_AAC, + durationMs, + contentType: ContentType.Audio, + }; + + const message = ChatMessage.createMessage( + 1, + 1, + "Some text", + "chat-id", + audioContent + ); + + const buf = message.encode(); + const dec = ChatMessage.decode(buf); + + expect(dec.contentType).eq(ChatMessage_ContentType.CONTENT_TYPE_AUDIO); + expect(dec.audio?.payload?.toString()).eq(payload.toString()); + expect(dec.audio?.type).eq(ImageType.IMAGE_TYPE_PNG); + expect(dec.audio?.durationMs).eq(durationMs); + }); + + it("Encode & decode Sticker message", () => { + const hash = "deadbeef"; + const pack = 12345; + + const stickerContent: StickerContent = { + hash, + pack, + contentType: ContentType.Sticker, + }; + + const message = ChatMessage.createMessage( + 1, + 1, + "Some text", + "chat-id", + stickerContent + ); + + const buf = message.encode(); + const dec = ChatMessage.decode(buf); + + expect(dec.contentType).eq(ChatMessage_ContentType.CONTENT_TYPE_STICKER); + expect(dec.sticker?.hash).eq(hash); + expect(dec.sticker?.pack).eq(pack); + }); +}); diff --git a/packages/status-communities/src/chat_message.ts b/packages/status-communities/src/chat_message.ts index b11447f4..961f735e 100644 --- a/packages/status-communities/src/chat_message.ts +++ b/packages/status-communities/src/chat_message.ts @@ -3,24 +3,103 @@ import { Reader } from "protobufjs"; import * as proto from "./proto/communities/v1/chat_message"; import { AudioMessage, + AudioMessage_AudioType, ChatMessage_ContentType, ImageMessage, StickerMessage, } from "./proto/communities/v1/chat_message"; -import { MessageType } from "./proto/communities/v1/enums"; +import { ImageType, MessageType } from "./proto/communities/v1/enums"; + +export type MediaContent = StickerContent | ImageContent | AudioContent; + +export enum ContentType { + Sticker, + Image, + Audio, +} + +export interface StickerContent { + hash: string; + pack: number; + contentType: ContentType.Sticker; +} + +export interface ImageContent { + image: Uint8Array; + imageType: ImageType; + contentType: ContentType.Image; +} + +export interface AudioContent { + audio: Uint8Array; + audioType: AudioMessage_AudioType; + durationMs: number; + contentType: ContentType.Audio; +} + +function isSticker(content: MediaContent): content is StickerContent { + return content.contentType === ContentType.Sticker; +} + +function isImage(content: MediaContent): content is ImageContent { + return content.contentType === ContentType.Image; +} + +function isAudio(content: MediaContent): content is AudioContent { + return content.contentType === ContentType.Audio; +} export class ChatMessage { private constructor(public proto: proto.ChatMessage) {} /** - * Create a chat message to be sent to an Open (permission = no membership) community + * Create a chat message to be sent to an Open (permission = no membership) community. + * + * @throws string If mediaContent is malformed */ public static createMessage( clock: number, timestamp: number, text: string, - chatId: string + chatId: string, + mediaContent?: MediaContent ): ChatMessage { + let sticker, image, audio; + let contentType = ChatMessage_ContentType.CONTENT_TYPE_TEXT_PLAIN; + + if (mediaContent) { + if (isSticker(mediaContent)) { + if (!mediaContent.hash || !mediaContent.pack) + throw "Malformed Sticker Content"; + sticker = { + hash: mediaContent.hash, + pack: mediaContent.pack, + }; + contentType = ChatMessage_ContentType.CONTENT_TYPE_STICKER; + } else if (isImage(mediaContent)) { + if (!mediaContent.image || !mediaContent.imageType) + throw "Malformed Image Content"; + image = { + payload: mediaContent.image, + type: mediaContent.imageType, + }; + contentType = ChatMessage_ContentType.CONTENT_TYPE_IMAGE; + } else if (isAudio(mediaContent)) { + if ( + !mediaContent.audio || + !mediaContent.audioType || + !mediaContent.durationMs + ) + throw "Malformed Audio Content"; + audio = { + payload: mediaContent.audio, + type: mediaContent.audioType, + durationMs: mediaContent.durationMs, + }; + contentType = ChatMessage_ContentType.CONTENT_TYPE_AUDIO; + } + } + const proto = { clock, // ms? timestamp, //ms? @@ -34,10 +113,10 @@ export class ChatMessage { /** The type of message (public/one-to-one/private-group-chat) */ messageType: MessageType.MESSAGE_TYPE_COMMUNITY_CHAT, /** The type of the content of the message */ - contentType: ChatMessage_ContentType.CONTENT_TYPE_COMMUNITY, - sticker: undefined, - image: undefined, - audio: undefined, + contentType, + sticker, + image, + audio, community: undefined, // Used to share a community grant: undefined, }; diff --git a/packages/status-communities/src/messenger.ts b/packages/status-communities/src/messenger.ts index 45b4a78a..b2f66f19 100644 --- a/packages/status-communities/src/messenger.ts +++ b/packages/status-communities/src/messenger.ts @@ -4,7 +4,7 @@ import { CreateOptions as WakuCreateOptions } from "js-waku/build/main/lib/waku" import { ApplicationMetadataMessage } from "./application_metadata_message"; import { Chat } from "./chat"; -import { ChatMessage } from "./chat_message"; +import { ChatMessage, MediaContent } from "./chat_message"; import { Identity } from "./identity"; import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message"; @@ -71,15 +71,16 @@ export class Messenger { /** * Sends a message on the given chat Id. - * - * @param text - * @param chatId */ - public async sendMessage(text: string, chatId: string): Promise { + public async sendMessage( + text: string, + chatId: string, + mediaContent?: MediaContent + ): Promise { const chat = this.chatsById.get(chatId); if (!chat) throw `Failed to send message, chat not joined: ${chatId}`; - const chatMessage = chat.createMessage(text); + const chatMessage = chat.createMessage(text, mediaContent); const appMetadataMessage = ApplicationMetadataMessage.create( chatMessage.encode(),