// TODO: Make types for allt he proto events. This is quite messy without that. #include "include/chat.h" #include "include/session.h" #include #include #include #include #include #include static size_t next_msg_id = 1; #define CHAT_BUF_SIZE SESSION_CHAT_BUF_SIZE /* * cb_chat - libwebsockets protocol callback. * Handles connection lifecycle, incoming messages, and writable events. */ int cb_chat( struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len ) { Session** ps_p = (Session**)user; Session* sess = ps_p ? *ps_p : NULL; Session* head = session_get_head(); switch (reason) { case LWS_CALLBACK_ESTABLISHED: // New connection, create session. if (ps_p) { *ps_p = session_create(wsi); } break; case LWS_CALLBACK_RECEIVE: { // Parse inc JSON packet. if (len > CHAT_BUF_SIZE) { lwsl_warn( "Received JSON payload exceeds limit: %zu bytes\n", len ); break; } yyjson_doc* doc = yyjson_read((const char*)in, len, 0); if (!doc) break; const yyjson_val* root = yyjson_doc_get_root(doc); const yyjson_val* tval = yyjson_obj_get((yyjson_val*)root, "type"); const char* type = (tval && yyjson_is_str((yyjson_val*)tval)) ? yyjson_get_str((yyjson_val*)tval) : NULL; // JOIN. if (type && strcmp(type, "join") == 0 && sess) { const yyjson_val* data = yyjson_obj_get((yyjson_val*)root, "data"); const yyjson_val* uval = data ? yyjson_obj_get((yyjson_val*)data, "username") : NULL; // Enforce max username length. const char* raw_name = (uval && yyjson_is_str((yyjson_val*)uval)) ? yyjson_get_str((yyjson_val*)uval) : "Anonymous"; if (raw_name[0] == '\0') { raw_name = "Anonymous"; } char name_buf[SESSION_USERNAME_MAX_LEN]; size_t _i; for (_i = 0; raw_name[_i] && _i < SESSION_USERNAME_MAX_LEN - 1; ++_i) { unsigned char c = (unsigned char)raw_name[_i]; name_buf[_i] = isprint(c) ? c : '?'; } name_buf[_i] = '\0'; session_set_username(sess, name_buf); // #1: Welcome our new client. { yyjson_mut_doc* wdoc = yyjson_mut_doc_new(NULL); yyjson_mut_val* wroot = yyjson_mut_obj(wdoc); yyjson_mut_doc_set_root(wdoc, wroot); yyjson_mut_obj_add_str(wdoc, wroot, "type", "welcome"); yyjson_mut_val* wdata = yyjson_mut_obj(wdoc); yyjson_mut_obj_add_val(wdoc, wroot, "data", wdata); yyjson_mut_obj_add_uint( wdoc, wdata, "you", session_get_id(sess) ); yyjson_mut_val* wusers = yyjson_mut_arr(wdoc); yyjson_mut_obj_add_val(wdoc, wdata, "users", wusers); for (Session* s = head; s; s = s->next) { if (session_has_username(s)) { yyjson_mut_val* uobj = yyjson_mut_obj(wdoc); yyjson_mut_arr_add_val(wusers, uobj); yyjson_mut_obj_add_uint( wdoc, uobj, "id", session_get_id(s) ); yyjson_mut_obj_add_str( wdoc, uobj, "username", session_get_username(s) ); } } size_t out_len; char* out = yyjson_mut_write(wdoc, 0, &out_len); size_t copy_len = out_len < SESSION_CHAT_BUF_SIZE ? out_len : SESSION_CHAT_BUF_SIZE; sess->buf_len = copy_len; memcpy(&sess->buf[LWS_PRE], out, copy_len); lws_write( sess->wsi, &sess->buf[LWS_PRE], sess->buf_len, LWS_WRITE_TEXT ); free(out); yyjson_mut_doc_free(wdoc); } // #2: Introduce our new client to everybody else. { yyjson_mut_doc* jdoc = yyjson_mut_doc_new(NULL); yyjson_mut_val* jroot = yyjson_mut_obj(jdoc); yyjson_mut_doc_set_root(jdoc, jroot); yyjson_mut_obj_add_str(jdoc, jroot, "type", "join-event"); yyjson_mut_val* jdata = yyjson_mut_obj(jdoc); yyjson_mut_obj_add_val(jdoc, jroot, "data", jdata); yyjson_mut_obj_add_uint( jdoc, jdata, "id", session_get_id(sess) ); yyjson_mut_obj_add_str( jdoc, jdata, "username", session_get_username(sess) ); size_t out_len; char* out = yyjson_mut_write(jdoc, 0, &out_len); size_t copy_len = out_len < SESSION_CHAT_BUF_SIZE ? out_len : SESSION_CHAT_BUF_SIZE; for (Session* s = head; s; s = s->next) { s->buf_len = copy_len; memcpy(&s->buf[LWS_PRE], out, copy_len); lws_callback_on_writable(s->wsi); } free(out); yyjson_mut_doc_free(jdoc); } } // NAME. else if (type && strcmp(type, "name") == 0 && sess && session_has_username(sess)) { const yyjson_val* dataVal = root ? yyjson_obj_get((yyjson_val*)root, "data") : NULL; const yyjson_val* nval = dataVal ? yyjson_obj_get((yyjson_val*)dataVal, "username") : NULL; // Enforce max name length. const char* raw_newname = (nval && yyjson_is_str((yyjson_val*)nval)) ? yyjson_get_str((yyjson_val*)nval) : NULL; char newname_buf[SESSION_USERNAME_MAX_LEN]; if (raw_newname && raw_newname[0] != '\0') { size_t _j; for (_j = 0; raw_newname[_j] && _j < SESSION_USERNAME_MAX_LEN - 1; ++_j) { unsigned char c = (unsigned char)raw_newname[_j]; newname_buf[_j] = isprint(c) ? c : '?'; } newname_buf[_j] = '\0'; } else { // Disallow empty names. strcpy(newname_buf, session_get_username(sess)); } const char* newname = newname_buf; // Buffer old name before updating. char oldname_buf[SESSION_USERNAME_MAX_LEN]; const char* current = session_get_username(sess); if (current) { strncpy(oldname_buf, current, SESSION_USERNAME_MAX_LEN - 1); oldname_buf[SESSION_USERNAME_MAX_LEN - 1] = '\0'; } else { oldname_buf[0] = '\0'; } // Now update to new name. session_set_username(sess, newname); // Broadcast name-event to other clients. { yyjson_mut_doc* ndoc = yyjson_mut_doc_new(NULL); yyjson_mut_val* nroot = yyjson_mut_obj(ndoc); yyjson_mut_doc_set_root(ndoc, nroot); yyjson_mut_obj_add_str(ndoc, nroot, "type", "name-event"); yyjson_mut_val* ndata = yyjson_mut_obj(ndoc); yyjson_mut_obj_add_val(ndoc, nroot, "data", ndata); yyjson_mut_obj_add_uint( ndoc, ndata, "id", session_get_id(sess) ); yyjson_mut_obj_add_str(ndoc, ndata, "old", oldname_buf); yyjson_mut_obj_add_str(ndoc, ndata, "new", newname); size_t out_len; char* out = yyjson_mut_write(ndoc, 0, &out_len); size_t copy_len = out_len < SESSION_CHAT_BUF_SIZE ? out_len : SESSION_CHAT_BUF_SIZE; for (Session* s = head; s; s = s->next) { s->buf_len = copy_len; memcpy(&s->buf[LWS_PRE], out, copy_len); lws_callback_on_writable(s->wsi); } free(out); yyjson_mut_doc_free(ndoc); } } // MSG. else if (type && strcmp(type, "msg") == 0 && sess && session_has_username(sess)) { const yyjson_val* data = yyjson_obj_get((yyjson_val*)root, "data"); const yyjson_val* cval = data ? yyjson_obj_get((yyjson_val*)data, "content") : NULL; // Enforce maximum message content length const char* raw_msg = (cval && yyjson_is_str((yyjson_val*)cval)) ? yyjson_get_str((yyjson_val*)cval) : ""; char msg_buf[CHAT_BUF_SIZE]; strncpy(msg_buf, raw_msg, CHAT_BUF_SIZE - 1); msg_buf[CHAT_BUF_SIZE - 1] = '\0'; // Sanitize message to printable characters. for (size_t _k = 0; msg_buf[_k]; ++_k) { unsigned char c = (unsigned char)msg_buf[_k]; if (!isprint(c)) { msg_buf[_k] = '?'; } } const char* msg = msg_buf; // Build msg-event JSON with ID, parent and timestamp. // Get parent ID if present. const yyjson_val* dataVal = yyjson_obj_get((yyjson_val*)root, "data"); const yyjson_val* pval = dataVal ? yyjson_obj_get((yyjson_val*)dataVal, "parent") : NULL; uint64_t parent_id = (pval && yyjson_is_uint((yyjson_val*)pval)) ? yyjson_get_uint((yyjson_val*)pval) : 0; time_t now = time(NULL); size_t msg_id = next_msg_id++; // Send ack with the new message ID. { yyjson_mut_doc* ackdoc = yyjson_mut_doc_new(NULL); yyjson_mut_val* ackroot = yyjson_mut_obj(ackdoc); yyjson_mut_doc_set_root(ackdoc, ackroot); yyjson_mut_obj_add_str(ackdoc, ackroot, "type", "msg-ack"); yyjson_mut_val* ackdat = yyjson_mut_obj(ackdoc); yyjson_mut_obj_add_val(ackdoc, ackroot, "data", ackdat); yyjson_mut_obj_add_uint(ackdoc, ackdat, "id", msg_id); size_t ack_len; char* ack_out = yyjson_mut_write(ackdoc, 0, &ack_len); size_t copy_ack = ack_len < SESSION_CHAT_BUF_SIZE ? ack_len : SESSION_CHAT_BUF_SIZE; sess->buf_len = copy_ack; memcpy(&sess->buf[LWS_PRE], ack_out, copy_ack); lws_write( sess->wsi, &sess->buf[LWS_PRE], sess->buf_len, LWS_WRITE_TEXT ); free(ack_out); yyjson_mut_doc_free(ackdoc); } yyjson_mut_doc* mdoc = yyjson_mut_doc_new(NULL); yyjson_mut_val* mroot = yyjson_mut_obj(mdoc); yyjson_mut_doc_set_root(mdoc, mroot); yyjson_mut_obj_add_str(mdoc, mroot, "type", "msg-event"); yyjson_mut_val* mdat = yyjson_mut_obj(mdoc); yyjson_mut_obj_add_val(mdoc, mroot, "data", mdat); yyjson_mut_obj_add_uint(mdoc, mdat, "id", msg_id); if (parent_id) yyjson_mut_obj_add_uint(mdoc, mdat, "parent", parent_id); else yyjson_mut_obj_add_null(mdoc, mdat, "parent"); yyjson_mut_obj_add_uint(mdoc, mdat, "ts", (uint64_t)now); yyjson_mut_obj_add_str( mdoc, mdat, "username", session_get_username(sess) ); yyjson_mut_obj_add_str(mdoc, mdat, "content", msg); size_t out_len; char* out = yyjson_mut_write(mdoc, 0, &out_len); size_t copy_len = out_len < SESSION_CHAT_BUF_SIZE ? out_len : SESSION_CHAT_BUF_SIZE; for (Session* s = head; s; s = s->next) { s->buf_len = copy_len; memcpy(&s->buf[LWS_PRE], out, copy_len); lws_callback_on_writable(s->wsi); } free(out); yyjson_mut_doc_free(mdoc); // Writable events already scheduled for each session above. } yyjson_doc_free(doc); break; } case LWS_CALLBACK_SERVER_WRITEABLE: if (sess && sess->buf_len > 0) { lws_write( sess->wsi, &sess->buf[LWS_PRE], sess->buf_len, LWS_WRITE_TEXT ); sess->buf_len = 0; } break; case LWS_CALLBACK_CLOSED: // Goodbye. // TODO: Add leave event to proto. if (sess) { session_destroy(sess); } break; default: break; } return 0; }