logos-chat/src/chat/inbox.nim
Jazz Turner-Baggs d7af4e09ec
Update Examples (#40)
* Update examples.

* Update readme
2025-12-16 19:46:19 -08:00

172 lines
5.5 KiB
Nim

import std/[strutils]
import
chronicles,
chronos,
results,
strformat
import
conversations/convo_type,
conversations,
conversation_store,
crypto,
delivery/waku_client,
errors,
identity,
proto_types,
types
logScope:
topics = "chat inbox"
type
Inbox* = ref object of Conversation
identity: Identity
inbox_addr: string
const
TopicPrefixInbox = "/inbox/"
proc `$`*(conv: Inbox): string =
fmt"Inbox: addr->{conv.inbox_addr}"
proc initInbox*(ident: Identity): Inbox =
## Initializes an Inbox object with the given address and invite callback.
return Inbox(identity: ident)
proc encrypt*(frame: InboxV1Frame): EncryptedPayload =
return encrypt_plain(frame)
proc decrypt*(inbox: Inbox, encbytes: EncryptedPayload): Result[InboxV1Frame, string] =
let res_frame = decrypt_plain(encbytes.plaintext, InboxV1Frame)
if res_frame.isErr:
error "Failed to decrypt frame: ", err = res_frame.error
return err("Failed to decrypt frame: " & res_frame.error)
result = res_frame
proc wrap_env*(payload: EncryptedPayload, convo_id: string): WapEnvelopeV1 =
let bytes = encode(payload)
let salt = generateSalt()
return WapEnvelopeV1(
payload: bytes,
salt: salt,
conversation_hint: convo_id,
)
proc conversation_id_for*(pubkey: PublicKey): string =
## Generates a conversation ID based on the public key.
return "/convo/inbox/v1/" & pubkey.get_addr()
# TODO derive this from instance of Inbox
proc topic_inbox*(client_addr: string): string =
return TopicPrefixInbox & client_addr
proc parseTopic*(topic: string): Result[string, ChatError] =
if not topic.startsWith(TopicPrefixInbox):
return err(ChatError(code: errTopic, context: "Invalid inbox topic prefix"))
let id = topic.split('/')[^1]
if id == "":
return err(ChatError(code: errTopic, context: "Empty inbox id"))
return ok(id)
method id*(convo: Inbox): string =
return conversation_id_for(convo.identity.getPubkey())
## Encrypt and Send a frame to the remote account
proc sendFrame(ds: WakuClient, remote: PublicKey, frame: InboxV1Frame ): Future[void] {.async.} =
let env = wrapEnv(encrypt(frame),conversation_id_for(remote) )
await ds.sendPayload(topic_inbox(remote.get_addr()), env)
proc newPrivateInvite(initator_static: PublicKey,
initator_ephemeral: PublicKey,
recipient_static: PublicKey,
recipient_ephemeral: uint32,
payload: EncryptedPayload) : InboxV1Frame =
let invite = InvitePrivateV1(
initiator: @(initator_static.bytes()),
initiatorEphemeral: @(initator_ephemeral.bytes()),
participant: @(recipient_static.bytes()),
participantEphemeralId: 0,
discriminator: "",
initial_message: payload
)
result = InboxV1Frame(invitePrivateV1: invite, recipient: "")
#################################################
# Conversation Creation
#################################################
## Establish a PrivateConversation with a remote client
proc inviteToPrivateConversation*(self: Inbox, ds: Wakuclient, remote_static: PublicKey, remote_ephemeral: PublicKey, content: Content ) : Future[PrivateV1] {.async.} =
# Create SeedKey
# TODO: Update key derivations when noise is integrated
var local_ephemeral = generateKey()
var sk{.noInit.} : array[32, byte] = default(array[32, byte])
# Initialize PrivateConversation
let (convo, encPayload) = initPrivateV1Sender(self.identity, ds, remote_static, sk, content, nil)
result = convo
# # Build Invite
let frame = newPrivateInvite(self.identity.getPubkey(), local_ephemeral.getPublicKey(), remote_static, 0, encPayload)
# Send
await sendFrame(ds, remote_static, frame)
## Receive am Invitation to create a new private conversation
proc createPrivateV1FromInvite*[T: ConversationStore](client: T,
invite: InvitePrivateV1) =
let destPubkey = loadPublicKeyFromBytes(invite.initiator).valueOr:
raise newException(ValueError, "Invalid public key in intro bundle.")
let deliveryAckCb = proc(
conversation: Conversation,
msgId: string): Future[void] {.async.} =
client.notifyDeliveryAck(conversation, msgId)
# TODO: remove placeholder key
var key : array[32, byte] = default(array[32,byte])
let convo = initPrivateV1Recipient(client.identity(), client.ds, destPubkey, key, deliveryAckCb)
notice "Creating PrivateV1 conversation", client = client.getId(),
convoId = convo.getConvoId()
convo.handleFrame(client, invite.initial_message)
# Calling `addConversation` must only occur after the conversation is completely configured.
# The client calls the OnNewConversation callback, which returns execution to the application.
client.addConversation(convo)
proc handleFrame*[T: ConversationStore](convo: Inbox, client: T, bytes: seq[
byte]) =
## Dispatcher for Incoming `InboxV1Frames`.
## Calls further processing depending on the kind of frame.
let enc = decode(bytes, EncryptedPayload).valueOr:
raise newException(ValueError, "Failed to decode payload")
let frame = convo.decrypt(enc).valueOr:
error "Decrypt failed", client = client.getId(), error = error
raise newException(ValueError, "Failed to Decrypt MEssage: " &
error)
case getKind(frame):
of typeInvitePrivateV1:
createPrivateV1FromInvite(client, frame.invitePrivateV1)
of typeNote:
notice "Receive Note", client = client.getId(), text = frame.note.text
method sendMessage*(convo: Inbox, content_frame: Content) : Future[MessageId] {.async.} =
warn "Cannot send message to Inbox"
result = "program_error"