mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-04-02 01:23:11 +00:00
Merge cdd58ec7f4f01db8ac439f1c2ab2a3f186d14eae into 8cddd9ddcfb446deeff96fd5a68d6e4b14927d9f
This commit is contained in:
commit
41db10fa19
4
.gitignore
vendored
4
.gitignore
vendored
@ -30,4 +30,8 @@ target
|
|||||||
# Temporary data folder
|
# Temporary data folder
|
||||||
tmp
|
tmp
|
||||||
|
|
||||||
|
# Auto-generated C headers (regenerated by flake.nix build)
|
||||||
|
libchat.h
|
||||||
|
double_ratchet.h
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@ -4,7 +4,14 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["rlib","staticlib","dylib"]
|
crate-type = ["rlib","staticlib","cdylib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
headers = ["safer-ffi/headers"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "generate-libchat-headers"
|
||||||
|
required-features = ["headers"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|||||||
@ -47,11 +47,13 @@ pub struct ContextHandle(pub(crate) Context);
|
|||||||
/// Creates a new libchat Ctx
|
/// Creates a new libchat Ctx
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// Opaque handle to the store. Must be freed with destroy_context()
|
/// Opaque handle to the store. Must be freed with destroy_context().
|
||||||
|
/// Uses lossy UTF-8 conversion: invalid bytes are replaced with U+FFFD
|
||||||
|
/// so the caller always gets a deterministic name reflecting their input.
|
||||||
#[ffi_export]
|
#[ffi_export]
|
||||||
pub fn create_context(name: repr_c::String) -> repr_c::Box<ContextHandle> {
|
pub fn create_context(name: c_slice::Ref<'_, u8>) -> repr_c::Box<ContextHandle> {
|
||||||
// Deference name to to `str` and then borrow to &str
|
let name_str = std::string::String::from_utf8_lossy(name.as_slice()).into_owned();
|
||||||
Box::new(ContextHandle(Context::new_with_name(&*name))).into()
|
Box::new(ContextHandle(Context::new_with_name(name_str))).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the friendly name of the contexts installation.
|
/// Returns the friendly name of the contexts installation.
|
||||||
@ -61,6 +63,13 @@ pub fn installation_name(ctx: &ContextHandle) -> repr_c::String {
|
|||||||
ctx.0.installation_name().to_string().into()
|
ctx.0.installation_name().to_string().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the local delivery address (hex-encoded Blake2b-512 of the installation public key).
|
||||||
|
/// This is the address other installations use to send messages to this context.
|
||||||
|
#[ffi_export]
|
||||||
|
pub fn local_delivery_address(ctx: &ContextHandle) -> repr_c::String {
|
||||||
|
ctx.0.local_delivery_address().to_string().into()
|
||||||
|
}
|
||||||
|
|
||||||
/// Destroys a conversation store and frees its memory
|
/// Destroys a conversation store and frees its memory
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
@ -182,11 +191,21 @@ pub fn list_conversations(ctx: &mut ContextHandle) -> ListConvoResult {
|
|||||||
#[ffi_export]
|
#[ffi_export]
|
||||||
pub fn send_content(
|
pub fn send_content(
|
||||||
ctx: &mut ContextHandle,
|
ctx: &mut ContextHandle,
|
||||||
convo_id: repr_c::String,
|
convo_id: c_slice::Ref<'_, u8>,
|
||||||
content: c_slice::Ref<'_, u8>,
|
content: c_slice::Ref<'_, u8>,
|
||||||
out: &mut SendContentResult,
|
out: &mut SendContentResult,
|
||||||
) {
|
) {
|
||||||
let payloads = match ctx.0.send_content(&convo_id, &content) {
|
let convo_id_str = match std::str::from_utf8(convo_id.as_slice()) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => {
|
||||||
|
*out = SendContentResult {
|
||||||
|
error_code: ErrorCode::BadConvoId as i32,
|
||||||
|
payloads: safer_ffi::Vec::EMPTY,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let payloads = match ctx.0.send_content(convo_id_str, &content) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
*out = SendContentResult {
|
*out = SendContentResult {
|
||||||
|
|||||||
3
core/conversations/src/bin/generate-libchat-headers.rs
Normal file
3
core/conversations/src/bin/generate-libchat-headers.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
libchat::generate_headers()
|
||||||
|
}
|
||||||
@ -65,6 +65,10 @@ impl Context {
|
|||||||
self._identity.get_name()
|
self._identity.get_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn local_delivery_address(&self) -> &str {
|
||||||
|
self.inbox.id()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_private_convo(
|
pub fn create_private_convo(
|
||||||
&mut self,
|
&mut self,
|
||||||
remote_bundle: &Introduction,
|
remote_bundle: &Introduction,
|
||||||
|
|||||||
@ -55,11 +55,16 @@ impl BaseConvoId {
|
|||||||
pub struct PrivateV1Convo {
|
pub struct PrivateV1Convo {
|
||||||
local_convo_id: String,
|
local_convo_id: String,
|
||||||
remote_convo_id: String,
|
remote_convo_id: String,
|
||||||
|
remote_delivery_address: String,
|
||||||
dr_state: RatchetState,
|
dr_state: RatchetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrivateV1Convo {
|
impl PrivateV1Convo {
|
||||||
pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey) -> Self {
|
pub fn new_initiator(
|
||||||
|
seed_key: SymmetricKey32,
|
||||||
|
remote: PublicKey,
|
||||||
|
remote_delivery_address: String,
|
||||||
|
) -> Self {
|
||||||
let base_convo_id = BaseConvoId::new(&seed_key);
|
let base_convo_id = BaseConvoId::new(&seed_key);
|
||||||
let local_convo_id = base_convo_id.id_for_participant(Role::Initiator);
|
let local_convo_id = base_convo_id.id_for_participant(Role::Initiator);
|
||||||
let remote_convo_id = base_convo_id.id_for_participant(Role::Responder);
|
let remote_convo_id = base_convo_id.id_for_participant(Role::Responder);
|
||||||
@ -73,11 +78,16 @@ impl PrivateV1Convo {
|
|||||||
Self {
|
Self {
|
||||||
local_convo_id,
|
local_convo_id,
|
||||||
remote_convo_id,
|
remote_convo_id,
|
||||||
|
remote_delivery_address,
|
||||||
dr_state,
|
dr_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey) -> Self {
|
pub fn new_responder(
|
||||||
|
seed_key: SymmetricKey32,
|
||||||
|
dh_self: &PrivateKey,
|
||||||
|
remote_delivery_address: String,
|
||||||
|
) -> Self {
|
||||||
let base_convo_id = BaseConvoId::new(&seed_key);
|
let base_convo_id = BaseConvoId::new(&seed_key);
|
||||||
let local_convo_id = base_convo_id.id_for_participant(Role::Responder);
|
let local_convo_id = base_convo_id.id_for_participant(Role::Responder);
|
||||||
let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator);
|
let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator);
|
||||||
@ -92,6 +102,7 @@ impl PrivateV1Convo {
|
|||||||
Self {
|
Self {
|
||||||
local_convo_id,
|
local_convo_id,
|
||||||
remote_convo_id,
|
remote_convo_id,
|
||||||
|
remote_delivery_address,
|
||||||
dr_state,
|
dr_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,7 +190,7 @@ impl Convo for PrivateV1Convo {
|
|||||||
let data = self.encrypt(frame);
|
let data = self.encrypt(frame);
|
||||||
|
|
||||||
Ok(vec![AddressedEncryptedPayload {
|
Ok(vec![AddressedEncryptedPayload {
|
||||||
delivery_address: "delivery_address".into(),
|
delivery_address: self.remote_delivery_address.clone(),
|
||||||
data,
|
data,
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
@ -236,8 +247,9 @@ mod tests {
|
|||||||
let seed_key_saro = SymmetricKey32::from(seed_key);
|
let seed_key_saro = SymmetricKey32::from(seed_key);
|
||||||
let seed_key_raya = SymmetricKey32::from(seed_key);
|
let seed_key_raya = SymmetricKey32::from(seed_key);
|
||||||
let send_content_bytes = vec![0, 2, 4, 6, 8];
|
let send_content_bytes = vec![0, 2, 4, 6, 8];
|
||||||
let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya);
|
let mut sr_convo =
|
||||||
let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya);
|
PrivateV1Convo::new_initiator(seed_key_saro, pub_raya, "test_addr".into());
|
||||||
|
let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya, "test_addr".into());
|
||||||
|
|
||||||
let send_frame = PrivateV1Frame {
|
let send_frame = PrivateV1Frame {
|
||||||
conversation_id: "_".into(),
|
conversation_id: "_".into(),
|
||||||
|
|||||||
@ -16,12 +16,6 @@ use crate::inbox::handshake::InboxHandshake;
|
|||||||
use crate::proto;
|
use crate::proto;
|
||||||
use crate::types::{AddressedEncryptedPayload, ContentData};
|
use crate::types::{AddressedEncryptedPayload, ContentData};
|
||||||
|
|
||||||
/// Compute the deterministic Delivery_address for an installation
|
|
||||||
fn delivery_address_for_installation(_: PublicKey) -> String {
|
|
||||||
// TODO: Implement Delivery Address
|
|
||||||
"delivery_address".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Inbox {
|
pub struct Inbox {
|
||||||
ident: Rc<Identity>,
|
ident: Rc<Identity>,
|
||||||
local_convo_id: String,
|
local_convo_id: String,
|
||||||
@ -78,13 +72,17 @@ impl Inbox {
|
|||||||
let (seed_key, ephemeral_pub) =
|
let (seed_key, ephemeral_pub) =
|
||||||
InboxHandshake::perform_as_initiator(self.ident.secret(), &pkb, &mut rng);
|
InboxHandshake::perform_as_initiator(self.ident.secret(), &pkb, &mut rng);
|
||||||
|
|
||||||
let mut convo = PrivateV1Convo::new_initiator(seed_key, *remote_bundle.ephemeral_key());
|
let remote_delivery_addr =
|
||||||
|
Inbox::inbox_identifier_for_key(*remote_bundle.installation_key());
|
||||||
|
let mut convo = PrivateV1Convo::new_initiator(
|
||||||
|
seed_key,
|
||||||
|
*remote_bundle.ephemeral_key(),
|
||||||
|
remote_delivery_addr.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let mut payloads = convo.send_message(initial_message)?;
|
let mut payloads = convo.send_message(initial_message)?;
|
||||||
|
|
||||||
// Wrap First payload in Invite
|
|
||||||
if let Some(first_message) = payloads.get_mut(0) {
|
if let Some(first_message) = payloads.get_mut(0) {
|
||||||
// Take the the value of .data - it's being replaced at the end of this block
|
|
||||||
let frame = Self::wrap_in_invite(std::mem::take(&mut first_message.data));
|
let frame = Self::wrap_in_invite(std::mem::take(&mut first_message.data));
|
||||||
|
|
||||||
// TODO: Encrypt frame
|
// TODO: Encrypt frame
|
||||||
@ -102,10 +100,7 @@ impl Inbox {
|
|||||||
payload: Bytes::from_owner(ciphertext),
|
payload: Bytes::from_owner(ciphertext),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the address field with the Inbox delivery_Address
|
first_message.delivery_address = remote_delivery_addr;
|
||||||
first_message.delivery_address =
|
|
||||||
delivery_address_for_installation(*remote_bundle.installation_key());
|
|
||||||
// Update the data field with new Payload
|
|
||||||
first_message.data = proto::EncryptedPayload {
|
first_message.data = proto::EncryptedPayload {
|
||||||
encryption: Some(proto::Encryption::InboxHandshake(handshake)),
|
encryption: Some(proto::Encryption::InboxHandshake(handshake)),
|
||||||
};
|
};
|
||||||
@ -128,12 +123,22 @@ impl Inbox {
|
|||||||
let key_index = hex::encode(header.responder_ephemeral.as_ref());
|
let key_index = hex::encode(header.responder_ephemeral.as_ref());
|
||||||
let ephemeral_key = self.lookup_ephemeral_key(&key_index)?;
|
let ephemeral_key = self.lookup_ephemeral_key(&key_index)?;
|
||||||
|
|
||||||
|
// Extract initiator's identity key for delivery address before header is consumed
|
||||||
|
let initiator_static_bytes: [u8; 32] = header
|
||||||
|
.initiator_static
|
||||||
|
.as_ref()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ChatError::BadBundleValue("wrong size - initiator static".into()))?;
|
||||||
|
let remote_delivery_addr =
|
||||||
|
Inbox::inbox_identifier_for_key(PublicKey::from(initiator_static_bytes));
|
||||||
|
|
||||||
// Perform handshake and decrypt frame
|
// Perform handshake and decrypt frame
|
||||||
let (seed_key, frame) = self.perform_handshake(ephemeral_key, header, handshake.payload)?;
|
let (seed_key, frame) = self.perform_handshake(ephemeral_key, header, handshake.payload)?;
|
||||||
|
|
||||||
match frame.frame_type.unwrap() {
|
match frame.frame_type.unwrap() {
|
||||||
proto::inbox_v1_frame::FrameType::InvitePrivateV1(_invite_private_v1) => {
|
proto::inbox_v1_frame::FrameType::InvitePrivateV1(_invite_private_v1) => {
|
||||||
let mut convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key);
|
let mut convo =
|
||||||
|
PrivateV1Convo::new_responder(seed_key, ephemeral_key, remote_delivery_addr);
|
||||||
|
|
||||||
let Some(enc_payload) = _invite_private_v1.initial_message else {
|
let Some(enc_payload) = _invite_private_v1.initial_message else {
|
||||||
return Err(ChatError::Protocol("missing initial encpayload".into()));
|
return Err(ChatError::Protocol("missing initial encpayload".into()));
|
||||||
|
|||||||
@ -14,6 +14,13 @@ pub use api::*;
|
|||||||
pub use context::{Context, Introduction};
|
pub use context::{Context, Introduction};
|
||||||
pub use errors::ChatError;
|
pub use errors::ChatError;
|
||||||
|
|
||||||
|
#[cfg(feature = "headers")]
|
||||||
|
pub fn generate_headers() -> std::io::Result<()> {
|
||||||
|
safer_ffi::headers::builder()
|
||||||
|
.to_file("libchat.h")?
|
||||||
|
.generate()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
@ -24,8 +31,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_message_roundtrip() {
|
fn test_message_roundtrip() {
|
||||||
let mut saro = create_context("saro".into());
|
let mut saro = create_context(b"saro".as_slice().into());
|
||||||
let mut raya = create_context("raya".into());
|
let mut raya = create_context(b"raya".as_slice().into());
|
||||||
|
|
||||||
// Raya Creates Bundle and Sends to Saro
|
// Raya Creates Bundle and Sends to Saro
|
||||||
let mut intro_result = CreateIntroResult {
|
let mut intro_result = CreateIntroResult {
|
||||||
|
|||||||
101
flake.nix
Normal file
101
flake.nix
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
description = "libchat - Logos Chat cryptographic library";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
rust-overlay = {
|
||||||
|
url = "github:oxalica/rust-overlay";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, rust-overlay }:
|
||||||
|
let
|
||||||
|
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ rust-overlay.overlays.default ];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems ({ pkgs }:
|
||||||
|
let
|
||||||
|
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml;
|
||||||
|
rustPlatform = pkgs.makeRustPlatform {
|
||||||
|
cargo = rustToolchain;
|
||||||
|
rustc = rustToolchain;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = rustPlatform.buildRustPackage {
|
||||||
|
pname = "libchat";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = pkgs.lib.cleanSourceWith {
|
||||||
|
src = ./.;
|
||||||
|
filter = path: type:
|
||||||
|
let base = builtins.baseNameOf path;
|
||||||
|
in !(builtins.elem base [ "target" "nim-bindings" ".git" ".github" "tmp" ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ./Cargo.lock;
|
||||||
|
outputHashes = {
|
||||||
|
"chat-proto-0.1.0" = "sha256-aCl80VOIkd/GK3gnmRuFoSAvPBfeE/FKCaNlLt5AbUU=";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# perl: required by openssl-sys (transitive dep)
|
||||||
|
nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.cmake ];
|
||||||
|
doCheck = false; # tests require network access unavailable in nix sandbox
|
||||||
|
|
||||||
|
postBuild = ''
|
||||||
|
cargo run --frozen --release --bin generate-libchat-headers --features headers
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out/lib $out/include
|
||||||
|
|
||||||
|
# Copy shared library (platform-dependent extension)
|
||||||
|
cp target/release/liblibchat.so $out/lib/ 2>/dev/null || true
|
||||||
|
cp target/release/liblibchat.dylib $out/lib/ 2>/dev/null || true
|
||||||
|
cp target/release/liblibchat.a $out/lib/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Fail if no library was produced
|
||||||
|
if [ -z "$(ls $out/lib/liblibchat.* 2>/dev/null)" ]; then
|
||||||
|
echo "ERROR: No library artifact found in target/release/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy generated header
|
||||||
|
cp libchat.h $out/include/
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Logos Chat cryptographic library (C FFI)";
|
||||||
|
platforms = platforms.unix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
devShells = forAllSystems ({ pkgs }:
|
||||||
|
let
|
||||||
|
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = [
|
||||||
|
rustToolchain
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.cmake
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -85,7 +85,7 @@ type
|
|||||||
|
|
||||||
## Creates a new libchat Context
|
## Creates a new libchat Context
|
||||||
## Returns: Opaque handle to the context. Must be freed with destroy_context()
|
## Returns: Opaque handle to the context. Must be freed with destroy_context()
|
||||||
proc create_context*(name: ReprCString): ContextHandle {.importc.}
|
proc create_context*(name: SliceUint8): ContextHandle {.importc.}
|
||||||
|
|
||||||
## Returns the friendly name of the context's identity
|
## Returns the friendly name of the context's identity
|
||||||
## The result must be freed by the caller (repr_c::String ownership transfers)
|
## The result must be freed by the caller (repr_c::String ownership transfers)
|
||||||
@ -129,7 +129,7 @@ proc list_conversations*(
|
|||||||
## The result must be freed with destroy_send_content_result()
|
## The result must be freed with destroy_send_content_result()
|
||||||
proc send_content*(
|
proc send_content*(
|
||||||
ctx: ContextHandle,
|
ctx: ContextHandle,
|
||||||
convo_id: ReprCString,
|
convo_id: SliceUint8,
|
||||||
content: SliceUint8,
|
content: SliceUint8,
|
||||||
): SendContentResult {.importc.}
|
): SendContentResult {.importc.}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ type
|
|||||||
## Create a new conversations context
|
## Create a new conversations context
|
||||||
proc newConversationsContext*(name: string): LibChat =
|
proc newConversationsContext*(name: string): LibChat =
|
||||||
|
|
||||||
result.handle = create_context(name.toReprCString)
|
result.handle = create_context(name.toSlice)
|
||||||
result.buffer_size = 256
|
result.buffer_size = 256
|
||||||
if result.handle.isNil:
|
if result.handle.isNil:
|
||||||
raise newException(IOError, "Failed to create context")
|
raise newException(IOError, "Failed to create context")
|
||||||
@ -95,7 +95,7 @@ proc sendContent*(ctx: LibChat, convoId: string, content: seq[byte]): Result[seq
|
|||||||
if content.len == 0:
|
if content.len == 0:
|
||||||
return err("content is zero length")
|
return err("content is zero length")
|
||||||
|
|
||||||
let res = bindings.send_content(ctx.handle, convoId.toReprCString, content.toSlice())
|
let res = bindings.send_content(ctx.handle, convoId.toSlice, content.toSlice())
|
||||||
defer: destroy_send_content_result(res)
|
defer: destroy_send_content_result(res)
|
||||||
|
|
||||||
if res.error_code != 0:
|
if res.error_code != 0:
|
||||||
|
|||||||
@ -72,7 +72,7 @@ proc testHelperProcs() =
|
|||||||
proc testContextLifecycle() =
|
proc testContextLifecycle() =
|
||||||
echo "\n--- testContextLifecycle ---"
|
echo "\n--- testContextLifecycle ---"
|
||||||
|
|
||||||
let ctx = create_context(toReprCString("lifecycle-test"))
|
let ctx = create_context(toSlice("lifecycle-test"))
|
||||||
check(ctx != nil, "create_context: returns non-nil handle")
|
check(ctx != nil, "create_context: returns non-nil handle")
|
||||||
|
|
||||||
let iname = installation_name(ctx)
|
let iname = installation_name(ctx)
|
||||||
@ -94,10 +94,10 @@ proc testContextLifecycle() =
|
|||||||
proc testFullConversationFlow() =
|
proc testFullConversationFlow() =
|
||||||
echo "\n--- testFullConversationFlow ---"
|
echo "\n--- testFullConversationFlow ---"
|
||||||
|
|
||||||
let aliceCtx = create_context(toReprCString("alice"))
|
let aliceCtx = create_context(toSlice("alice"))
|
||||||
check(aliceCtx != nil, "Alice: create_context non-nil")
|
check(aliceCtx != nil, "Alice: create_context non-nil")
|
||||||
|
|
||||||
let bobCtx = create_context(toReprCString("bob"))
|
let bobCtx = create_context(toSlice("bob"))
|
||||||
check(bobCtx != nil, "Bob: create_context non-nil")
|
check(bobCtx != nil, "Bob: create_context non-nil")
|
||||||
|
|
||||||
# --- create_intro_bundle ---
|
# --- create_intro_bundle ---
|
||||||
@ -177,7 +177,7 @@ proc testFullConversationFlow() =
|
|||||||
# --- send_content ---
|
# --- send_content ---
|
||||||
var sendRes = send_content(
|
var sendRes = send_content(
|
||||||
aliceCtx,
|
aliceCtx,
|
||||||
toReprCString(aliceConvoId),
|
toSlice(aliceConvoId),
|
||||||
toSlice("How are you, Bob?")
|
toSlice("How are you, Bob?")
|
||||||
)
|
)
|
||||||
check(sendRes.error_code == ErrNone,
|
check(sendRes.error_code == ErrNone,
|
||||||
@ -213,13 +213,13 @@ proc testFullConversationFlow() =
|
|||||||
proc testErrorCases() =
|
proc testErrorCases() =
|
||||||
echo "\n--- testErrorCases ---"
|
echo "\n--- testErrorCases ---"
|
||||||
|
|
||||||
let ctx = create_context(toReprCString("error-tester"))
|
let ctx = create_context(toSlice("error-tester"))
|
||||||
check(ctx != nil, "error-tester: create_context non-nil")
|
check(ctx != nil, "error-tester: create_context non-nil")
|
||||||
|
|
||||||
# send_content with a nonexistent convo_id must fail
|
# send_content with a nonexistent convo_id must fail
|
||||||
var badSend = send_content(
|
var badSend = send_content(
|
||||||
ctx,
|
ctx,
|
||||||
toReprCString("00000000-0000-0000-0000-nonexistent"),
|
toSlice("00000000-0000-0000-0000-nonexistent"),
|
||||||
toSlice("payload")
|
toSlice("payload")
|
||||||
)
|
)
|
||||||
check(badSend.error_code != ErrNone,
|
check(badSend.error_code != ErrNone,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user