From 058467771e9ee05a42654f668dc4dcef09a17606 Mon Sep 17 00:00:00 2001 From: pablo Date: Mon, 22 Dec 2025 14:14:37 +0200 Subject: [PATCH 1/4] feat: libchat C library building and example TUI using it --- .gitignore | 2 + .gitmodules | 5 + Makefile | 22 + config.nims | 7 + examples/cbindings/.clang-format | 6 + examples/cbindings/Makefile | 39 ++ examples/cbindings/README.md | 59 ++ examples/cbindings/cbindings_chat_tui.c | 738 ++++++++++++++++++++++++ library/api/client_api.nim | 156 +++++ library/api/conversation_api.nim | 78 +++ library/api/identity_api.nim | 54 ++ library/declare_lib.nim | 13 + library/libchat.h | 107 ++++ library/libchat.nim | 98 ++++ library/nim.cfg | 1 + library/utils.nim | 114 ++++ nim_chat_poc.nimble | 27 +- 17 files changed, 1524 insertions(+), 2 deletions(-) create mode 100644 config.nims create mode 100644 examples/cbindings/.clang-format create mode 100644 examples/cbindings/Makefile create mode 100644 examples/cbindings/README.md create mode 100644 examples/cbindings/cbindings_chat_tui.c create mode 100644 library/api/client_api.nim create mode 100644 library/api/conversation_api.nim create mode 100644 library/api/identity_api.nim create mode 100644 library/declare_lib.nim create mode 100644 library/libchat.h create mode 100644 library/libchat.nim create mode 100644 library/nim.cfg create mode 100644 library/utils.nim diff --git a/.gitignore b/.gitignore index 5a3358e..7fe05ef 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,5 @@ result # App data .registry/ .savedkeys/ + +AGENTS.md diff --git a/.gitmodules b/.gitmodules index 44e4b4a..f1cc4cf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -41,3 +41,8 @@ url = https://github.com/mratsim/constantine.git ignore = untracked branch = master +[submodule "vendor/nim-ffi"] + path = vendor/nim-ffi + url = https://github.com/logos-messaging/nim-ffi/ + ignore = untracked + branch = master diff --git a/Makefile b/Makefile index 3ba4ebe..2a13940 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,28 @@ tui bot_echo pingpong: | build-waku-librln build-waku-nat nim_chat_poc.nims echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim $@ $(NIM_PARAMS) --path:src nim_chat_poc.nims +########### +## Library ## +########### + +# Determine shared library extension based on OS +ifeq ($(shell uname -s),Darwin) + LIBCHAT_EXT := dylib +else ifeq ($(shell uname -s),Linux) + LIBCHAT_EXT := so +else + LIBCHAT_EXT := dll +endif + +LIBCHAT := build/libchat.$(LIBCHAT_EXT) + +.PHONY: libchat +libchat: | build-waku-librln build-waku-nat nim_chat_poc.nims + echo -e $(BUILD_MSG) "$(LIBCHAT)" && \ + $(ENV_SCRIPT) nim libchat $(NIM_PARAMS) --path:src nim_chat_poc.nims && \ + echo -e "\n\x1B[92mLibrary built successfully:\x1B[39m" && \ + echo " $(shell pwd)/$(LIBCHAT)" + endif diff --git a/config.nims b/config.nims new file mode 100644 index 0000000..27a5d0b --- /dev/null +++ b/config.nims @@ -0,0 +1,7 @@ +import std/os + +# all vendor subdirectories +for dir in walkDir(thisDir() / "vendor"): + if dir.kind == pcDir: + switch("path", dir.path) + switch("path", dir.path / "src") diff --git a/examples/cbindings/.clang-format b/examples/cbindings/.clang-format new file mode 100644 index 0000000..97c975d --- /dev/null +++ b/examples/cbindings/.clang-format @@ -0,0 +1,6 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +BreakBeforeBraces: Attach +ColumnLimit: 100 diff --git a/examples/cbindings/Makefile b/examples/cbindings/Makefile new file mode 100644 index 0000000..aa26a01 --- /dev/null +++ b/examples/cbindings/Makefile @@ -0,0 +1,39 @@ +# Makefile for cbindings_chat_tui C example + +CC = gcc +CFLAGS = -Wall -Wextra -I../../library -pthread +LDFLAGS = -L../../build -lchat -lncurses -Wl,-rpath,../../build + +BUILD_DIR = ../../build +TARGET = $(BUILD_DIR)/cbindings_chat_tui +SRC = cbindings_chat_tui.c + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +clean: + rm -f $(TARGET) + +# Run with default settings +run: $(TARGET) + LD_LIBRARY_PATH=$(BUILD_DIR) $(TARGET) + +# Run as alice on port 60001 +run_alice: $(TARGET) + LD_LIBRARY_PATH=$(BUILD_DIR) $(TARGET) --name=Alice --port=60001 + +# Run as bob on port 60002 +run_bob: $(TARGET) + LD_LIBRARY_PATH=$(BUILD_DIR) $(TARGET) --name=Bob --port=60002 + +help: + @echo "Usage:" + @echo " make - Build cbindings_chat_tui" + @echo " make run - Build and run with defaults" + @echo " make run_alice - Build and run as Alice on port 60001" + @echo " make run_bob - Build and run as Bob on port 60002" + @echo " make clean - Remove built files" diff --git a/examples/cbindings/README.md b/examples/cbindings/README.md new file mode 100644 index 0000000..a2c42d8 --- /dev/null +++ b/examples/cbindings/README.md @@ -0,0 +1,59 @@ +# C Bindings Example - Chat TUI + +A simple terminal user interface that demonstrates how to use libchat from C. + +## Build + +1. First, build libchat from the root folder: + + ```bash + make libchat + ``` + +2. Then build the C example: + + ```bash + cd examples/cbindings + make + ``` + +## Run + +Terminal 1: + +```bash +make run_alice +# Runs as Alice on port 60001 +``` + +Terminal 2: + +```bash +make run_bob +# Runs as Bob on port 60002 +``` + +## Workflow + +1. Start the application - it automatically uses your inbox conversation +2. Type `/bundle` to get your IntroBundle JSON (will be copied to clipboard) +3. In the other terminal type `/join ` to start a conversation +4. You can send messages from one termnial to the other + +## Command Line Options + +```text +--name= Identity name (default: user) +--port= Waku port (default: random 50000-50200) +--cluster= Waku cluster ID (default: 42) +--shard= Waku shard ID (default: 2) +--peer= Static peer multiaddr to connect to +--help Show help +``` + +## Waku Configuration + +- **Cluster ID**: 42 +- **Shard ID**: 2 +- **PubSub Topic**: `/waku/2/rs/42/2` +- **Port**: Random between 50000-50200 (or specify with --port) diff --git a/examples/cbindings/cbindings_chat_tui.c b/examples/cbindings/cbindings_chat_tui.c new file mode 100644 index 0000000..9043f3e --- /dev/null +++ b/examples/cbindings/cbindings_chat_tui.c @@ -0,0 +1,738 @@ +/** + * Simple Terminal UI for libchat.c + * Commands: + * /join - Join a conversation + * /bundle - Show your intro bundle + * /quit - Exit + * - Send message to current conversation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libchat.h" + +// Constants +static const int LOG_PANEL_HEIGHT = 6; +static const int MSG_PANEL_HEIGHT = 12; +static const int MAX_MESSAGES = 100; +static const int MAX_LOGS = 50; +static const size_t MAX_LINE_LEN = 2048; +static const size_t MAX_INPUT_LEN = 2048; + +// Application state structures +typedef struct { + char current_convo[128]; + char inbox_id[128]; + char my_name[64]; + char my_address[128]; + void *ctx; +} ChatState; + +typedef struct { + char (*lines)[2048]; + int count; + int max; + pthread_mutex_t mutex; +} TextBuffer; + +typedef struct { + WINDOW *log_win, *msg_win, *input_win; + WINDOW *log_border, *msg_border; + SCREEN *screen; + FILE *tty_out, *tty_in; +} UI; + +typedef struct { + char buffer[2048]; + int len; + int pos; +} InputState; + +typedef struct { + ChatState chat; + TextBuffer messages; + TextBuffer logs; + UI ui; + InputState input; + FILE *log_file; + char log_filename[256]; + atomic_int running; + atomic_int needs_refresh; + atomic_int resize_pending; +} App; + +static App g_app; + +// Forward declarations +static void refresh_ui(void); +static void add_text(TextBuffer *buf, const char *text, const char *prefix); +static void handle_input(const char *input); + +////////////////////////////////////////////////////////////////////////////// +// Utility functions +////////////////////////////////////////////////////////////////////////////// + +static const char *get_timestamp(void) { + static char buf[16]; + time_t now = time(NULL); + struct tm *tm = localtime(&now); + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); + return buf; +} + +// Simple JSON string value extractor +static int json_extract(const char *json, const char **keys, char **values, + size_t *sizes, int n) { + int found = 0; + for (int i = 0; i < n; i++) { + values[i][0] = '\0'; + // Build search pattern: "key": + char pattern[128]; + snprintf(pattern, sizeof(pattern), "\"%s\":", keys[i]); + const char *pos = strstr(json, pattern); + if (!pos) continue; + + pos += strlen(pattern); + while (*pos == ' ') pos++; // skip whitespace + if (*pos != '"') continue; // only handle string values + pos++; // skip opening quote + + const char *end = strchr(pos, '"'); + if (!end) continue; + + size_t len = end - pos; + if (len >= sizes[i]) len = sizes[i] - 1; + strncpy(values[i], pos, len); + values[i][len] = '\0'; + found++; + } + return found; +} + +static void string_to_hex(const char *str, char *hex, size_t hex_size) { + size_t len = strlen(str); + if (len * 2 + 1 > hex_size) + len = (hex_size - 1) / 2; + for (size_t i = 0; i < len; i++) { + snprintf(hex + i * 2, 3, "%02x", (unsigned char)str[i]); + } + hex[len * 2] = '\0'; +} + +static void hex_to_string(const char *hex, char *str, size_t str_size) { + size_t hex_len = strlen(hex); + size_t len = hex_len / 2; + if (len >= str_size) + len = str_size - 1; + for (size_t i = 0; i < len; i++) { + char byte[3] = {hex[i * 2], hex[i * 2 + 1], '\0'}; + char *end; + unsigned long val = strtoul(byte, &end, 16); + if (end != byte + 2) { + str[i] = '?'; // Invalid hex + } else { + str[i] = (char)val; + } + } + str[len] = '\0'; +} + +static int copy_to_clipboard(const char *data, size_t len) { + const char *cmd = NULL; +#ifdef __APPLE__ + cmd = "pbcopy"; +#else + if (system("which wl-copy >/dev/null 2>&1") == 0) + cmd = "wl-copy"; + else if (system("which xclip >/dev/null 2>&1") == 0) + cmd = "xclip -selection clipboard"; + else if (system("which xsel >/dev/null 2>&1") == 0) + cmd = "xsel --clipboard --input"; +#endif + if (!cmd) return 0; + FILE *pipe = popen(cmd, "w"); + if (!pipe) return 0; + fwrite(data, 1, len, pipe); + pclose(pipe); + return 1; +} + +////////////////////////////////////////////////////////////////////////////// +// Text buffer operations +////////////////////////////////////////////////////////////////////////////// + +static int textbuf_init(TextBuffer *buf, int max_lines) { + buf->lines = calloc(max_lines, MAX_LINE_LEN); + if (!buf->lines) return -1; + buf->count = 0; + buf->max = max_lines; + pthread_mutex_init(&buf->mutex, NULL); + return 0; +} + +static void textbuf_destroy(TextBuffer *buf) { + free(buf->lines); + buf->lines = NULL; + pthread_mutex_destroy(&buf->mutex); +} + +static void add_text(TextBuffer *buf, const char *text, const char *prefix) { + pthread_mutex_lock(&buf->mutex); + if (buf->count >= buf->max) { + memmove(buf->lines[0], buf->lines[1], (buf->max - 1) * MAX_LINE_LEN); + buf->count = buf->max - 1; + } + if (prefix) { + snprintf(buf->lines[buf->count], MAX_LINE_LEN, "[%s] %s", prefix, text); + } else { + snprintf(buf->lines[buf->count], MAX_LINE_LEN, "%s", text); + } + buf->count++; + pthread_mutex_unlock(&buf->mutex); + atomic_store(&g_app.needs_refresh, 1); +} + +static inline void add_message(const char *msg) { + add_text(&g_app.messages, msg, NULL); +} + +static inline void add_log(const char *log) { + add_text(&g_app.logs, log, get_timestamp()); +} + +////////////////////////////////////////////////////////////////////////////// +// ncurses UI +////////////////////////////////////////////////////////////////////////////// + +static void create_windows(void) { + int max_y, max_x; + getmaxyx(stdscr, max_y, max_x); + + int log_height = LOG_PANEL_HEIGHT + 2; + int msg_height = MSG_PANEL_HEIGHT + 2; + int input_height = 3; + int available = max_y - input_height; + + if (log_height + msg_height > available) { + log_height = available / 3; + msg_height = available - log_height; + } + + UI *ui = &g_app.ui; + ui->log_border = newwin(log_height, max_x, 0, 0); + ui->msg_border = newwin(msg_height, max_x, log_height, 0); + ui->log_win = derwin(ui->log_border, log_height - 2, max_x - 2, 1, 1); + ui->msg_win = derwin(ui->msg_border, msg_height - 2, max_x - 2, 1, 1); + ui->input_win = newwin(input_height, max_x, log_height + msg_height, 0); + + scrollok(ui->log_win, TRUE); + scrollok(ui->msg_win, TRUE); + keypad(ui->input_win, TRUE); + nodelay(ui->input_win, TRUE); + + if (has_colors()) { + start_color(); + use_default_colors(); + init_pair(1, COLOR_CYAN, -1); + init_pair(2, COLOR_GREEN, -1); + init_pair(3, COLOR_YELLOW, -1); + init_pair(4, COLOR_RED, -1); + init_pair(5, COLOR_MAGENTA, -1); + } +} + +static void destroy_windows(void) { + UI *ui = &g_app.ui; + if (ui->log_win) delwin(ui->log_win); + if (ui->msg_win) delwin(ui->msg_win); + if (ui->input_win) delwin(ui->input_win); + if (ui->log_border) delwin(ui->log_border); + if (ui->msg_border) delwin(ui->msg_border); + memset(ui, 0, sizeof(*ui) - sizeof(ui->screen) - sizeof(ui->tty_out) - sizeof(ui->tty_in)); +} + +static void draw_borders(void) { + UI *ui = &g_app.ui; + ChatState *chat = &g_app.chat; + + wattron(ui->log_border, COLOR_PAIR(3) | A_DIM); + box(ui->log_border, 0, 0); + mvwprintw(ui->log_border, 0, 2, " Logs "); + wattroff(ui->log_border, COLOR_PAIR(3) | A_DIM); + + wattron(ui->msg_border, COLOR_PAIR(1) | A_BOLD); + box(ui->msg_border, 0, 0); + if (chat->current_convo[0]) { + mvwprintw(ui->msg_border, 0, 2, " Messages [%s] [%s] ", chat->my_name, chat->current_convo); + } else { + mvwprintw(ui->msg_border, 0, 2, " Messages [%s] [no conversation] ", chat->my_name); + } + wattroff(ui->msg_border, COLOR_PAIR(1) | A_BOLD); + + wattron(ui->input_win, COLOR_PAIR(2) | A_BOLD); + box(ui->input_win, 0, 0); + mvwprintw(ui->input_win, 0, 2, " Input "); + wattroff(ui->input_win, COLOR_PAIR(2) | A_BOLD); + + wnoutrefresh(ui->log_border); + wnoutrefresh(ui->msg_border); +} + +static void draw_textbuf(WINDOW *win, TextBuffer *buf) { + werase(win); + pthread_mutex_lock(&buf->mutex); + + int max_y = getmaxy(win); + int max_x = getmaxx(win); + int total_lines = 0; + int *lines_per = alloca(buf->count * sizeof(int)); + + for (int i = 0; i < buf->count; i++) { + int len = (int)strlen(buf->lines[i]); + lines_per[i] = len == 0 ? 1 : (len + max_x - 1) / max_x; + total_lines += lines_per[i]; + } + + int skip = total_lines > max_y ? total_lines - max_y : 0; + int start = 0, skipped = 0; + while (start < buf->count && skipped + lines_per[start] <= skip) { + skipped += lines_per[start++]; + } + + int row = 0; + for (int i = start; i < buf->count && row < max_y; i++) { + wmove(win, row, 0); + if (buf == &g_app.logs) wattron(win, A_DIM); + wprintw(win, "%s", buf->lines[i]); + if (buf == &g_app.logs) wattroff(win, A_DIM); + row += lines_per[i]; + } + + pthread_mutex_unlock(&buf->mutex); + wnoutrefresh(win); +} + +static void draw_input(void) { + UI *ui = &g_app.ui; + InputState *inp = &g_app.input; + int max_x = getmaxx(ui->input_win); + + mvwhline(ui->input_win, 1, 1, ' ', max_x - 2); + wattron(ui->input_win, COLOR_PAIR(2) | A_BOLD); + mvwprintw(ui->input_win, 1, 1, "> "); + wattroff(ui->input_win, COLOR_PAIR(2) | A_BOLD); + + int available = max_x - 5; + int display_start = inp->pos > available - 1 ? inp->pos - available + 1 : 0; + mvwprintw(ui->input_win, 1, 3, "%.*s", available, inp->buffer + display_start); + wmove(ui->input_win, 1, 3 + inp->pos - display_start); + wnoutrefresh(ui->input_win); +} + +static void refresh_ui(void) { + if (!atomic_exchange(&g_app.needs_refresh, 0)) return; + + if (atomic_exchange(&g_app.resize_pending, 0)) { + endwin(); + refresh(); + destroy_windows(); + create_windows(); + } + + draw_borders(); + draw_textbuf(g_app.ui.log_win, &g_app.logs); + draw_textbuf(g_app.ui.msg_win, &g_app.messages); + draw_input(); + doupdate(); +} + +////////////////////////////////////////////////////////////////////////////// +// Signal handling (async-signal-safe) +////////////////////////////////////////////////////////////////////////////// + +static void handle_sigint(int sig) { + (void)sig; + atomic_store(&g_app.running, 0); +} + +static void handle_sigwinch(int sig) { + (void)sig; + atomic_store(&g_app.resize_pending, 1); + atomic_store(&g_app.needs_refresh, 1); +} + +////////////////////////////////////////////////////////////////////////////// +// FFI Callbacks +////////////////////////////////////////////////////////////////////////////// + +static void general_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + char buf[256]; + if (ret == RET_OK) { + snprintf(buf, sizeof(buf), "OK%s%.*s", len > 0 ? ": " : "", (int)(len > 60 ? 60 : len), msg); + } else { + snprintf(buf, sizeof(buf), "ERR: %.*s", (int)(len > 60 ? 60 : len), msg); + } + add_log(buf); +} + +static void event_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + (void)ret; + (void)len; + + char event_type[32] = {0}; + char convo_id[128] = {0}; + char content[2048] = {0}; + + const char *keys[] = {"eventType", "conversationId", "content"}; + char *values[] = {event_type, convo_id, content}; + size_t sizes[] = {sizeof(event_type), sizeof(convo_id), sizeof(content)}; + json_extract(msg, keys, values, sizes, 3); + + if (strcmp(event_type, "new_message") == 0) { + char decoded[2048], buf[2048]; + hex_to_string(content, decoded, sizeof(decoded)); + snprintf(buf, sizeof(buf), "<- %s", decoded); + add_message(buf); + } else if (strcmp(event_type, "new_conversation") == 0) { + strncpy(g_app.chat.current_convo, convo_id, sizeof(g_app.chat.current_convo) - 1); + char buf[256]; + snprintf(buf, sizeof(buf), "* New conversation: %.32s...", g_app.chat.current_convo); + add_message(buf); + } else if (strcmp(event_type, "delivery_ack") == 0) { + add_log("Delivery acknowledged"); + } + + char buf[256]; + snprintf(buf, sizeof(buf), "EVT: %.70s%s", msg, len > 70 ? "..." : ""); + add_log(buf); +} + +static void bundle_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + if (ret == RET_OK && len > 0) { + char buf[2048]; + snprintf(buf, sizeof(buf), "%.*s", (int)(len < sizeof(buf) - 1 ? len : sizeof(buf) - 1), msg); + + if (copy_to_clipboard(msg, len)) { + add_message("Your IntroBundle (copied to clipboard):"); + add_log("Bundle copied to clipboard"); + } else { + add_message("Your IntroBundle:"); + } + add_message(""); + add_message(buf); + add_message(""); + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "Failed to get bundle: %.*s", (int)len, msg); + add_message(buf); + } +} + +static void identity_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; (void)len; + if (ret == RET_OK) { + const char *keys[] = {"name", "address"}; + char *values[] = {g_app.chat.my_name, g_app.chat.my_address}; + size_t sizes[] = {sizeof(g_app.chat.my_name), sizeof(g_app.chat.my_address)}; + json_extract(msg, keys, values, sizes, 2); + + char buf[256]; + snprintf(buf, sizeof(buf), "Identity: %s (%.24s...)", g_app.chat.my_name, g_app.chat.my_address); + add_log(buf); + } +} + +static void inbox_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + if (ret == RET_OK && len > 0) { + snprintf(g_app.chat.inbox_id, sizeof(g_app.chat.inbox_id), "%.*s", (int)len, msg); + char buf[256]; + snprintf(buf, sizeof(buf), "Inbox: %.24s...", g_app.chat.inbox_id); + add_log(buf); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Command handling +////////////////////////////////////////////////////////////////////////////// + +static void cmd_join(const char *args) { + if (!args || !*args) { + add_message("Usage: /join "); + return; + } + char hex_msg[256]; + string_to_hex("Hello!", hex_msg, sizeof(hex_msg)); + chat_new_private_conversation(g_app.chat.ctx, general_callback, NULL, args, hex_msg); + add_message("* Creating conversation..."); +} + +static void cmd_send(const char *message) { + if (!g_app.chat.current_convo[0]) { + add_message("No active conversation. Use /join or receive an invite."); + return; + } + char hex_msg[4096]; + string_to_hex(message, hex_msg, sizeof(hex_msg)); + chat_send_message(g_app.chat.ctx, general_callback, NULL, g_app.chat.current_convo, hex_msg); + + char buf[2048]; + snprintf(buf, sizeof(buf), "-> You: %s", message); + add_message(buf); +} + +static void handle_input(const char *input) { + if (!input || !*input) return; + + if (input[0] != '/') { + cmd_send(input); + return; + } + + if (strncmp(input, "/quit", 5) == 0 || strncmp(input, "/q", 2) == 0) { + atomic_store(&g_app.running, 0); + } else if (strncmp(input, "/join ", 6) == 0) { + cmd_join(input + 6); + } else if (strncmp(input, "/bundle", 7) == 0) { + chat_create_intro_bundle(g_app.chat.ctx, bundle_callback, NULL); + } else if (strncmp(input, "/help", 5) == 0) { + add_message("Commands:"); + add_message(" /join - Join conversation with IntroBundle"); + add_message(" /bundle - Show your IntroBundle"); + add_message(" /quit - Exit"); + add_message(" - Send message"); + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "Unknown command: %s", input); + add_message(buf); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Input processing +////////////////////////////////////////////////////////////////////////////// + +static void process_input_char(int ch) { + InputState *inp = &g_app.input; + + switch (ch) { + case '\n': + case KEY_ENTER: + if (inp->len > 0) { + inp->buffer[inp->len] = '\0'; + handle_input(inp->buffer); + inp->len = inp->pos = 0; + inp->buffer[0] = '\0'; + } + break; + case KEY_BACKSPACE: + case 127: + case 8: + if (inp->pos > 0) { + memmove(inp->buffer + inp->pos - 1, inp->buffer + inp->pos, inp->len - inp->pos + 1); + inp->pos--; + inp->len--; + } + break; + case KEY_DC: + if (inp->pos < inp->len) { + memmove(inp->buffer + inp->pos, inp->buffer + inp->pos + 1, inp->len - inp->pos); + inp->len--; + } + break; + case KEY_LEFT: + if (inp->pos > 0) inp->pos--; + break; + case KEY_RIGHT: + if (inp->pos < inp->len) inp->pos++; + break; + default: + if (ch >= 32 && ch < 127 && inp->len < (int)MAX_INPUT_LEN - 1) { + memmove(inp->buffer + inp->pos + 1, inp->buffer + inp->pos, inp->len - inp->pos + 1); + inp->buffer[inp->pos++] = ch; + inp->len++; + } + break; + } + atomic_store(&g_app.needs_refresh, 1); +} + +////////////////////////////////////////////////////////////////////////////// +// Initialization and cleanup +////////////////////////////////////////////////////////////////////////////// + +static int init_logging(const char *name) { + time_t now = time(NULL); + snprintf(g_app.log_filename, sizeof(g_app.log_filename), "chat_tui_%s_%ld.log", name, (long)now); + + g_app.log_file = fopen(g_app.log_filename, "w"); + if (!g_app.log_file) { + g_app.log_file = fopen("/dev/null", "w"); + } + + g_app.ui.tty_out = fopen("/dev/tty", "w"); + g_app.ui.tty_in = fopen("/dev/tty", "r"); + if (!g_app.ui.tty_out || !g_app.ui.tty_in) { + fprintf(stderr, "Error: Could not open /dev/tty\n"); + return -1; + } + + fflush(stdout); + fflush(stderr); + dup2(fileno(g_app.log_file), STDOUT_FILENO); + dup2(fileno(g_app.log_file), STDERR_FILENO); + return 0; +} + +static int init_ui(void) { + g_app.ui.screen = newterm(NULL, g_app.ui.tty_out, g_app.ui.tty_in); + if (!g_app.ui.screen) return -1; + + set_term(g_app.ui.screen); + cbreak(); + noecho(); + curs_set(1); + if (has_colors()) { + start_color(); + use_default_colors(); + } + create_windows(); + return 0; +} + +static void cleanup(void) { + if (g_app.chat.ctx) { + chat_stop(g_app.chat.ctx, general_callback, NULL); + chat_destroy(g_app.chat.ctx, general_callback, NULL); + } + + destroy_windows(); + if (g_app.ui.screen) { + endwin(); + delscreen(g_app.ui.screen); + } + + if (g_app.ui.tty_out) fclose(g_app.ui.tty_out); + if (g_app.ui.tty_in) fclose(g_app.ui.tty_in); + if (g_app.log_file) fclose(g_app.log_file); + + textbuf_destroy(&g_app.messages); + textbuf_destroy(&g_app.logs); + + FILE *tty = fopen("/dev/tty", "w"); + if (tty) { + fprintf(tty, "Goodbye! (Library logs saved to %s)\n", g_app.log_filename); + fclose(tty); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Main +////////////////////////////////////////////////////////////////////////////// + +int main(int argc, char *argv[]) { + const char *name = "user"; + int port = 0, cluster_id = 42, shard_id = 2; + const char *peer = NULL; + + for (int i = 1; i < argc; i++) { + if (strncmp(argv[i], "--name=", 7) == 0) name = argv[i] + 7; + else if (strncmp(argv[i], "--port=", 7) == 0) port = atoi(argv[i] + 7); + else if (strncmp(argv[i], "--cluster=", 10) == 0) cluster_id = atoi(argv[i] + 10); + else if (strncmp(argv[i], "--shard=", 8) == 0) shard_id = atoi(argv[i] + 8); + else if (strncmp(argv[i], "--peer=", 7) == 0) peer = argv[i] + 7; + else if (strcmp(argv[i], "--help") == 0) { + printf("Usage: %s [options]\n", argv[0]); + printf(" --name= Your display name\n"); + printf(" --port= Listen port (0 for random)\n"); + printf(" --cluster= Cluster ID (default: 42)\n"); + printf(" --shard= Shard ID (default: 2)\n"); + printf(" --peer= Static peer multiaddr\n"); + return 0; + } + } + + // Initialize application state + memset(&g_app, 0, sizeof(g_app)); + strncpy(g_app.chat.my_name, name, sizeof(g_app.chat.my_name) - 1); + atomic_store(&g_app.running, 1); + + if (textbuf_init(&g_app.messages, MAX_MESSAGES) < 0 || + textbuf_init(&g_app.logs, MAX_LOGS) < 0) { + fprintf(stderr, "Failed to allocate buffers\n"); + return 1; + } + + if (init_logging(name) < 0) { + textbuf_destroy(&g_app.messages); + textbuf_destroy(&g_app.logs); + return 1; + } + + // Build config and create chat context + char config[2048]; + if (peer) { + snprintf(config, sizeof(config), + "{\"name\":\"%s\",\"port\":%d,\"clusterId\":%d,\"shardId\":%d,\"staticPeer\":\"%s\"}", + name, port, cluster_id, shard_id, peer); + } else { + snprintf(config, sizeof(config), + "{\"name\":\"%s\",\"port\":%d,\"clusterId\":%d,\"shardId\":%d}", + name, port, cluster_id, shard_id); + } + + g_app.chat.ctx = chat_new(config, general_callback, NULL); + if (!g_app.chat.ctx) { + fprintf(g_app.log_file, "Failed to create chat context\n"); + cleanup(); + return 1; + } + + set_event_callback(g_app.chat.ctx, event_callback, NULL); + + if (init_ui() < 0) { + fprintf(g_app.log_file, "Failed to initialize ncurses\n"); + cleanup(); + return 1; + } + + signal(SIGINT, handle_sigint); + signal(SIGWINCH, handle_sigwinch); + + add_log("Starting client..."); + chat_start(g_app.chat.ctx, general_callback, NULL); + chat_get_identity(g_app.chat.ctx, identity_callback, NULL); + chat_get_default_inbox_id(g_app.chat.ctx, inbox_callback, NULL); + + add_message("Welcome to Chat TUI!"); + add_message("Type /help for commands, /quit to exit"); + add_message(""); + + atomic_store(&g_app.needs_refresh, 1); + refresh_ui(); + + // Main loop + while (atomic_load(&g_app.running)) { + int ch; + while ((ch = wgetch(g_app.ui.input_win)) != ERR) { + process_input_char(ch); + } + refresh_ui(); + usleep(10000); + } + + add_log("Shutting down..."); + cleanup(); + return 0; +} \ No newline at end of file diff --git a/library/api/client_api.nim b/library/api/client_api.nim new file mode 100644 index 0000000..0ff2bf2 --- /dev/null +++ b/library/api/client_api.nim @@ -0,0 +1,156 @@ +## Client API - FFI bindings for Client lifecycle and operations +## Uses the {.ffi.} pragma for async request handling + +import std/json +import chronicles +import chronos +import ffi + +import ../../src/chat +import ../../src/chat/proto_types +import ../../src/chat/delivery/waku_client +import ../../src/chat/identity +import ../utils + +logScope: + topics = "chat ffi client" + +################################################# +# Client Creation Request (for chat_new) +################################################# + +type ChatCallbacks* = object + onNewMessage*: MessageCallback + onNewConversation*: NewConvoCallback + onDeliveryAck*: DeliveryAckCallback + +proc createChatClient( + configJson: cstring, chatCallbacks: ChatCallbacks +): Future[Result[Client, string]] {.async.} = + try: + let config = parseJson($configJson) + + # Parse identity name + let name = config.getOrDefault("name").getStr("anonymous") + + # Parse Waku configuration or use defaults + var wakuCfg = DefaultConfig() + + if config.hasKey("port"): + wakuCfg.port = config["port"].getInt().uint16 + + if config.hasKey("clusterId"): + wakuCfg.clusterId = config["clusterId"].getInt().uint16 + + if config.hasKey("shardId"): + wakuCfg.shardId = @[config["shardId"].getInt().uint16] + + if config.hasKey("staticPeers"): + wakuCfg.staticPeers = @[] + for peer in config["staticPeers"]: + wakuCfg.staticPeers.add(peer.getStr()) + + # Create identity + let identity = createIdentity(name) + + # Create Waku client + let wakuClient = initWakuClient(wakuCfg) + + # Create Chat client + let client = newClient(wakuClient, identity) + + # Register event handlers + client.onNewMessage(chatCallbacks.onNewMessage) + client.onNewConversation(chatCallbacks.onNewConversation) + client.onDeliveryAck(chatCallbacks.onDeliveryAck) + + notice "Chat client created", name = name + return ok(client) + except CatchableError as e: + return err("failed to create client: " & e.msg) + +registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[Client]): + proc( + configJson: cstring, chatCallbacks: ChatCallbacks + ): Future[Result[string, string]] {.async.} = + ctx[].myLib[] = (await createChatClient(configJson, chatCallbacks)).valueOr: + error "CreateClientRequest failed", error = error + return err($error) + return ok("") + +################################################# +# Client Lifecycle Operations +################################################# + +proc chat_start( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + try: + await ctx[].myLib[].start() + return ok("") + except CatchableError as e: + error "chat_start failed", error = e.msg + return err("failed to start client: " & e.msg) + +proc chat_stop( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + try: + await ctx[].myLib[].stop() + return ok("") + except CatchableError as e: + error "chat_stop failed", error = e.msg + return err("failed to stop client: " & e.msg) + +################################################# +# Client Info Operations +################################################# + +proc chat_get_id( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Get the client's identifier + let clientId = ctx[].myLib[].getId() + return ok(clientId) + +proc chat_get_default_inbox_id( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Get the default inbox conversation ID + let inboxId = ctx[].myLib[].defaultInboxConversationId() + return ok(inboxId) + +################################################# +# Conversation List Operations +################################################# + +proc chat_list_conversations( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## List all conversations as JSON array + let convos = ctx[].myLib[].listConversations() + var convoList = newJArray() + for convo in convos: + convoList.add(%*{"id": convo.id()}) + return ok($convoList) + +proc chat_get_conversation( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer, + convoId: cstring +) {.ffi.} = + ## Get a specific conversation by ID + let convo = ctx[].myLib[].getConversation($convoId) + return ok($(%*{"id": convo.id()})) + diff --git a/library/api/conversation_api.nim b/library/api/conversation_api.nim new file mode 100644 index 0000000..3d8b153 --- /dev/null +++ b/library/api/conversation_api.nim @@ -0,0 +1,78 @@ +## Conversation API - FFI bindings for conversation operations +## Uses the {.ffi.} pragma for async request handling + +import std/[json, options] +import chronicles +import chronos +import ffi +import stew/byteutils + +import ../../src/chat +import ../../src/chat/proto_types +import ../utils + +logScope: + topics = "chat ffi conversation" + +################################################# +# Private Conversation Operations +################################################# + +proc chat_new_private_conversation( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer, + introBundleJson: cstring, + contentHex: cstring +) {.ffi.} = + ## Create a new private conversation with the given IntroBundle + ## introBundleJson: JSON string with {"ident": "hex...", "ephemeral": "hex..."} + ## contentHex: Initial message content as hex-encoded string + try: + let bundleJson = parseJson($introBundleJson) + + # Parse IntroBundle from JSON + let identBytes = hexToSeqByte(bundleJson["ident"].getStr()) + let ephemeralBytes = hexToSeqByte(bundleJson["ephemeral"].getStr()) + + let introBundle = IntroBundle( + ident: identBytes, + ephemeral: ephemeralBytes + ) + + # Convert hex content to bytes + let content = hexToSeqByte($contentHex) + + # Create the conversation + let errOpt = await ctx[].myLib[].newPrivateConversation(introBundle, content) + if errOpt.isSome(): + return err("failed to create conversation: " & $errOpt.get()) + + return ok("") + except CatchableError as e: + error "chat_new_private_conversation failed", error = e.msg + return err("failed to create private conversation: " & e.msg) + +################################################# +# Message Operations +################################################# + +proc chat_send_message( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer, + convoId: cstring, + contentHex: cstring +) {.ffi.} = + ## Send a message to a conversation + ## convoId: Conversation ID string + ## contentHex: Message content as hex-encoded string + try: + let convo = ctx[].myLib[].getConversation($convoId) + let content = hexToSeqByte($contentHex) + + let msgId = await convo.sendMessage(content) + return ok(msgId) + except CatchableError as e: + error "chat_send_message failed", error = e.msg + return err("failed to send message: " & e.msg) diff --git a/library/api/identity_api.nim b/library/api/identity_api.nim new file mode 100644 index 0000000..bb903d8 --- /dev/null +++ b/library/api/identity_api.nim @@ -0,0 +1,54 @@ +## Identity API - FFI bindings for identity operations +## Uses the {.ffi.} pragma for async request handling + +import std/json +import chronicles +import chronos +import ffi +import stew/byteutils + +import ../../src/chat +import ../../src/chat/crypto +import ../../src/chat/proto_types +import ../utils + +logScope: + topics = "chat ffi identity" + +################################################# +# Identity Operations +################################################# + +proc chat_get_identity( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Get the client identity + ## Returns JSON string: {"name": "...", "address": "...", "pubkey": "hex..."} + let ident = ctx[].myLib[].identity() + let identJson = %*{ + "name": ident.getName(), + "address": ident.getAddr(), + "pubkey": ident.getPubkey().toHex() + } + return ok($identJson) + +################################################# +# IntroBundle Operations +################################################# + +proc chat_create_intro_bundle( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Create an IntroBundle for initiating private conversations + ## Returns JSON string: {"ident": "hex...", "ephemeral": "hex..."} + let bundle = ctx[].myLib[].createIntroBundle() + let bundleJson = %*{ + "ident": bundle.ident.toHex(), + "ephemeral": bundle.ephemeral.toHex() + } + return ok($bundleJson) + diff --git a/library/declare_lib.nim b/library/declare_lib.nim new file mode 100644 index 0000000..ea950cb --- /dev/null +++ b/library/declare_lib.nim @@ -0,0 +1,13 @@ +import ffi +import ../src/chat/client + +declareLibrary("chat") + +proc set_event_callback( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.dynlib, exportc, cdecl.} = + ctx[].eventCallback = cast[pointer](callback) + ctx[].eventUserData = userData + diff --git a/library/libchat.h b/library/libchat.h new file mode 100644 index 0000000..a2bf02a --- /dev/null +++ b/library/libchat.h @@ -0,0 +1,107 @@ +// Generated manually +#ifndef __libchat__ +#define __libchat__ + +#include +#include + +// The possible returned values for the functions that return int +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, + void *userData); + +////////////////////////////////////////////////////////////////////////////// +// Client Lifecycle +////////////////////////////////////////////////////////////////////////////// + +// Creates a new instance of the chat client. +// Sets up the chat client from the given configuration. +// Returns a pointer to the Context needed by the rest of the API functions. +// configJson: JSON object with fields: +// - "name": string - identity name (default: "anonymous") +// - "port": int - Waku port (optional) +// - "clusterId": int - Waku cluster ID (optional) +// - "shardId": int - Waku shard ID (optional) +// - "staticPeers": array of strings - static peer multiaddrs (optional) +void *chat_new(const char *configJson, FFICallBack callback, void *userData); + +// Start the chat client and begin listening for messages +int chat_start(void *ctx, FFICallBack callback, void *userData); + +// Stop the chat client +int chat_stop(void *ctx, FFICallBack callback, void *userData); + +// Destroys an instance of a chat client created with chat_new +int chat_destroy(void *ctx, FFICallBack callback, void *userData); + +// Sets a callback that will be invoked whenever an event occurs. +// Events are JSON objects with "eventType" field: +// - "new_message": +// {"eventType":"new_message","conversationId":"...","messageId":"...","content":"hex...","timestamp":...} +// - "new_conversation": +// {"eventType":"new_conversation","conversationId":"...","conversationType":"private"} +// - "delivery_ack": +// {"eventType":"delivery_ack","conversationId":"...","messageId":"..."} +void set_event_callback(void *ctx, FFICallBack callback, void *userData); + +////////////////////////////////////////////////////////////////////////////// +// Client Info +////////////////////////////////////////////////////////////////////////////// + +// Get the client's identifier +int chat_get_id(void *ctx, FFICallBack callback, void *userData); + +// Get the default inbox conversation ID +int chat_get_default_inbox_id(void *ctx, FFICallBack callback, void *userData); + +////////////////////////////////////////////////////////////////////////////// +// Conversation Operations +////////////////////////////////////////////////////////////////////////////// + +// List all conversations as JSON array +// Returns: JSON array of objects with "id" field +int chat_list_conversations(void *ctx, FFICallBack callback, void *userData); + +// Get a specific conversation by ID +// Returns: JSON object with "id" field +int chat_get_conversation(void *ctx, FFICallBack callback, void *userData, + const char *convoId); + +// Create a new private conversation with the given IntroBundle +// introBundleJson: JSON string with {"ident": "hex...", "ephemeral": "hex..."} +// contentHex: Initial message content as hex-encoded string +int chat_new_private_conversation(void *ctx, FFICallBack callback, + void *userData, const char *introBundleJson, + const char *contentHex); + +// Send a message to a conversation +// convoId: Conversation ID string +// contentHex: Message content as hex-encoded string +// Returns: Message ID on success +int chat_send_message(void *ctx, FFICallBack callback, void *userData, + const char *convoId, const char *contentHex); + +////////////////////////////////////////////////////////////////////////////// +// Identity Operations +////////////////////////////////////////////////////////////////////////////// + +// Get the client identity +// Returns JSON: {"name": "...", "address": "...", "pubkey": "hex..."} +int chat_get_identity(void *ctx, FFICallBack callback, void *userData); + +// Create an IntroBundle for initiating private conversations +// Returns JSON: {"ident": "hex...", "ephemeral": "hex..."} +int chat_create_intro_bundle(void *ctx, FFICallBack callback, void *userData); + +#ifdef __cplusplus +} +#endif + +#endif /* __libchat__ */ diff --git a/library/libchat.nim b/library/libchat.nim new file mode 100644 index 0000000..93844b7 --- /dev/null +++ b/library/libchat.nim @@ -0,0 +1,98 @@ +## libchat - C bindings for the Chat SDK +## Main entry point for the shared library +## +## This library exposes the Chat SDK functionality through a C-compatible FFI interface. +## It uses nim-ffi for thread-safe async request handling. + +import std/[json, options] +import chronicles, chronos, ffi +import stew/byteutils + +import + ../src/chat/client, + ../src/chat/conversations, + ../src/chat/identity, + ../src/chat/delivery/waku_client, + ../src/chat/proto_types, + ./declare_lib, + ./utils + +logScope: + topics = "chat ffi" + +################################################################################ +## Include different APIs, i.e. all procs with {.ffi.} pragma +include + ./api/client_api, + ./api/conversation_api, + ./api/identity_api + +################################################################################ + +proc chat_new( + configJson: cstring, callback: FFICallBack, userData: pointer +): pointer {.dynlib, exportc, cdecl.} = + initializeLibrary() + + ## Creates a new instance of the Chat Client. + if isNil(callback): + echo "error: missing callback in chat_new" + return nil + + ## Create the Chat thread that will keep waiting for req from the main thread. + var ctx = ffi.createFFIContext[Client]().valueOr: + let msg = "Error in createFFIContext: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + ctx.userData = userData + + proc onNewMessage(ctx: ptr FFIContext[Client]): MessageCallback = + return proc(conversation: Conversation, msg: ReceivedMessage): Future[void] {.async.} = + callEventCallback(ctx, "onNewMessage"): + $newJsonMessageEvent( + conversation.id(), + "", + msg.content.toHex(), + msg.timestamp + ) + + proc onNewConversation(ctx: ptr FFIContext[Client]): NewConvoCallback = + return proc(conversation: Conversation): Future[void] {.async.} = + callEventCallback(ctx, "onNewConversation"): + $newJsonConversationEvent(conversation.id(), "private") + + proc onDeliveryAck(ctx: ptr FFIContext[Client]): DeliveryAckCallback = + return proc(conversation: Conversation, msgId: MessageId): Future[void] {.async.} = + callEventCallback(ctx, "onDeliveryAck"): + $newJsonDeliveryAckEvent(conversation.id(), msgId) + + let chatCallbacks = ChatCallbacks( + onNewMessage: onNewMessage(ctx), + onNewConversation: onNewConversation(ctx), + onDeliveryAck: onDeliveryAck(ctx), + ) + + ffi.sendRequestToFFIThread( + ctx, CreateClientRequest.ffiNewReq(callback, userData, configJson, chatCallbacks) + ).isOkOr: + let msg = "error in sendRequestToFFIThread: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + return ctx + +proc chat_destroy( + ctx: ptr FFIContext[Client], callback: FFICallBack, userData: pointer +): cint {.dynlib, exportc, cdecl.} = + initializeLibrary() + checkParams(ctx, callback, userData) + + ffi.destroyFFIContext(ctx).isOkOr: + let msg = "libchat error: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return RET_ERR + + callback(RET_OK, nil, 0, userData) + + return RET_OK diff --git a/library/nim.cfg b/library/nim.cfg new file mode 100644 index 0000000..3dc6221 --- /dev/null +++ b/library/nim.cfg @@ -0,0 +1 @@ +path = "../" diff --git a/library/utils.nim b/library/utils.nim new file mode 100644 index 0000000..5c342f7 --- /dev/null +++ b/library/utils.nim @@ -0,0 +1,114 @@ +## Utility functions for C-bindings +## Provides C-string helpers and JSON event serialization + +import std/json +import ffi + +# Re-export common FFI types +export ffi + +################################################# +# C-String Helpers +################################################# + +proc toCString*(s: string): cstring = + ## Convert Nim string to C string (caller must manage memory) + result = s.cstring + +proc fromCString*(cs: cstring): string = + ## Convert C string to Nim string (makes a copy) + if cs.isNil: + result = "" + else: + result = $cs + +proc toBytes*(data: ptr byte, len: csize_t): seq[byte] = + ## Convert C byte array to Nim seq[byte] + if data.isNil or len == 0: + result = @[] + else: + result = newSeq[byte](len) + copyMem(addr result[0], data, len) + +################################################# +# JSON Event Types +################################################# + +type + JsonEventType* = enum + EventNewMessage = "new_message" + EventNewConversation = "new_conversation" + EventDeliveryAck = "delivery_ack" + EventError = "error" + +type + JsonMessageEvent* = object + eventType*: string + conversationId*: string + messageId*: string + content*: string + timestamp*: int64 + + JsonConversationEvent* = object + eventType*: string + conversationId*: string + conversationType*: string + + JsonDeliveryAckEvent* = object + eventType*: string + conversationId*: string + messageId*: string + + JsonErrorEvent* = object + eventType*: string + error*: string + +################################################# +# JSON Event Constructors +################################################# + +proc newJsonMessageEvent*(convoId, msgId, content: string, timestamp: int64): JsonMessageEvent = + result = JsonMessageEvent( + eventType: $EventNewMessage, + conversationId: convoId, + messageId: msgId, + content: content, + timestamp: timestamp + ) + +proc newJsonConversationEvent*(convoId, convoType: string): JsonConversationEvent = + result = JsonConversationEvent( + eventType: $EventNewConversation, + conversationId: convoId, + conversationType: convoType + ) + +proc newJsonDeliveryAckEvent*(convoId, msgId: string): JsonDeliveryAckEvent = + result = JsonDeliveryAckEvent( + eventType: $EventDeliveryAck, + conversationId: convoId, + messageId: msgId + ) + +proc newJsonErrorEvent*(error: string): JsonErrorEvent = + result = JsonErrorEvent( + eventType: $EventError, + error: error + ) + +################################################# +# JSON Serialization +################################################# + +proc `$`*(event: JsonMessageEvent): string = + $(%*event) + +proc `$`*(event: JsonConversationEvent): string = + $(%*event) + +proc `$`*(event: JsonDeliveryAckEvent): string = + $(%*event) + +proc `$`*(event: JsonErrorEvent): string = + $(%*event) + diff --git a/nim_chat_poc.nimble b/nim_chat_poc.nimble index cc5537e..3d63a0a 100644 --- a/nim_chat_poc.nimble +++ b/nim_chat_poc.nimble @@ -21,7 +21,8 @@ requires "nim >= 2.2.4", "regex", "web3", "https://github.com/jazzz/nim-sds#exports", - "waku" + "waku", + "ffi" proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = if not dirExists "build": @@ -33,7 +34,25 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = exec "nim " & lang & " --out:build/" & name & " --mm:refc " & extra_params & " " & srcDir & name & ".nim" - + +proc buildLibrary(name: string, srcDir = "library/", params = "", lang = "c") = + ## Build a shared library (.so on Linux, .dylib on macOS, .dll on Windows) + if not dirExists "build": + mkDir "build" + + # Determine library extension based on OS + let libExt = when defined(macosx): "dylib" + elif defined(windows): "dll" + else: "so" + + var extra_params = params + for i in 2 ..< paramCount(): + extra_params &= " " & paramStr(i) + + exec "nim " & lang & " --app:lib --out:build/lib" & name & "." & libExt & + " --mm:refc --nimMainPrefix:lib" & name & " " & extra_params & " " & + srcDir & "lib" & name & ".nim" + proc test(name: string, params = "-d:chronicles_log_level=DEBUG", lang = "c") = buildBinary name, "tests/", params exec "build/" & name @@ -56,3 +75,7 @@ task bot_echo, "Build the EchoBot example": task pingpong, "Build the Pingpong example": let name = "pingpong" buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' " + +task libchat, "Build the Chat SDK shared library (C bindings)": + buildLibrary "chat", "library/", + "-d:chronicles_log_level='INFO' -d:chronicles_enabled=on --path:src --path:vendor/nim-ffi" From 0359d19e7bc7bc6311e0ffdf93773b4c2752a6cc Mon Sep 17 00:00:00 2001 From: pablo Date: Fri, 9 Jan 2026 11:29:14 +0200 Subject: [PATCH 2/4] fix: pr comments --- examples/cbindings/cbindings_chat_tui.c | 4 ++-- library/api/client_api.nim | 10 ++++----- library/api/conversation_api.nim | 6 ++--- library/api/identity_api.nim | 8 +++---- library/declare_lib.nim | 2 +- library/libchat.nim | 14 ++++++------ library/utils.nim | 29 +------------------------ 7 files changed, 23 insertions(+), 50 deletions(-) diff --git a/examples/cbindings/cbindings_chat_tui.c b/examples/cbindings/cbindings_chat_tui.c index 9043f3e..faffbe4 100644 --- a/examples/cbindings/cbindings_chat_tui.c +++ b/examples/cbindings/cbindings_chat_tui.c @@ -537,8 +537,8 @@ static void process_input_char(int ch) { } break; case KEY_BACKSPACE: - case 127: - case 8: + case 127: // DEL + case 8: // BS if (inp->pos > 0) { memmove(inp->buffer + inp->pos - 1, inp->buffer + inp->pos, inp->len - inp->pos + 1); inp->pos--; diff --git a/library/api/client_api.nim b/library/api/client_api.nim index 0ff2bf2..71c2827 100644 --- a/library/api/client_api.nim +++ b/library/api/client_api.nim @@ -6,11 +6,11 @@ import chronicles import chronos import ffi -import ../../src/chat -import ../../src/chat/proto_types -import ../../src/chat/delivery/waku_client -import ../../src/chat/identity -import ../utils +import src/chat +import src/chat/proto_types +import src/chat/delivery/waku_client +import src/chat/identity +import library/utils logScope: topics = "chat ffi client" diff --git a/library/api/conversation_api.nim b/library/api/conversation_api.nim index 3d8b153..79779f4 100644 --- a/library/api/conversation_api.nim +++ b/library/api/conversation_api.nim @@ -7,9 +7,9 @@ import chronos import ffi import stew/byteutils -import ../../src/chat -import ../../src/chat/proto_types -import ../utils +import src/chat +import src/chat/proto_types +import library/utils logScope: topics = "chat ffi conversation" diff --git a/library/api/identity_api.nim b/library/api/identity_api.nim index bb903d8..6d2ccdb 100644 --- a/library/api/identity_api.nim +++ b/library/api/identity_api.nim @@ -7,10 +7,10 @@ import chronos import ffi import stew/byteutils -import ../../src/chat -import ../../src/chat/crypto -import ../../src/chat/proto_types -import ../utils +import src/chat +import src/chat/crypto +import src/chat/proto_types +import library/utils logScope: topics = "chat ffi identity" diff --git a/library/declare_lib.nim b/library/declare_lib.nim index ea950cb..c6d1aa4 100644 --- a/library/declare_lib.nim +++ b/library/declare_lib.nim @@ -1,5 +1,5 @@ import ffi -import ../src/chat/client +import src/chat/client declareLibrary("chat") diff --git a/library/libchat.nim b/library/libchat.nim index 93844b7..9437f7c 100644 --- a/library/libchat.nim +++ b/library/libchat.nim @@ -9,13 +9,13 @@ import chronicles, chronos, ffi import stew/byteutils import - ../src/chat/client, - ../src/chat/conversations, - ../src/chat/identity, - ../src/chat/delivery/waku_client, - ../src/chat/proto_types, - ./declare_lib, - ./utils + src/chat/client, + src/chat/conversations, + src/chat/identity, + src/chat/delivery/waku_client, + src/chat/proto_types, + library/declare_lib, + library/utils logScope: topics = "chat ffi" diff --git a/library/utils.nim b/library/utils.nim index 5c342f7..5361fd0 100644 --- a/library/utils.nim +++ b/library/utils.nim @@ -1,34 +1,7 @@ ## Utility functions for C-bindings -## Provides C-string helpers and JSON event serialization +## Provides JSON event serialization helpers import std/json -import ffi - -# Re-export common FFI types -export ffi - -################################################# -# C-String Helpers -################################################# - -proc toCString*(s: string): cstring = - ## Convert Nim string to C string (caller must manage memory) - result = s.cstring - -proc fromCString*(cs: cstring): string = - ## Convert C string to Nim string (makes a copy) - if cs.isNil: - result = "" - else: - result = $cs - -proc toBytes*(data: ptr byte, len: csize_t): seq[byte] = - ## Convert C byte array to Nim seq[byte] - if data.isNil or len == 0: - result = @[] - else: - result = newSeq[byte](len) - copyMem(addr result[0], data, len) ################################################# # JSON Event Types From 78d90b7b11daa2dfe9cc8fdf81c10961d8a1d8c6 Mon Sep 17 00:00:00 2001 From: pablo Date: Fri, 9 Jan 2026 11:49:04 +0200 Subject: [PATCH 3/4] fix: using ctx --- library/api/client_api.nim | 14 +++++++------- library/api/conversation_api.nim | 4 ++-- library/api/identity_api.nim | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/library/api/client_api.nim b/library/api/client_api.nim index 71c2827..8dca8fa 100644 --- a/library/api/client_api.nim +++ b/library/api/client_api.nim @@ -73,7 +73,7 @@ registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[Client]): proc( configJson: cstring, chatCallbacks: ChatCallbacks ): Future[Result[string, string]] {.async.} = - ctx[].myLib[] = (await createChatClient(configJson, chatCallbacks)).valueOr: + ctx.myLib[] = (await createChatClient(configJson, chatCallbacks)).valueOr: error "CreateClientRequest failed", error = error return err($error) return ok("") @@ -88,7 +88,7 @@ proc chat_start( userData: pointer ) {.ffi.} = try: - await ctx[].myLib[].start() + await ctx.myLib[].start() return ok("") except CatchableError as e: error "chat_start failed", error = e.msg @@ -100,7 +100,7 @@ proc chat_stop( userData: pointer ) {.ffi.} = try: - await ctx[].myLib[].stop() + await ctx.myLib[].stop() return ok("") except CatchableError as e: error "chat_stop failed", error = e.msg @@ -116,7 +116,7 @@ proc chat_get_id( userData: pointer ) {.ffi.} = ## Get the client's identifier - let clientId = ctx[].myLib[].getId() + let clientId = ctx.myLib[].getId() return ok(clientId) proc chat_get_default_inbox_id( @@ -125,7 +125,7 @@ proc chat_get_default_inbox_id( userData: pointer ) {.ffi.} = ## Get the default inbox conversation ID - let inboxId = ctx[].myLib[].defaultInboxConversationId() + let inboxId = ctx.myLib[].defaultInboxConversationId() return ok(inboxId) ################################################# @@ -138,7 +138,7 @@ proc chat_list_conversations( userData: pointer ) {.ffi.} = ## List all conversations as JSON array - let convos = ctx[].myLib[].listConversations() + let convos = ctx.myLib[].listConversations() var convoList = newJArray() for convo in convos: convoList.add(%*{"id": convo.id()}) @@ -151,6 +151,6 @@ proc chat_get_conversation( convoId: cstring ) {.ffi.} = ## Get a specific conversation by ID - let convo = ctx[].myLib[].getConversation($convoId) + let convo = ctx.myLib[].getConversation($convoId) return ok($(%*{"id": convo.id()})) diff --git a/library/api/conversation_api.nim b/library/api/conversation_api.nim index 79779f4..9320eda 100644 --- a/library/api/conversation_api.nim +++ b/library/api/conversation_api.nim @@ -44,7 +44,7 @@ proc chat_new_private_conversation( let content = hexToSeqByte($contentHex) # Create the conversation - let errOpt = await ctx[].myLib[].newPrivateConversation(introBundle, content) + let errOpt = await ctx.myLib[].newPrivateConversation(introBundle, content) if errOpt.isSome(): return err("failed to create conversation: " & $errOpt.get()) @@ -68,7 +68,7 @@ proc chat_send_message( ## convoId: Conversation ID string ## contentHex: Message content as hex-encoded string try: - let convo = ctx[].myLib[].getConversation($convoId) + let convo = ctx.myLib[].getConversation($convoId) let content = hexToSeqByte($contentHex) let msgId = await convo.sendMessage(content) diff --git a/library/api/identity_api.nim b/library/api/identity_api.nim index 6d2ccdb..024d583 100644 --- a/library/api/identity_api.nim +++ b/library/api/identity_api.nim @@ -26,7 +26,7 @@ proc chat_get_identity( ) {.ffi.} = ## Get the client identity ## Returns JSON string: {"name": "...", "address": "...", "pubkey": "hex..."} - let ident = ctx[].myLib[].identity() + let ident = ctx.myLib[].identity() let identJson = %*{ "name": ident.getName(), "address": ident.getAddr(), @@ -45,7 +45,7 @@ proc chat_create_intro_bundle( ) {.ffi.} = ## Create an IntroBundle for initiating private conversations ## Returns JSON string: {"ident": "hex...", "ephemeral": "hex..."} - let bundle = ctx[].myLib[].createIntroBundle() + let bundle = ctx.myLib[].createIntroBundle() let bundleJson = %*{ "ident": bundle.ident.toHex(), "ephemeral": bundle.ephemeral.toHex() From ce0f4e2aae0aa51dcb5bcc11299f52c44c8018d1 Mon Sep 17 00:00:00 2001 From: pablo Date: Mon, 12 Jan 2026 18:16:01 +0200 Subject: [PATCH 4/4] fix: rename Client to ChatClient --- library/api/client_api.nim | 20 ++++++------- library/api/conversation_api.nim | 4 +-- library/api/identity_api.nim | 4 +-- library/declare_lib.nim | 2 +- library/libchat.nim | 12 ++++---- src/chat/client.nim | 48 ++++++++++++++++---------------- 6 files changed, 45 insertions(+), 45 deletions(-) diff --git a/library/api/client_api.nim b/library/api/client_api.nim index 8dca8fa..0edb798 100644 --- a/library/api/client_api.nim +++ b/library/api/client_api.nim @@ -26,7 +26,7 @@ type ChatCallbacks* = object proc createChatClient( configJson: cstring, chatCallbacks: ChatCallbacks -): Future[Result[Client, string]] {.async.} = +): Future[Result[ChatClient, string]] {.async.} = try: let config = parseJson($configJson) @@ -69,7 +69,7 @@ proc createChatClient( except CatchableError as e: return err("failed to create client: " & e.msg) -registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[Client]): +registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[ChatClient]): proc( configJson: cstring, chatCallbacks: ChatCallbacks ): Future[Result[string, string]] {.async.} = @@ -79,11 +79,11 @@ registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[Client]): return ok("") ################################################# -# Client Lifecycle Operations +# ChatClient Lifecycle Operations ################################################# proc chat_start( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.ffi.} = @@ -95,7 +95,7 @@ proc chat_start( return err("failed to start client: " & e.msg) proc chat_stop( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.ffi.} = @@ -107,11 +107,11 @@ proc chat_stop( return err("failed to stop client: " & e.msg) ################################################# -# Client Info Operations +# ChatClient Info Operations ################################################# proc chat_get_id( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.ffi.} = @@ -120,7 +120,7 @@ proc chat_get_id( return ok(clientId) proc chat_get_default_inbox_id( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.ffi.} = @@ -133,7 +133,7 @@ proc chat_get_default_inbox_id( ################################################# proc chat_list_conversations( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.ffi.} = @@ -145,7 +145,7 @@ proc chat_list_conversations( return ok($convoList) proc chat_get_conversation( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer, convoId: cstring diff --git a/library/api/conversation_api.nim b/library/api/conversation_api.nim index 9320eda..97d7919 100644 --- a/library/api/conversation_api.nim +++ b/library/api/conversation_api.nim @@ -19,7 +19,7 @@ logScope: ################################################# proc chat_new_private_conversation( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer, introBundleJson: cstring, @@ -58,7 +58,7 @@ proc chat_new_private_conversation( ################################################# proc chat_send_message( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer, convoId: cstring, diff --git a/library/api/identity_api.nim b/library/api/identity_api.nim index 024d583..2503242 100644 --- a/library/api/identity_api.nim +++ b/library/api/identity_api.nim @@ -20,7 +20,7 @@ logScope: ################################################# proc chat_get_identity( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.ffi.} = @@ -39,7 +39,7 @@ proc chat_get_identity( ################################################# proc chat_create_intro_bundle( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.ffi.} = diff --git a/library/declare_lib.nim b/library/declare_lib.nim index c6d1aa4..2051f37 100644 --- a/library/declare_lib.nim +++ b/library/declare_lib.nim @@ -4,7 +4,7 @@ import src/chat/client declareLibrary("chat") proc set_event_callback( - ctx: ptr FFIContext[Client], + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ) {.dynlib, exportc, cdecl.} = diff --git a/library/libchat.nim b/library/libchat.nim index 9437f7c..816419f 100644 --- a/library/libchat.nim +++ b/library/libchat.nim @@ -34,20 +34,20 @@ proc chat_new( ): pointer {.dynlib, exportc, cdecl.} = initializeLibrary() - ## Creates a new instance of the Chat Client. + ## Creates a new instance of the ChatClient. if isNil(callback): echo "error: missing callback in chat_new" return nil ## Create the Chat thread that will keep waiting for req from the main thread. - var ctx = ffi.createFFIContext[Client]().valueOr: + var ctx = ffi.createFFIContext[ChatClient]().valueOr: let msg = "Error in createFFIContext: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return nil ctx.userData = userData - proc onNewMessage(ctx: ptr FFIContext[Client]): MessageCallback = + proc onNewMessage(ctx: ptr FFIContext[ChatClient]): MessageCallback = return proc(conversation: Conversation, msg: ReceivedMessage): Future[void] {.async.} = callEventCallback(ctx, "onNewMessage"): $newJsonMessageEvent( @@ -57,12 +57,12 @@ proc chat_new( msg.timestamp ) - proc onNewConversation(ctx: ptr FFIContext[Client]): NewConvoCallback = + proc onNewConversation(ctx: ptr FFIContext[ChatClient]): NewConvoCallback = return proc(conversation: Conversation): Future[void] {.async.} = callEventCallback(ctx, "onNewConversation"): $newJsonConversationEvent(conversation.id(), "private") - proc onDeliveryAck(ctx: ptr FFIContext[Client]): DeliveryAckCallback = + proc onDeliveryAck(ctx: ptr FFIContext[ChatClient]): DeliveryAckCallback = return proc(conversation: Conversation, msgId: MessageId): Future[void] {.async.} = callEventCallback(ctx, "onDeliveryAck"): $newJsonDeliveryAckEvent(conversation.id(), msgId) @@ -83,7 +83,7 @@ proc chat_new( return ctx proc chat_destroy( - ctx: ptr FFIContext[Client], callback: FFICallBack, userData: pointer + ctx: ptr FFIContext[ChatClient], callback: FFICallBack, userData: pointer ): cint {.dynlib, exportc, cdecl.} = initializeLibrary() checkParams(ctx, callback, userData) diff --git a/src/chat/client.nim b/src/chat/client.nim index b561a62..ef3d6c4 100644 --- a/src/chat/client.nim +++ b/src/chat/client.nim @@ -47,7 +47,7 @@ type KeyEntry* = object privateKey: PrivateKey timestamp: int64 -type Client* = ref object +type ChatClient* = ref object ident: Identity ds*: WakuClient keyStore: Table[string, KeyEntry] # Keyed by HexEncoded Public Key @@ -64,9 +64,9 @@ type Client* = ref object # Constructors ################################################# -proc newClient*(ds: WakuClient, ident: Identity): Client {.raises: [IOError, +proc newClient*(ds: WakuClient, ident: Identity): ChatClient {.raises: [IOError, ValueError, SerializationError].} = - ## Creates new instance of a `Client` with a given `WakuConfig` + ## Creates new instance of a `ChatClient` with a given `WakuConfig` try: let rm = newReliabilityManager().valueOr: raise newException(ValueError, fmt"SDS InitializationError") @@ -74,7 +74,7 @@ proc newClient*(ds: WakuClient, ident: Identity): Client {.raises: [IOError, let defaultInbox = initInbox(ident) var q = QueueRef(queue: newAsyncQueue[ChatPayload](10)) - var c = Client(ident: ident, + var c = ChatClient(ident: ident, ds: ds, keyStore: initTable[string, KeyEntry](), conversations: initTable[string, Conversation](), @@ -96,17 +96,17 @@ proc newClient*(ds: WakuClient, ident: Identity): Client {.raises: [IOError, # Parameter Access ################################################# -proc getId*(client: Client): string = +proc getId*(client: ChatClient): string = result = client.ident.getName() -proc identity*(client: Client): Identity = +proc identity*(client: ChatClient): Identity = result = client.ident -proc defaultInboxConversationId*(self: Client): string = +proc defaultInboxConversationId*(self: ChatClient): string = ## Returns the default inbox address for the client. result = conversationIdFor(self.ident.getPubkey()) -proc getConversationFromHint(self: Client, +proc getConversationFromHint(self: ChatClient, conversationHint: string): Result[Option[Conversation], string] = # TODO: Implementing Hinting @@ -116,31 +116,31 @@ proc getConversationFromHint(self: Client, ok(some(self.conversations[conversationHint])) -proc listConversations*(client: Client): seq[Conversation] = +proc listConversations*(client: ChatClient): seq[Conversation] = result = toSeq(client.conversations.values()) ################################################# # Callback Handling ################################################# -proc onNewMessage*(client: Client, callback: MessageCallback) = +proc onNewMessage*(client: ChatClient, callback: MessageCallback) = client.newMessageCallbacks.add(callback) -proc notifyNewMessage*(client: Client, convo: Conversation, msg: ReceivedMessage) = +proc notifyNewMessage*(client: ChatClient, convo: Conversation, msg: ReceivedMessage) = for cb in client.newMessageCallbacks: discard cb(convo, msg) -proc onNewConversation*(client: Client, callback: NewConvoCallback) = +proc onNewConversation*(client: ChatClient, callback: NewConvoCallback) = client.newConvoCallbacks.add(callback) -proc notifyNewConversation(client: Client, convo: Conversation) = +proc notifyNewConversation(client: ChatClient, convo: Conversation) = for cb in client.newConvoCallbacks: discard cb(convo) -proc onDeliveryAck*(client: Client, callback: DeliveryAckCallback) = +proc onDeliveryAck*(client: ChatClient, callback: DeliveryAckCallback) = client.deliveryAckCallbacks.add(callback) -proc notifyDeliveryAck(client: Client, convo: Conversation, +proc notifyDeliveryAck(client: ChatClient, convo: Conversation, messageId: MessageId) = for cb in client.deliveryAckCallbacks: discard cb(convo, messageId) @@ -149,7 +149,7 @@ proc notifyDeliveryAck(client: Client, convo: Conversation, # Functional ################################################# -proc createIntroBundle*(self: var Client): IntroBundle = +proc createIntroBundle*(self: var ChatClient): IntroBundle = ## Generates an IntroBundle for the client, which includes ## the required information to send a message. @@ -175,16 +175,16 @@ proc createIntroBundle*(self: var Client): IntroBundle = # Conversation Initiation ################################################# -proc addConversation*(client: Client, convo: Conversation) = +proc addConversation*(client: ChatClient, convo: Conversation) = notice "Creating conversation", client = client.getId(), convoId = convo.id() client.conversations[convo.id()] = convo client.notifyNewConversation(convo) -proc getConversation*(client: Client, convoId: string): Conversation = +proc getConversation*(client: ChatClient, convoId: string): Conversation = notice "Get conversation", client = client.getId(), convoId = convoId result = client.conversations[convoId] -proc newPrivateConversation*(client: Client, +proc newPrivateConversation*(client: ChatClient, introBundle: IntroBundle, content: Content): Future[Option[ChatError]] {.async.} = ## Creates a private conversation with the given `IntroBundle`. ## `IntroBundles` are provided out-of-band. @@ -202,7 +202,7 @@ proc newPrivateConversation*(client: Client, # Receives a incoming payload, decodes it, and processes it. ################################################# -proc parseMessage(client: Client, msg: ChatPayload) {.raises: [ValueError, +proc parseMessage(client: ChatClient, msg: ChatPayload) {.raises: [ValueError, SerializationError].} = let envelopeRes = decode(msg.bytes, WapEnvelopeV1) if envelopeRes.isErr: @@ -231,7 +231,7 @@ proc parseMessage(client: Client, msg: ChatPayload) {.raises: [ValueError, # Async Tasks ################################################# -proc messageQueueConsumer(client: Client) {.async.} = +proc messageQueueConsumer(client: ChatClient) {.async.} = ## Main message processing loop info "Message listener started" @@ -257,8 +257,8 @@ proc messageQueueConsumer(client: Client) {.async.} = # Control Functions ################################################# -proc start*(client: Client) {.async.} = - ## Start `Client` and listens for incoming messages. +proc start*(client: ChatClient) {.async.} = + ## Start `ChatClient` and listens for incoming messages. client.ds.addDispatchQueue(client.inboundQueue) asyncSpawn client.ds.start() @@ -268,7 +268,7 @@ proc start*(client: Client) {.async.} = notice "Client start complete", client = client.getId() -proc stop*(client: Client) {.async.} = +proc stop*(client: ChatClient) {.async.} = ## Stop the client. await client.ds.stop() client.isRunning = false