From f595ff554d7bb48b592840af194bb757ed3d79b6 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Sat, 23 Aug 2025 17:30:04 -0700 Subject: [PATCH] Add dynamic contentFrame --- src/chat_sdk/client.nim | 9 ++-- src/chat_sdk/conversation_store.nim | 4 +- src/chat_sdk/conversations/convo_type.nim | 3 +- src/chat_sdk/conversations/private_v1.nim | 7 +-- src/chat_sdk/inbox.nim | 3 +- src/content_types/all.nim | 55 +++++++++++++++++++++++ src/content_types/protos/text_frame.proto | 9 ++++ src/nim_chat_poc.nim | 53 +++++++++++++++++++--- 8 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 src/content_types/all.nim create mode 100644 src/content_types/protos/text_frame.proto diff --git a/src/chat_sdk/client.nim b/src/chat_sdk/client.nim index 349bcd4..33998d5 100644 --- a/src/chat_sdk/client.nim +++ b/src/chat_sdk/client.nim @@ -51,7 +51,7 @@ type Client* = ref object inboundQueue: QueueRef isRunning: bool - newMessageCallbacks: seq[MessageCallback[string]] + newMessageCallbacks: seq[MessageCallback[ContentFrame]] newConvoCallbacks: seq[NewConvoCallback] ################################################# @@ -115,12 +115,13 @@ proc listConversations*(client: Client): seq[Conversation] = # Callback Handling ################################################# -proc onNewMessage*(client: Client, callback: MessageCallback[string]) = +proc onNewMessage*(client: Client, callback: MessageCallback[ContentFrame]) = client.newMessageCallbacks.add(callback) -proc notifyNewMessage(client: Client, convo: Conversation, msg: string) = +proc notifyNewMessage(client: Client, convo: Conversation, + content: ContentFrame) = for cb in client.newMessageCallbacks: - discard cb(convo, msg) + discard cb(convo, content) proc onNewConversation*(client: Client, callback: NewConvoCallback) = client.newConvoCallbacks.add(callback) diff --git a/src/chat_sdk/conversation_store.nim b/src/chat_sdk/conversation_store.nim index b887f6a..38383f6 100644 --- a/src/chat_sdk/conversation_store.nim +++ b/src/chat_sdk/conversation_store.nim @@ -3,6 +3,7 @@ import std/[options, times] import ./conversations/convo_type import crypto import identity +import proto_types type ConvoId = string @@ -13,4 +14,5 @@ type proc identity(self: Self): Identity proc getId(self: Self): string - proc notifyNewMessage(self: Self, convo: Conversation, msg: string) + proc notifyNewMessage(self: Self, convo: Conversation, + content: ContentFrame) diff --git a/src/chat_sdk/conversations/convo_type.nim b/src/chat_sdk/conversations/convo_type.nim index 869007c..66caf05 100644 --- a/src/chat_sdk/conversations/convo_type.nim +++ b/src/chat_sdk/conversations/convo_type.nim @@ -2,6 +2,7 @@ import chronos import strformat import strutils +import ../proto_types import ../delivery/waku_client import ../utils @@ -24,6 +25,6 @@ method id*(self: Conversation): string {.raises: [Defect, ValueError].} = panic("ProgramError: Missing concrete implementation") method sendMessage*(convo: Conversation, ds: WakuClient, - text: string) {.async, base, gcsafe.} = + content_frame: ContentFrame) {.async, base, gcsafe.} = # TODO: make this a compile time check panic("ProgramError: Missing concrete implementation") diff --git a/src/chat_sdk/conversations/private_v1.nim b/src/chat_sdk/conversations/private_v1.nim index 1c5e663..6ee2e99 100644 --- a/src/chat_sdk/conversations/private_v1.nim +++ b/src/chat_sdk/conversations/private_v1.nim @@ -91,17 +91,18 @@ proc handleFrame*[T: ConversationStore](convo: PrivateV1, client: T, case frame.getKind(): of typeContentFrame: # TODO: Using client.getId() results in an error in this context - client.notifyNewMessage(convo, toUtfString(frame.content.bytes)) + client.notifyNewMessage(convo, frame.content) of typePlaceholder: notice "Got Placeholder", text = frame.placeholder.counter -method sendMessage*(convo: PrivateV1, ds: WakuClient, text: string) {.async.} = +method sendMessage*(convo: PrivateV1, ds: WakuClient, + content_frame: ContentFrame) {.async.} = try: let frame = PrivateV1Frame(sender: @(convo.owner.getPubkey().bytes()), - content: ContentFrame(domain: 0, tag: 1, bytes: text.toBytes())) + content: content_frame) await convo.sendFrame(ds, frame) except Exception as e: diff --git a/src/chat_sdk/inbox.nim b/src/chat_sdk/inbox.nim index b809368..7ede48b 100644 --- a/src/chat_sdk/inbox.nim +++ b/src/chat_sdk/inbox.nim @@ -99,5 +99,6 @@ proc handleFrame*[T: ConversationStore](convo: Inbox, client: T, bytes: seq[ notice "Receive Note", client = client.getId(), text = frame.note.text -method sendMessage*(convo: Inbox, ds: WakuClient, text: string) {.async.} = +method sendMessage*(convo: Inbox, ds: WakuClient, + content_frame: ContentFrame) {.async.} = warn "Cannot send message to Inbox" diff --git a/src/content_types/all.nim b/src/content_types/all.nim new file mode 100644 index 0000000..5d8be0e --- /dev/null +++ b/src/content_types/all.nim @@ -0,0 +1,55 @@ +# Can this be an external package? It would be preferable to have these types +# easy to import and use. + +import protobuf_serialization # This import is needed or th macro will not work +import protobuf_serialization/proto_parser +import results +import std/random +import strformat + +import ../chat_sdk/proto_types + + +export protobuf_serialization + +import_proto3 "protos/text_frame.proto" +# import_proto3 "../../protos/common_frames.proto" + +export ContentFrame, TextFrame + +type ContentTypes = TextFrame + +# protobuf_serialization does not support enums, so it needs to be manually implemented +type + TextEncoding* = enum + Utf8 = 0 + + + +proc encode*(frame: object): seq[byte] = + ## Encodes the frame into a byte sequence using Protobuf serialization. + result = Protobuf.encode(frame) + + +proc decode*[T: object] (bytes: seq[byte], proto: typedesc[ + T]): Result[T, string] = + ## Encodes the frame into a byte sequence using Protobuf serialization. + + try: + result = ok(Protobuf.decode(bytes, proto)) + except ProtobufError as e: + result = err("Failed to decode payload: " & e.msg) + + +proc toContentFrame*(frame: TextFrame): ContentFrame = + result = ContentFrame(domain: 0, tag: 0, bytes: encode(frame)) + + +proc initTextFrame*(text: string): TextFrame = + result = TextFrame(encoding: ord(Utf8), text: text) + + +proc `$`*(frame: TextFrame): string = + + result = fmt"TextFrame(encoding:{TextEncoding(frame.encoding)} text:{frame.text})" + diff --git a/src/content_types/protos/text_frame.proto b/src/content_types/protos/text_frame.proto new file mode 100644 index 0000000..80b34c5 --- /dev/null +++ b/src/content_types/protos/text_frame.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package wap.content_types; + + +message TextFrame { + uint32 encoding = 1; + string text = 2; +} diff --git a/src/nim_chat_poc.nim b/src/nim_chat_poc.nim index adb7d6b..cde730a 100644 --- a/src/nim_chat_poc.nim +++ b/src/nim_chat_poc.nim @@ -7,6 +7,24 @@ import chat_sdk/conversations import chat_sdk/delivery/waku_client import chat_sdk/utils +import content_types/all + + +const SELF_DEFINED = 99 + +type ImageFrame {.proto3.} = object + url {.fieldNumber: 1.}: string + altText {.fieldNumber: 2.}: string + + +proc initImage(url: string): ContentFrame = + result = ContentFrame(domain: SELF_DEFINED, tag: 0, bytes: encode(ImageFrame( + url: url, altText: "This is an image"))) + +proc `$`*(frame: ImageFrame): string = + result = fmt"ImageFrame(url:{frame.url} alt_text:{frame.altText})" + + proc initLogging() = when defined(chronicles_runtime_filtering): setLogLevel(LogLevel.Debug) @@ -14,6 +32,25 @@ proc initLogging() = discard setTopicState("waku relay", chronicles.Normal, LogLevel.Error) discard setTopicState("chat client", chronicles.Enabled, LogLevel.Debug) +proc getContent(content: ContentFrame): string = + notice "GetContent", domain = content.domain, tag = content.tag + + # TODO: Hide this complexity from developers + if content.domain == 0: + if content.tag == 0: + let m = decode(content.bytes, TextFrame).valueOr: + raise newException(ValueError, fmt"Badly formed Content (domain:{content.domain} tag:{content.tag})") + return fmt"{m}" + + if content.domain == SELF_DEFINED: + if content.tag == 0: + let m = decode(content.bytes, ImageFrame).valueOr: + raise newException(ValueError, fmt"Badly formed Content (domain:{content.domain} tag:{content.tag})") + return fmt"{m}" + + raise newException(ValueError, fmt"Unhandled content (domain:{content.domain} tag:{content.tag})") + + proc main() {.async.} = # Create Configurations @@ -29,22 +66,24 @@ proc main() {.async.} = # Start Clients var saro = newClient("Saro", cfg_saro) - saro.onNewMessage(proc(convo: Conversation, msg: string) {.async.} = - echo " Saro <------ :: " & msg + saro.onNewMessage(proc(convo: Conversation, msg: ContentFrame) {.async.} = + echo " Saro <------ :: " & getContent(msg) await sleepAsync(10000) - await convo.sendMessage(saro.ds, "Ping")) + await convo.sendMessage(saro.ds, initImage( + "https://waku.org/theme/image/logo-black.svg")) + ) await saro.start() var raya = newClient("Raya", cfg_raya) - raya.onNewMessage(proc(convo: Conversation, msg: string) {.async.} = - echo " ------> Raya :: " & msg + raya.onNewMessage(proc(convo: Conversation, msg: ContentFrame) {.async.} = + echo " ------> Raya :: " & getContent(msg) await sleepAsync(10000) - await convo.sendMessage(raya.ds, "Pong") + await convo.sendMessage(raya.ds, initTextFrame("Pong").toContentFrame()) ) raya.onNewConversation(proc(convo: Conversation) {.async.} = echo " ------> Raya :: New Conversation: " & convo.id() - await convo.sendMessage(raya.ds, "Hello") + await convo.sendMessage(raya.ds, initTextFrame("Hello").toContentFrame()) ) await raya.start()