From 21a2b2033485c106a537e5e2302f4e4d96a66b94 Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 14 Aug 2022 16:07:34 +0200 Subject: Implement XMPP connect / disconnect --- .gitignore | 2 +- README.txt | 1 + gtk.c | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ gui.c | 267 ------------------------------------------------------------ gui.h | 11 ++- limox.c | 148 +++++++++++++++++++++++++++++++++ limox.h | 8 ++ main.c | 4 +- meson.build | 2 +- 9 files changed, 435 insertions(+), 273 deletions(-) create mode 100644 gtk.c delete mode 100644 gui.c create mode 100644 limox.c create mode 100644 limox.h diff --git a/.gitignore b/.gitignore index e1ab00c..a103539 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -limox_oldrepo.tar +.local diff --git a/README.txt b/README.txt index 5efd598..c35763e 100644 --- a/README.txt +++ b/README.txt @@ -15,6 +15,7 @@ Roadmap - [ ] finish minimal viable product (MVP) to send and receive text messages - [x] static GUI widgets - [x] dynamic GUI widgets (like text messages) + - [ ] connection, stream and presence management - [ ] roster request - [ ] receiving one-to-one text messages - [ ] sending one-to-one text messages diff --git a/gtk.c b/gtk.c new file mode 100644 index 0000000..3139cbb --- /dev/null +++ b/gtk.c @@ -0,0 +1,265 @@ + + +#include +#include + +#include "limox.h" + + +struct chat { + GtkWidget* roster_item; // the button in the roster list + GtkWidget* chat_layout_box; // the layout box of the chat page + GtkWidget* chat_content_box; // the content box of the chat page + GtkWidget* text_entry; // where new messages are typed + struct chat* next; // entry to support the linked list +}; + + +// linked list of chats +static struct chat* chats = NULL; + +// the GTK application is available as global variable +static GtkApplication* app; + +// all GTK widgets are accessible in this file via global variables +static GtkWidget* window; +static GtkWidget* stack; + +// connector page +static GtkStackPage* connector_page; +static GtkWidget* connector_box; +static GtkWidget* connector_jid_label; +static GtkWidget* connector_jid_entry; +static GtkWidget* connector_pwd_label; +static GtkWidget* connector_pwd_entry; +static GtkWidget* connector_button; + +// roster page +static GtkStackPage* roster_page; +static GtkWidget* roster_layout_box; +static GtkWidget* roster_scrolled; +static GtkWidget* roster_content_box; +static GtkWidget* roster_button; + + +static void quit_cb(void) { + limox_quit(); + g_application_quit(G_APPLICATION(app)); +} + +static void connect_cb(void) { + + const char* jid_text = gtk_editable_get_text(GTK_EDITABLE(connector_jid_entry)); + const char* pwd_text = gtk_editable_get_text(GTK_EDITABLE(connector_pwd_entry)); + + gtk_stack_set_visible_child(GTK_STACK(stack), roster_layout_box); + + // just dummy output + printf("Connecting with:\nJID: %s\nPWD: %s\n", jid_text, pwd_text); + + limox_connect(jid_text, pwd_text); +} + +static void disconnect_cb(void) { + + gtk_stack_set_visible_child(GTK_STACK(stack), connector_box); + + // just dummy output + printf("Disconnected!\n"); + + limox_disconnect(); +} + +static void to_chat(GtkWidget* layout_box) { + gtk_stack_set_visible_child(GTK_STACK(stack), layout_box); +} + +static void to_roster(void) { + gtk_stack_set_visible_child(GTK_STACK(stack), roster_layout_box); +} + +void send_message(struct chat* chat) { + + // get recipient and message text + const char* recipient = gtk_button_get_label(GTK_BUTTON(chat->roster_item)); + const char* text = gtk_editable_get_text(GTK_EDITABLE(chat->text_entry)); + + // execute dummy XMPP send TODO + printf("Sending to %s:\n> %s\n", recipient, text); + + // add message content to the chat + GtkWidget* message = gtk_label_new(text); + gtk_box_append(GTK_BOX(chat->chat_content_box), message); + + // clear text input + GtkEntryBuffer* empty_buffer = gtk_entry_buffer_new("", 0); + gtk_entry_set_buffer(GTK_ENTRY(chat->text_entry), empty_buffer); +} + +void add_chat(char* jid) { + + // create chat struct and initialize + struct chat* chat = malloc(sizeof(struct chat)); + chat->roster_item = gtk_button_new_with_label(jid); + chat->chat_layout_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20); + chat->chat_content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + chat->text_entry = gtk_entry_new(); + chat->next = NULL; + + // add chat page to stack + gtk_stack_add_child(GTK_STACK(stack), chat->chat_layout_box); + + // add a button to go back to roster + GtkWidget* back = gtk_button_new_with_label("back"); + gtk_box_append(GTK_BOX(chat->chat_layout_box), back); + g_signal_connect_swapped(back, "clicked", G_CALLBACK(to_roster), NULL); + + // add a scrolled window to chat page + GtkWidget* scrolled = gtk_scrolled_window_new(); + gtk_widget_set_vexpand(scrolled, TRUE); + gtk_box_append(GTK_BOX(chat->chat_layout_box), scrolled); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled), + chat->chat_content_box); + + // add text entry and send button to chat page + gtk_box_append(GTK_BOX(chat->chat_layout_box), chat->text_entry); + GtkWidget* send_button = gtk_button_new_with_label("send"); + gtk_box_append(GTK_BOX(chat->chat_layout_box), send_button); + g_signal_connect_swapped(send_button, "clicked", G_CALLBACK(send_message), + chat); + + // add chat to roster list + gtk_box_append(GTK_BOX(roster_content_box), chat->roster_item); + g_signal_connect_swapped(chat->roster_item, "clicked", G_CALLBACK(to_chat), + chat->chat_layout_box); + + // append chat to linked list of chats + struct chat* current = chats; + while (true) { + if (current == NULL) { + chats = chat; + break; + } else if (current->next == NULL) { + current->next = chat; + break; + } else { + current = current->next; + } + } + +} + +void add_incoming_text_message(char* sender_jid, char* content) { + + // do not add message if list of chats is empty + if (chats == NULL) { + return; + } + + // find chat with corresponding JID + struct chat* chat = chats; + while (true) { + const char* jid = gtk_button_get_label(GTK_BUTTON(chat->roster_item)); + if (strcmp(jid, sender_jid) == 0) { + break; // this seems to be the correct chat + } else if (chat->next == NULL) { + return; // no matching chat found - cannot add message + } else { + chat = chat->next; // go to next chat in linked list + } + } + + // add given message content to the chat + GtkWidget* message = gtk_label_new(content); + gtk_box_append(GTK_BOX(chat->chat_content_box), message); + +} + +static void build_static_widgets(void) { + + // main window with stack + window = gtk_window_new(); + gtk_window_set_title(GTK_WINDOW(window), "LimoX"); + gtk_window_set_default_size(GTK_WINDOW(window), 800, 600); + stack = gtk_stack_new(); + gtk_window_set_child(GTK_WINDOW(window), stack); + + // connector page + connector_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20); + connector_page = gtk_stack_add_child(GTK_STACK(stack), connector_box); + connector_jid_label = gtk_label_new("XMPP address (JID)"); + gtk_box_append(GTK_BOX(connector_box), connector_jid_label); + connector_jid_entry = gtk_entry_new(); + gtk_box_append(GTK_BOX(connector_box), connector_jid_entry); + connector_pwd_label = gtk_label_new("Password"); + gtk_box_append(GTK_BOX(connector_box), connector_pwd_label); + connector_pwd_entry = gtk_password_entry_new(); + gtk_box_append(GTK_BOX(connector_box), connector_pwd_entry); + connector_button = gtk_button_new_with_label("Connect"); + gtk_box_append(GTK_BOX(connector_box), connector_button); + + // roster page + roster_layout_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20); + roster_page = gtk_stack_add_child(GTK_STACK(stack), roster_layout_box); + roster_button = gtk_button_new_with_label("Disconnect"); + gtk_box_append(GTK_BOX(roster_layout_box), roster_button); + roster_scrolled = gtk_scrolled_window_new(); + gtk_widget_set_vexpand(roster_scrolled, TRUE); + roster_content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(roster_scrolled), + roster_content_box); + gtk_box_append(GTK_BOX(roster_layout_box), roster_scrolled); + + // TODO remove this + // add dummy roster items + for (int i=0; i<10; i++) { + // char buffer[50]; + char* buffer = malloc(50*sizeof(char)); + sprintf(buffer, "contact-%d@example.com", i+1); + add_chat(buffer); + } + + // TODO just for debugging + for (int i=0; i<20; i++) { + add_incoming_text_message("contact-2@example.com", "Just a test."); + // message = gtk_label_new("Test message"); + // gtk_box_append(GTK_BOX(chat->chat_content_box), message); + } + +} + +static void activate(void) { + + build_static_widgets(); + + // configure widgets + gtk_window_set_application(GTK_WINDOW(window), app); + g_signal_connect_swapped(window, "close-request", G_CALLBACK(quit_cb), app); + g_signal_connect_swapped( + connector_button, "clicked", G_CALLBACK(connect_cb), NULL); + g_signal_connect_swapped( + roster_button, "clicked", G_CALLBACK(disconnect_cb), NULL); + + gtk_widget_show(window); +} + +static void idle_cb(void) { + + limox_run_once(); + +} + +void gui_run(void) { + +#ifdef GTK_SRCDIR + g_chdir(GTK_SRCDIR); +#endif + + app = gtk_application_new("eu.xengineering.limox", G_APPLICATION_FLAGS_NONE); + g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); + g_idle_add(G_SOURCE_FUNC(idle_cb), NULL); + + g_application_run(G_APPLICATION(app), 0, NULL); + g_object_unref(app); + +} diff --git a/gui.c b/gui.c deleted file mode 100644 index 760d66a..0000000 --- a/gui.c +++ /dev/null @@ -1,267 +0,0 @@ - - -#include -#include -#include - - -struct chat { - GtkWidget* roster_item; // the button in the roster list - GtkWidget* chat_layout_box; // the layout box of the chat page - GtkWidget* chat_content_box; // the content box of the chat page - GtkWidget* text_entry; // where new messages are typed - struct chat* next; // entry to support the linked list -}; - - -// linked list of chats -static struct chat* chats = NULL; - -// the GTK application is available as global variable -static GtkApplication* app; - -// all GTK widgets are accessible in this file via global variables -static GtkWidget* window; -static GtkWidget* stack; - -// connector page -static GtkStackPage* connector_page; -static GtkWidget* connector_box; -static GtkWidget* connector_jid_label; -static GtkWidget* connector_jid_entry; -static GtkWidget* connector_pwd_label; -static GtkWidget* connector_pwd_entry; -static GtkWidget* connector_button; - -// roster page -static GtkStackPage* roster_page; -static GtkWidget* roster_layout_box; -static GtkWidget* roster_scrolled; -static GtkWidget* roster_content_box; -static GtkWidget* roster_button; - - -static void quit_cb(void) { - g_application_quit(G_APPLICATION(app)); -} - -static void connect_cb(void) { - - const char* jid_text = gtk_editable_get_text(GTK_EDITABLE(connector_jid_entry)); - const char* pwd_text = gtk_editable_get_text(GTK_EDITABLE(connector_pwd_entry)); - - gtk_stack_set_visible_child(GTK_STACK(stack), roster_layout_box); - - // just dummy output - printf("Connecting with:\nJID: %s\nPWD: %s\n", jid_text, pwd_text); -} - -static void disconnect_cb(void) { - - gtk_stack_set_visible_child(GTK_STACK(stack), connector_box); - - // just dummy output - printf("Disconnected!\n"); -} - -static void to_chat(GtkWidget* layout_box) { - gtk_stack_set_visible_child(GTK_STACK(stack), layout_box); -} - -static void to_roster(void) { - gtk_stack_set_visible_child(GTK_STACK(stack), roster_layout_box); -} - -void send_message(struct chat* chat) { - - // get recipient and message text - const char* recipient = gtk_button_get_label(GTK_BUTTON(chat->roster_item)); - const char* text = gtk_editable_get_text(GTK_EDITABLE(chat->text_entry)); - - // execute dummy XMPP send TODO - printf("Sending to %s:\n> %s\n", recipient, text); - - // add message content to the chat - GtkWidget* message = gtk_label_new(text); - gtk_box_append(GTK_BOX(chat->chat_content_box), message); - - // clear text input - GtkEntryBuffer* empty_buffer = gtk_entry_buffer_new("", 0); - gtk_entry_set_buffer(GTK_ENTRY(chat->text_entry), empty_buffer); -} - -void add_chat(char* jid) { - - // create chat struct and initialize - struct chat* chat = malloc(sizeof(struct chat)); - chat->roster_item = gtk_button_new_with_label(jid); - chat->chat_layout_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20); - chat->chat_content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); - chat->text_entry = gtk_entry_new(); - chat->next = NULL; - - // add chat page to stack - gtk_stack_add_child(GTK_STACK(stack), chat->chat_layout_box); - - // add a button to go back to roster - GtkWidget* back = gtk_button_new_with_label("back"); - gtk_box_append(GTK_BOX(chat->chat_layout_box), back); - g_signal_connect_swapped(back, "clicked", G_CALLBACK(to_roster), NULL); - - // add a scrolled window to chat page - GtkWidget* scrolled = gtk_scrolled_window_new(); - gtk_widget_set_vexpand(scrolled, TRUE); - gtk_box_append(GTK_BOX(chat->chat_layout_box), scrolled); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled), - chat->chat_content_box); - - // add text entry and send button to chat page - gtk_box_append(GTK_BOX(chat->chat_layout_box), chat->text_entry); - GtkWidget* send_button = gtk_button_new_with_label("send"); - gtk_box_append(GTK_BOX(chat->chat_layout_box), send_button); - g_signal_connect_swapped(send_button, "clicked", G_CALLBACK(send_message), - chat); - - // add chat to roster list - gtk_box_append(GTK_BOX(roster_content_box), chat->roster_item); - g_signal_connect_swapped(chat->roster_item, "clicked", G_CALLBACK(to_chat), - chat->chat_layout_box); - - // append chat to linked list of chats - struct chat* current = chats; - while (true) { - if (current == NULL) { - chats = chat; - break; - } else if (current->next == NULL) { - current->next = chat; - break; - } else { - current = current->next; - } - } - -} - -void add_incoming_text_message(char* sender_jid, char* content) { - - // do not add message if list of chats is empty - if (chats == NULL) { - return; - } - - // find chat with corresponding JID - struct chat* chat = chats; - while (true) { - const char* jid = gtk_button_get_label(GTK_BUTTON(chat->roster_item)); - if (strcmp(jid, sender_jid) == 0) { - break; // this seems to be the correct chat - } else if (chat->next == NULL) { - return; // no matching chat found - cannot add message - } else { - chat = chat->next; // go to next chat in linked list - } - } - - // add given message content to the chat - GtkWidget* message = gtk_label_new(content); - gtk_box_append(GTK_BOX(chat->chat_content_box), message); - -} - -static void build_static_widgets(void) { - - // main window with stack - window = gtk_window_new(); - gtk_window_set_title(GTK_WINDOW(window), "LimoX"); - gtk_window_set_default_size(GTK_WINDOW(window), 800, 600); - stack = gtk_stack_new(); - gtk_window_set_child(GTK_WINDOW(window), stack); - - // connector page - connector_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20); - connector_page = gtk_stack_add_child(GTK_STACK(stack), connector_box); - connector_jid_label = gtk_label_new("XMPP address (JID)"); - gtk_box_append(GTK_BOX(connector_box), connector_jid_label); - connector_jid_entry = gtk_entry_new(); - gtk_box_append(GTK_BOX(connector_box), connector_jid_entry); - connector_pwd_label = gtk_label_new("Password"); - gtk_box_append(GTK_BOX(connector_box), connector_pwd_label); - connector_pwd_entry = gtk_password_entry_new(); - gtk_box_append(GTK_BOX(connector_box), connector_pwd_entry); - connector_button = gtk_button_new_with_label("Connect"); - gtk_box_append(GTK_BOX(connector_box), connector_button); - - // roster page - roster_layout_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20); - roster_page = gtk_stack_add_child(GTK_STACK(stack), roster_layout_box); - roster_button = gtk_button_new_with_label("Disconnect"); - gtk_box_append(GTK_BOX(roster_layout_box), roster_button); - roster_scrolled = gtk_scrolled_window_new(); - gtk_widget_set_vexpand(roster_scrolled, TRUE); - roster_content_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(roster_scrolled), - roster_content_box); - gtk_box_append(GTK_BOX(roster_layout_box), roster_scrolled); - - // TODO remove this - // add dummy roster items - for (int i=0; i<10; i++) { - // char buffer[50]; - char* buffer = malloc(50*sizeof(char)); - sprintf(buffer, "contact-%d@example.com", i+1); - add_chat(buffer); - } - - // TODO just for debugging - for (int i=0; i<20; i++) { - add_incoming_text_message("contact-2@example.com", "Just a test."); - // message = gtk_label_new("Test message"); - // gtk_box_append(GTK_BOX(chat->chat_content_box), message); - } - -} - -static void activate(void) { - - build_static_widgets(); - - // configure widgets - gtk_window_set_application(GTK_WINDOW(window), app); - g_signal_connect_swapped(window, "close-request", G_CALLBACK(quit_cb), app); - g_signal_connect_swapped( - connector_button, "clicked", G_CALLBACK(connect_cb), NULL); - g_signal_connect_swapped( - roster_button, "clicked", G_CALLBACK(disconnect_cb), NULL); - - gtk_widget_show(window); -} - -static void delay(unsigned long int micros) { - - unsigned long int now = clock(); - while ((clock() - now) < micros) {} - -} - -static void idle_cb(void) { - - // just a dummy workload - delay(1000); - -} - -void run_gui(void) { - -#ifdef GTK_SRCDIR - g_chdir(GTK_SRCDIR); -#endif - - app = gtk_application_new("eu.xengineering.limox", G_APPLICATION_FLAGS_NONE); - g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); - g_idle_add(G_SOURCE_FUNC(idle_cb), NULL); - - g_application_run(G_APPLICATION(app), 0, NULL); - g_object_unref(app); - -} diff --git a/gui.h b/gui.h index 5df3576..057e0b1 100644 --- a/gui.h +++ b/gui.h @@ -1,3 +1,8 @@ -void run_gui(void); -void add_chat(char* jid); -void add_incoming_text_message(char* sender_jid, char* content); +// interface for main.c +void gui_run(void); + +// interface for limox.c +void gui_connected(char* jid, char* password); +void gui_disconnected(void); +void gui_suspended(void); +void gui_resumed(void); diff --git a/limox.c b/limox.c new file mode 100644 index 0000000..618c2ef --- /dev/null +++ b/limox.c @@ -0,0 +1,148 @@ + + +#include +#include +#include +#include +#include +#include + +#include + +#include + + +// the state of limox +typedef enum { + DISCONNECTED, // initial state + CONNECTING, // connection was requested but is not yet established + CONNECTED, // there is an active XMPP connetion to the server + SUSPENDED // not connected, but there is an open XMPP stream to be + // reconnected (see XEP-0198 for details) +} limox_state_t; + + +// these variables stay initialized for the whole runtime +static limox_state_t state; +static xmpp_log_t* log; +static xmpp_ctx_t* ctx; + +// these variables stay initialized while the application is not disconnected +static xmpp_conn_t* conn; +static long flags; + +// this variable stays initialized while the connection is suspended +static xmpp_sm_state_t* sm_state; + +static void conn_handler(xmpp_conn_t *conn, xmpp_conn_event_t status,int error, + xmpp_stream_error_t *stream_error, void *userdata) { + + if (status == XMPP_CONN_CONNECT) { + printf("DEBUG: connected\n"); + + // send initial presence + xmpp_stanza_t* presence; + presence = xmpp_presence_new(ctx); + xmpp_send(conn, presence); + xmpp_stanza_release(presence); + } else if (status == XMPP_CONN_DISCONNECT) { + printf("got XMPP_CONN_DISCONNECT\n"); + } else { + printf("Unhandled connection event!\n"); + } +} + +void limox_init(void) { + + printf("limox_init()\n"); + + xmpp_initialize(); + log = xmpp_get_default_logger(XMPP_LEVEL_DEBUG); // or NULL for silence + ctx = xmpp_ctx_new(NULL, log); + + state = DISCONNECTED; + +} + +static void delay(unsigned long int micros) { + + unsigned long int now = clock(); + while ((clock() - now) < micros) {} + +} + +void limox_run_once(void) { + + if (state == CONNECTING) { + if (xmpp_connect_client(conn, NULL, 0, conn_handler, NULL) + == XMPP_EOK) { + state = CONNECTED; + } else { + limox_disconnect(); + } + } + + if (state != DISCONNECTED) { + xmpp_run_once(ctx, 200); + } + + // TODO This delay is useless. But without it this function gets only + // called once by GTK. This could be a GTK bug. + delay(10000); + +} + +void limox_quit(void) { + + printf("limox_quit()\n"); + + limox_disconnect(); + // TODO is it possible to free xmpp_log_t ? + log = NULL; + xmpp_ctx_free(ctx); + xmpp_shutdown(); + +} + +void limox_connect(const char* jid, const char* password) { + + printf("limox_connect()\n"); + + conn = xmpp_conn_new(ctx); + + flags = 0; + flags |= XMPP_CONN_FLAG_MANDATORY_TLS; + xmpp_conn_set_flags(conn, flags); + + xmpp_conn_set_jid(conn, jid); + xmpp_conn_set_pass(conn, password); + + state = CONNECTING; + +} + +void limox_disconnect(void) { + + printf("limox_disconnect()\n"); + + if (sm_state) { + xmpp_free_sm_state(sm_state); + sm_state = NULL; + } + + if (conn != NULL && xmpp_conn_is_connected(conn)) { + xmpp_disconnect(conn); + while (xmpp_conn_is_connected(conn)) { + xmpp_run_once(ctx, 200); + } + xmpp_conn_release(conn); + } + + // TODO free conn + conn = NULL; + + flags = 0; + + state = DISCONNECTED; + +} diff --git a/limox.h b/limox.h new file mode 100644 index 0000000..90a3b76 --- /dev/null +++ b/limox.h @@ -0,0 +1,8 @@ +// life cycle / event loop related functions +void limox_init(void); // initialize limox +void limox_run_once(void); // process event loop for a short amount of time +void limox_quit(void); // disconnect and clean up + +// interface for the GUI implementation +void limox_connect(const char* jid, const char* password); +void limox_disconnect(void); diff --git a/main.c b/main.c index 2d2b330..c94b023 100644 --- a/main.c +++ b/main.c @@ -5,6 +5,7 @@ #include #include "gui.h" +#include "limox.h" // error code definition @@ -37,7 +38,8 @@ int main(int argc, char* argv[]) { if (opts.unknown || opts.help) { print_help(); } else { - run_gui(); + limox_init(); + gui_run(); } } diff --git a/meson.build b/meson.build index a743460..34a33e4 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ project('LimoX', 'c') gtkdep = dependency('gtk4') strophedep = dependency('libstrophe') -executable('limox', ['main.c', 'gui.c'], dependencies : [gtkdep, strophedep]) +executable('limox', ['main.c', 'gtk.c', 'limox.c'], dependencies : [gtkdep, strophedep]) -- cgit v1.2.3-70-g09d2