Things.
This commit is contained in:
3
client/start.sh
Executable file
3
client/start.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
npm start
|
||||
@@ -11,7 +11,6 @@ CC = clang -std=c23
|
||||
LINK = clang
|
||||
CFLAGS = -Wall -DDBG -ggdb -fsanitize=leak -I$(INC_DIR) -I$(SRC_DIR)
|
||||
LDFLAGS = -lwebsockets
|
||||
LDFLAGS += -lsqlite3
|
||||
PRINT = echo -e
|
||||
|
||||
SRC_FILES = $(wildcard $(SRC_DIR)/*.c)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "include/chat.h"
|
||||
#include "include/api.h"
|
||||
#include "include/data.h"
|
||||
#include "include/db.h"
|
||||
#include "include/session.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
@@ -11,9 +10,33 @@
|
||||
#include <time.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
static const char DEFAULT_ROOM[] = "global";
|
||||
MsgData* chat_history[CHAT_HISTORY_SZ] = {NULL};
|
||||
size_t chat_history_head = 0; // Next insertion index.
|
||||
size_t chat_history_count = 0; // Number of valid entries.
|
||||
static MsgID next_msg_id = 1;
|
||||
|
||||
// Legacy in-memory ring removed; history is now persisted in SQLite.
|
||||
MsgData* chat_history_msg_add(MsgData* msg) {
|
||||
chat_history[chat_history_head] = msg;
|
||||
chat_history_head = (chat_history_head + 1) % CHAT_HISTORY_SZ;
|
||||
if (chat_history_count < CHAT_HISTORY_SZ) { chat_history_count++; }
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
MsgData** chat_history_nice(void) {
|
||||
MsgData** msgs = calloc(CHAT_HISTORY_SZ, sizeof(MsgData*));
|
||||
|
||||
// Oldest entry is head - count (mod size).
|
||||
size_t start = (chat_history_head + CHAT_HISTORY_SZ - chat_history_count) %
|
||||
CHAT_HISTORY_SZ;
|
||||
|
||||
for (size_t j = 0; j < chat_history_count; j++) {
|
||||
size_t idx = (start + j) % CHAT_HISTORY_SZ;
|
||||
msgs[j] = chat_history[idx];
|
||||
}
|
||||
|
||||
return msgs;
|
||||
}
|
||||
|
||||
// Parse a raw packet.
|
||||
Packet* packet_parse(const char* in, size_t len) {
|
||||
@@ -46,9 +69,6 @@ void do_join(Session* sess, Packet* packet) {
|
||||
session_set_name(sess, name);
|
||||
}
|
||||
|
||||
#define WELCOME_HISTORY_LIMIT 50
|
||||
#define HISTORY_LIMIT_MAX 200
|
||||
|
||||
// Do a welcome packet.
|
||||
void do_welcome(Session* sess) {
|
||||
// Build list of online users (only sessions with a name).
|
||||
@@ -65,20 +85,10 @@ void do_welcome(Session* sess) {
|
||||
oi++;
|
||||
}
|
||||
|
||||
// Fetch latest messages from DB, then reverse to oldest-first.
|
||||
MsgData** history = NULL;
|
||||
// Build history list.
|
||||
MsgData** history = chat_history_nice();
|
||||
size_t historyc = 0;
|
||||
if (db_fetch_messages(
|
||||
DEFAULT_ROOM, 0, WELCOME_HISTORY_LIMIT, &history, &historyc
|
||||
) != 0) {
|
||||
history = NULL;
|
||||
historyc = 0;
|
||||
}
|
||||
for (size_t i = 0; i < historyc / 2; i++) {
|
||||
MsgData* tmp = history[i];
|
||||
history[i] = history[historyc - 1 - i];
|
||||
history[historyc - 1 - i] = tmp;
|
||||
}
|
||||
while (historyc < CHAT_HISTORY_SZ && history[historyc]) historyc++;
|
||||
|
||||
PacketWelcome data = {
|
||||
.id = session_get_id(sess),
|
||||
@@ -93,84 +103,7 @@ void do_welcome(Session* sess) {
|
||||
session_send(sess, packet);
|
||||
free(packet);
|
||||
free(online);
|
||||
if (history) {
|
||||
for (size_t i = 0; i < historyc; i++) free(history[i]);
|
||||
free(history);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle a history request packet.
|
||||
static void do_history_req(Session* sess, Packet* packet) {
|
||||
if (!sess || !packet) return;
|
||||
|
||||
const yyjson_val* data = (const yyjson_val*)packet->data;
|
||||
const yyjson_val* jroom = data ? yyjson_obj_get(data, "room") : NULL;
|
||||
const char* room_str =
|
||||
(jroom && yyjson_is_str(jroom)) ? yyjson_get_str(jroom) : DEFAULT_ROOM;
|
||||
if (!room_verify(room_str)) room_str = DEFAULT_ROOM;
|
||||
|
||||
MsgID before = 0;
|
||||
const yyjson_val* jbefore = data ? yyjson_obj_get(data, "before") : NULL;
|
||||
if (jbefore) {
|
||||
if (yyjson_is_int(jbefore)) {
|
||||
int64_t v = yyjson_get_int(jbefore);
|
||||
if (v > 0) before = (MsgID)v;
|
||||
} else if (yyjson_is_str(jbefore)) {
|
||||
const char* s = yyjson_get_str(jbefore);
|
||||
char* endp = NULL;
|
||||
unsigned long long tmp = strtoull(s, &endp, 10);
|
||||
if (endp && *endp == '\0') before = (MsgID)tmp;
|
||||
}
|
||||
}
|
||||
|
||||
size_t limit = WELCOME_HISTORY_LIMIT;
|
||||
const yyjson_val* jlimit = data ? yyjson_obj_get(data, "limit") : NULL;
|
||||
if (jlimit && yyjson_is_int(jlimit)) {
|
||||
int64_t v = yyjson_get_int(jlimit);
|
||||
if (v > 0 && v <= HISTORY_LIMIT_MAX) limit = (size_t)v;
|
||||
}
|
||||
|
||||
MsgData** rows = NULL;
|
||||
size_t count = 0;
|
||||
if (db_fetch_messages(room_str, before, limit + 1, &rows, &count) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool has_more = false;
|
||||
if (count > limit) {
|
||||
has_more = true;
|
||||
free(rows[count - 1]);
|
||||
rows[count - 1] = NULL;
|
||||
count = limit;
|
||||
}
|
||||
|
||||
// reverse to oldest-first
|
||||
for (size_t i = 0; i < count / 2; i++) {
|
||||
MsgData* tmp = rows[i];
|
||||
rows[i] = rows[count - 1 - i];
|
||||
rows[count - 1 - i] = tmp;
|
||||
}
|
||||
|
||||
MsgID oldest_id = count ? rows[0]->id : 0;
|
||||
|
||||
PacketHistoryRes res = {
|
||||
.room = {0},
|
||||
.historyc = count,
|
||||
.history = rows,
|
||||
.has_more = has_more,
|
||||
.oldest_id = oldest_id
|
||||
};
|
||||
strncpy(res.room, room_str, ROOM_MAX_LENGTH - 1);
|
||||
res.room[ROOM_MAX_LENGTH - 1] = '\0';
|
||||
|
||||
Packet* out = packet_init(PACKET_TYPE_HISTORY_RES, &res);
|
||||
session_send(sess, out);
|
||||
free(out);
|
||||
|
||||
if (rows) {
|
||||
for (size_t i = 0; i < count; i++) free(rows[i]);
|
||||
free(rows);
|
||||
}
|
||||
free(history);
|
||||
}
|
||||
|
||||
// Do a welcome packet.
|
||||
@@ -255,18 +188,15 @@ void do_msg(Session* sess, Packet* packet) {
|
||||
}
|
||||
}
|
||||
|
||||
MsgData* msg = calloc(1, sizeof(MsgData));
|
||||
MsgData* msg = malloc(sizeof(MsgData));
|
||||
if (!msg) {
|
||||
lwsl_err("Failed to allocate MsgData.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
msg->id = next_msg_id++;
|
||||
msg->author.id = session_get_id(sess);
|
||||
msg->author.name = session_get_name(sess);
|
||||
strncpy(msg->author_name, *msg->author.name, NAME_MAX_LENGTH - 1);
|
||||
msg->author_name[NAME_MAX_LENGTH - 1] = '\0';
|
||||
strncpy(msg->room, DEFAULT_ROOM, ROOM_MAX_LENGTH - 1);
|
||||
msg->room[ROOM_MAX_LENGTH - 1] = '\0';
|
||||
msg->parent = parent;
|
||||
strncpy(msg->content, content, MSG_MAX_LENGTH - 1);
|
||||
msg->content[MSG_MAX_LENGTH - 1] = '\0';
|
||||
@@ -278,14 +208,7 @@ void do_msg(Session* sess, Packet* packet) {
|
||||
(msg->parent == UINT64_MAX ? "null" : "")
|
||||
);
|
||||
|
||||
if (db_insert_message(msg, &msg->id) != 0) {
|
||||
ack.status = "db_error";
|
||||
Packet* p = packet_init(PACKET_TYPE_MSG_ACK, &ack);
|
||||
session_send(sess, p);
|
||||
free(p);
|
||||
free(msg);
|
||||
return;
|
||||
}
|
||||
chat_history_msg_add(msg);
|
||||
|
||||
ack.id = msg->id;
|
||||
Packet* ackp = packet_init(PACKET_TYPE_MSG_ACK, &ack);
|
||||
@@ -295,7 +218,6 @@ void do_msg(Session* sess, Packet* packet) {
|
||||
Packet* evt = packet_init(PACKET_TYPE_MSG_EVT, msg);
|
||||
session_send_all(evt);
|
||||
free(evt);
|
||||
free(msg);
|
||||
}
|
||||
|
||||
static void do_ping(Session* sess) {
|
||||
@@ -427,9 +349,6 @@ int cb_chat(
|
||||
free(ackp);
|
||||
break;
|
||||
}
|
||||
case PACKET_TYPE_HISTORY_REQ:
|
||||
do_history_req(sess, packet);
|
||||
break;
|
||||
case PACKET_TYPE_PONG:
|
||||
// Client responded; nothing else to do (timer continues).
|
||||
if (sess) {
|
||||
|
||||
201
server/src/db.c
201
server/src/db.c
@@ -1,201 +0,0 @@
|
||||
#include "include/db.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <libwebsockets.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static sqlite3* db = NULL;
|
||||
|
||||
static int ensure_dir(const char* path) {
|
||||
struct stat st = {0};
|
||||
if (stat(path, &st) == -1) {
|
||||
if (mkdir(path, 0755) == -1 && errno != EEXIST) return -1;
|
||||
} else if (!S_ISDIR(st.st_mode)) {
|
||||
errno = ENOTDIR;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int exec_simple(const char* sql) {
|
||||
char* err = NULL;
|
||||
int rc = sqlite3_exec(db, sql, NULL, NULL, &err);
|
||||
if (rc != SQLITE_OK) {
|
||||
lwsl_err("SQLite exec failed: %s\n", err ? err : "(null)");
|
||||
sqlite3_free(err);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int db_init(const char* path) {
|
||||
if (db) return 0;
|
||||
|
||||
// Ensure parent directory exists.
|
||||
char dir[256];
|
||||
const char* slash = strrchr(path, '/');
|
||||
size_t dlen = slash ? (size_t)(slash - path) : 0;
|
||||
if (dlen >= sizeof(dir)) return -1;
|
||||
if (dlen > 0) {
|
||||
memcpy(dir, path, dlen);
|
||||
dir[dlen] = '\0';
|
||||
if (ensure_dir(dir) != 0) {
|
||||
lwsl_err(
|
||||
"Failed to create data dir %s: %s\n", dir, strerror(errno)
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (sqlite3_open(path, &db) != SQLITE_OK) {
|
||||
lwsl_err("Failed to open db at %s: %s\n", path, sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
|
||||
exec_simple("PRAGMA journal_mode=WAL;");
|
||||
exec_simple("PRAGMA synchronous=NORMAL;");
|
||||
exec_simple("PRAGMA foreign_keys=ON;");
|
||||
|
||||
const char* create_messages = "CREATE TABLE IF NOT EXISTS messages ("
|
||||
" id INTEGER PRIMARY KEY,"
|
||||
" room TEXT NOT NULL,"
|
||||
" author_id INTEGER NOT NULL,"
|
||||
" author_name TEXT NOT NULL,"
|
||||
" parent INTEGER,"
|
||||
" content TEXT NOT NULL,"
|
||||
" timestamp INTEGER NOT NULL,"
|
||||
" deleted_at INTEGER"
|
||||
");";
|
||||
|
||||
const char* create_idx_ts =
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_room_ts "
|
||||
"ON messages(room, timestamp DESC);";
|
||||
const char* create_idx_id =
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_room_id "
|
||||
"ON messages(room, id DESC);";
|
||||
|
||||
if (exec_simple(create_messages) != SQLITE_OK) return -1;
|
||||
if (exec_simple(create_idx_ts) != SQLITE_OK) return -1;
|
||||
if (exec_simple(create_idx_id) != SQLITE_OK) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void db_close(void) {
|
||||
if (db) {
|
||||
sqlite3_close(db);
|
||||
db = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int db_insert_message(const MsgData* msg, MsgID* out_id) {
|
||||
if (!db || !msg) return -1;
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO messages "
|
||||
"(room, author_id, author_name, parent, content, timestamp) "
|
||||
"VALUES (?1, ?2, ?3, ?4, ?5, ?6);";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
lwsl_err("prepare insert failed: %s\n", sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, msg->room, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)msg->author.id);
|
||||
sqlite3_bind_text(
|
||||
stmt, 3, (msg->author.name ? *msg->author.name : msg->author_name), -1,
|
||||
SQLITE_STATIC
|
||||
);
|
||||
if (msg->parent == UINT64_MAX) {
|
||||
sqlite3_bind_null(stmt, 4);
|
||||
} else {
|
||||
sqlite3_bind_int64(stmt, 4, (sqlite3_int64)msg->parent);
|
||||
}
|
||||
sqlite3_bind_text(stmt, 5, msg->content, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)msg->timestamp);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
lwsl_err("insert failed: %s\n", sqlite3_errmsg(db));
|
||||
sqlite3_finalize(stmt);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (out_id) { *out_id = (MsgID)sqlite3_last_insert_rowid(db); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int db_fetch_messages(
|
||||
const char* room, MsgID before_id, size_t limit, MsgData*** out_msgs,
|
||||
size_t* out_count
|
||||
) {
|
||||
if (!db || !room || !out_msgs || !out_count) return -1;
|
||||
|
||||
const char* sql = "SELECT id, room, author_id, author_name, "
|
||||
" parent, content, timestamp "
|
||||
"FROM messages "
|
||||
"WHERE room = ?1 "
|
||||
" AND deleted_at IS NULL "
|
||||
" AND (?2 = 0 OR id < ?2) "
|
||||
"ORDER BY id DESC "
|
||||
"LIMIT ?3;";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
lwsl_err("prepare select failed: %s\n", sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, room, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)before_id);
|
||||
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)limit);
|
||||
|
||||
size_t cap = limit;
|
||||
MsgData** rows = calloc(cap ? cap : 1, sizeof(MsgData*));
|
||||
size_t count = 0;
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
MsgData* m = calloc(1, sizeof(MsgData));
|
||||
m->id = (MsgID)sqlite3_column_int64(stmt, 0);
|
||||
const unsigned char* r = sqlite3_column_text(stmt, 1);
|
||||
if (r) {
|
||||
strncpy(m->room, (const char*)r, ROOM_MAX_LENGTH - 1);
|
||||
m->room[ROOM_MAX_LENGTH - 1] = '\0';
|
||||
}
|
||||
m->author.id = (UserID)sqlite3_column_int64(stmt, 2);
|
||||
const unsigned char* an = sqlite3_column_text(stmt, 3);
|
||||
if (an) {
|
||||
strncpy(m->author_name, (const char*)an, NAME_MAX_LENGTH - 1);
|
||||
m->author_name[NAME_MAX_LENGTH - 1] = '\0';
|
||||
m->author.name = &m->author_name;
|
||||
}
|
||||
if (sqlite3_column_type(stmt, 4) == SQLITE_NULL) {
|
||||
m->parent = UINT64_MAX;
|
||||
} else {
|
||||
m->parent = (MsgID)sqlite3_column_int64(stmt, 4);
|
||||
}
|
||||
const unsigned char* c = sqlite3_column_text(stmt, 5);
|
||||
if (c) {
|
||||
strncpy(m->content, (const char*)c, MSG_MAX_LENGTH - 1);
|
||||
m->content[MSG_MAX_LENGTH - 1] = '\0';
|
||||
}
|
||||
m->timestamp = (time_t)sqlite3_column_int64(stmt, 6);
|
||||
|
||||
rows[count++] = m;
|
||||
if (count == cap) break;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
*out_msgs = rows;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
@@ -7,7 +7,19 @@
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
// Message history (ring).
|
||||
#define CHAT_HISTORY_SZ 128
|
||||
extern MsgData* chat_history[CHAT_HISTORY_SZ];
|
||||
extern size_t chat_history_head; // Next insertion index.
|
||||
extern size_t chat_history_count; // Number of valid history entries.
|
||||
|
||||
// Add message to history ring.
|
||||
MsgData* chat_history_msg_add(MsgData* msg);
|
||||
|
||||
// Get a list of messages in order.
|
||||
MsgData** chat_history_nice(void);
|
||||
|
||||
/**
|
||||
* cb_chat - libwebsockets protocol callback for COMS chat.
|
||||
*
|
||||
* This function is registered in the protocols array passed to the
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#ifndef DB__H
|
||||
#define DB__H
|
||||
|
||||
#include "data.h"
|
||||
#include <sqlite3.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Initialize the SQLite database at the given path. Creates directories/tables.
|
||||
int db_init(const char* path);
|
||||
|
||||
// Close the global DB handle.
|
||||
void db_close(void);
|
||||
|
||||
// Insert a message; on success sets *out_id to the assigned rowid.
|
||||
int db_insert_message(const MsgData* msg, MsgID* out_id);
|
||||
|
||||
// Fetch newest-first messages for a room, optionally before a given id.
|
||||
// Returns an allocated array of MsgData* in *out_msgs (caller must free each
|
||||
// MsgData* and the array). Count in *out_count. If before_id==0, fetch latest.
|
||||
int db_fetch_messages(
|
||||
const char* room, MsgID before_id, size_t limit, MsgData*** out_msgs,
|
||||
size_t* out_count
|
||||
);
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "include/chat.h"
|
||||
#include "include/db.h"
|
||||
#include "include/session.h"
|
||||
|
||||
#include <libwebsockets.h>
|
||||
@@ -30,11 +29,6 @@ static struct lws_protocols protocols[] = {
|
||||
int main(void) {
|
||||
signal(SIGINT, handle_sigint);
|
||||
|
||||
if (db_init("var/data/chat.sqlite") != 0) {
|
||||
fprintf(stderr, "Failed to initialize database.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Create libws context.
|
||||
struct lws_context_creation_info info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
@@ -55,7 +49,6 @@ int main(void) {
|
||||
|
||||
// Cleanse.
|
||||
lws_context_destroy(context);
|
||||
db_close();
|
||||
printf("Server shutting down.\n");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
1
server/start.sh
Executable file
1
server/start.sh
Executable file
@@ -0,0 +1 @@
|
||||
make clean all run
|
||||
Reference in New Issue
Block a user