Files
coms/server/src/chat.c
2026-02-14 16:02:53 -05:00

332 lines
14 KiB
C

// TODO: Make types for allt he proto events. This is quite messy without that.
#include "include/chat.h"
#include "include/session.h"
#include <ctype.h>
#include <libwebsockets.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <yyjson.h>
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;
}