Some tyhigns.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,9 @@ server/.cache/
|
|||||||
# Node dependencies.
|
# Node dependencies.
|
||||||
client/node_modules/
|
client/node_modules/
|
||||||
|
|
||||||
|
# Client server config files.
|
||||||
|
client/domain.txt
|
||||||
|
|
||||||
# Logs.
|
# Logs.
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|||||||
@@ -29,118 +29,127 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const defaultName = "Anon";
|
const defaultName = "Anon";
|
||||||
const MAX_NAME_LEN = 15; // server limit is NAME_MAX_LENGTH-1 (15 chars)
|
const MAX_NAME_LEN = 15; // server limit is NAME_MAX_LENGTH-1 (15 chars)
|
||||||
|
|
||||||
const { socket, sendMessage, sendName } = initWebSocket(
|
// Fetch WebSocket domain from the client server so deployments can point to a
|
||||||
defaultName,
|
// remote websocket host without rebuilding the bundle.
|
||||||
chat,
|
fetch("/ws-domain")
|
||||||
inputContainer,
|
.then((res) => res.json())
|
||||||
usersList,
|
.then(({ domain }) => domain || "")
|
||||||
);
|
.catch(() => "")
|
||||||
|
.then((wsDomain) => {
|
||||||
// Renames.
|
const { socket, sendMessage, sendName } = initWebSocket(
|
||||||
nameButton.addEventListener("click", () => {
|
defaultName,
|
||||||
const newName = nameInput.value.trim() || defaultName;
|
chat,
|
||||||
if (newName.length > MAX_NAME_LEN) {
|
inputContainer,
|
||||||
addNotice(
|
usersList,
|
||||||
`<span class="err">Name may be at most ${MAX_NAME_LEN} characters.</span>`,
|
wsDomain,
|
||||||
);
|
);
|
||||||
renderChat(chat, inputContainer);
|
|
||||||
nameInput.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendName(newName);
|
// Renames.
|
||||||
updateUser(getMyId(), newName);
|
nameButton.addEventListener("click", () => {
|
||||||
const groups = getGroupedUsers();
|
const newName = nameInput.value.trim() || defaultName;
|
||||||
usersList.innerHTML = "Online: " + groups.join(", ");
|
if (newName.length > MAX_NAME_LEN) {
|
||||||
|
addNotice(
|
||||||
|
`<span class="err">Name may be at most ${MAX_NAME_LEN} characters.</span>`,
|
||||||
|
);
|
||||||
|
renderChat(chat, inputContainer);
|
||||||
|
nameInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
nameInput.value = "";
|
sendName(newName);
|
||||||
nameInput.placeholder = `Name: ${newName}`;
|
updateUser(getMyId(), newName);
|
||||||
input.focus();
|
const groups = getGroupedUsers();
|
||||||
});
|
usersList.innerHTML = "Online: " + groups.join(", ");
|
||||||
|
|
||||||
// Enter to set name.
|
nameInput.value = "";
|
||||||
nameInput.addEventListener("keydown", (e) => {
|
nameInput.placeholder = `Name: ${newName}`;
|
||||||
if (e.key === "Enter") nameButton.click();
|
input.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Button to send.
|
// Enter to set name.
|
||||||
sendButton.addEventListener("click", () => {
|
nameInput.addEventListener("keydown", (e) => {
|
||||||
const text = input.value.trim();
|
if (e.key === "Enter") nameButton.click();
|
||||||
if (!text) return;
|
});
|
||||||
sendMessage(text);
|
|
||||||
input.value = "";
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enter to send and escape to go to root.
|
// Button to send.
|
||||||
input.addEventListener("keydown", (e) => {
|
sendButton.addEventListener("click", () => {
|
||||||
if (e.key === "Enter") {
|
const text = input.value.trim();
|
||||||
sendButton.click();
|
if (!text) return;
|
||||||
} else if (e.key === "Escape") {
|
sendMessage(text);
|
||||||
setReplyTo(null);
|
input.value = "";
|
||||||
input.placeholder = "";
|
input.focus();
|
||||||
renderChat(chat, inputContainer);
|
});
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click message to reply.
|
// Enter to send and escape to go to root.
|
||||||
chat.addEventListener("click", (e) => {
|
input.addEventListener("keydown", (e) => {
|
||||||
const msgDiv = e.target.closest(".msg");
|
if (e.key === "Enter") {
|
||||||
if (!msgDiv) return;
|
sendButton.click();
|
||||||
const id = msgDiv.dataset.id;
|
} else if (e.key === "Escape") {
|
||||||
if (!id) return;
|
setReplyTo(null);
|
||||||
const threads = getThreads();
|
input.placeholder = "";
|
||||||
const author = threads.get(id)?.username;
|
renderChat(chat, inputContainer);
|
||||||
setReplyTo(id);
|
input.focus();
|
||||||
setFocused(id);
|
}
|
||||||
input.placeholder = author ? `Replying to @${author}` : "";
|
});
|
||||||
renderChat(chat, inputContainer);
|
|
||||||
input.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Go back to root.
|
// Click message to reply.
|
||||||
rootButton.addEventListener("click", () => {
|
chat.addEventListener("click", (e) => {
|
||||||
setReplyTo(null);
|
const msgDiv = e.target.closest(".msg");
|
||||||
clearFocused();
|
if (!msgDiv) return;
|
||||||
input.placeholder = "";
|
const id = msgDiv.dataset.id;
|
||||||
renderChat(chat, inputContainer);
|
if (!id) return;
|
||||||
input.focus();
|
const threads = getThreads();
|
||||||
});
|
const author = threads.get(id)?.username;
|
||||||
|
setReplyTo(id);
|
||||||
|
setFocused(id);
|
||||||
|
input.placeholder = author ? `Replying to @${author}` : "";
|
||||||
|
renderChat(chat, inputContainer);
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
|
||||||
// Global Escape handler: clear reply context, focus input, and clear focused message.
|
// Go back to root.
|
||||||
document.addEventListener("keydown", (e) => {
|
rootButton.addEventListener("click", () => {
|
||||||
if (e.key === "Escape") {
|
setReplyTo(null);
|
||||||
setReplyTo(null);
|
clearFocused();
|
||||||
clearFocused();
|
input.placeholder = "";
|
||||||
input.placeholder = "";
|
renderChat(chat, inputContainer);
|
||||||
renderChat(chat, inputContainer);
|
input.focus();
|
||||||
input.focus();
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Arrow key navigation for focused messages (also sets reply target).
|
// Global Escape handler: clear reply context, focus input, and clear focused message.
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
if (e.key === "Escape") {
|
||||||
const msgs = Array.from(chat.querySelectorAll("div.msg"));
|
setReplyTo(null);
|
||||||
if (!msgs.length) return;
|
clearFocused();
|
||||||
let idx = msgs.findIndex((div) => div.dataset.id === getFocused());
|
input.placeholder = "";
|
||||||
if (idx === -1) {
|
renderChat(chat, inputContainer);
|
||||||
idx = e.key === "ArrowDown" ? -1 : msgs.length;
|
input.focus();
|
||||||
}
|
}
|
||||||
idx =
|
});
|
||||||
e.key === "ArrowDown"
|
|
||||||
? Math.min(msgs.length - 1, idx + 1)
|
// Arrow key navigation for focused messages (also sets reply target).
|
||||||
: Math.max(0, idx - 1);
|
document.addEventListener("keydown", (e) => {
|
||||||
const newId = msgs[idx].dataset.id;
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||||
setFocused(newId);
|
const msgs = Array.from(chat.querySelectorAll("div.msg"));
|
||||||
setReplyTo(newId); // Move input box under focused message.
|
if (!msgs.length) return;
|
||||||
const threads = getThreads();
|
let idx = msgs.findIndex((div) => div.dataset.id === getFocused());
|
||||||
const author = threads.get(newId)?.username;
|
if (idx === -1) {
|
||||||
input.placeholder = author ? `Replying to @${author}` : "";
|
idx = e.key === "ArrowDown" ? -1 : msgs.length;
|
||||||
renderChat(chat, inputContainer);
|
}
|
||||||
const newDiv = chat.querySelector(`div[data-id="${newId}"]`);
|
idx =
|
||||||
if (newDiv) newDiv.scrollIntoView({ block: "nearest" });
|
e.key === "ArrowDown"
|
||||||
}
|
? Math.min(msgs.length - 1, idx + 1)
|
||||||
});
|
: Math.max(0, idx - 1);
|
||||||
|
const newId = msgs[idx].dataset.id;
|
||||||
|
setFocused(newId);
|
||||||
|
setReplyTo(newId); // Move input box under focused message.
|
||||||
|
const threads = getThreads();
|
||||||
|
const author = threads.get(newId)?.username;
|
||||||
|
input.placeholder = author ? `Replying to @${author}` : "";
|
||||||
|
renderChat(chat, inputContainer);
|
||||||
|
const newDiv = chat.querySelector(`div[data-id="${newId}"]`);
|
||||||
|
if (newDiv) newDiv.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,14 +27,20 @@ import { renderChat } from "./render.js";
|
|||||||
* @param {HTMLElement} chatEl - The container element for chat entries.
|
* @param {HTMLElement} chatEl - The container element for chat entries.
|
||||||
* @param {HTMLElement} inputContainer - The message input area element.
|
* @param {HTMLElement} inputContainer - The message input area element.
|
||||||
* @param {HTMLElement} usersListEl - The element displaying the online users.
|
* @param {HTMLElement} usersListEl - The element displaying the online users.
|
||||||
|
* @param {string} wsDomain - Host (and optional port) for the WebSocket server.
|
||||||
* @returns {{ socket: WebSocket, sendMessage: (content: string) => void }}
|
* @returns {{ socket: WebSocket, sendMessage: (content: string) => void }}
|
||||||
*/
|
*/
|
||||||
export function initWebSocket(username, chatEl, inputContainer, usersListEl) {
|
export function initWebSocket(
|
||||||
const socket = new WebSocket( //"ws://localhost:8080");
|
username,
|
||||||
(location.protocol === "https:" ? "wss" : "ws") +
|
chatEl,
|
||||||
"://" +
|
inputContainer,
|
||||||
location.host +
|
usersListEl,
|
||||||
"/ws",
|
wsDomain,
|
||||||
|
) {
|
||||||
|
const socket = new WebSocket(
|
||||||
|
`${location.protocol === "https:" ? "wss" : "ws"}://${
|
||||||
|
wsDomain || location.host
|
||||||
|
}/ws`,
|
||||||
"coms",
|
"coms",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
const isDev = process.env.NODE_ENV !== "production";
|
const isDev = process.env.NODE_ENV !== "production";
|
||||||
let livereload, connectLivereload;
|
let livereload, connectLivereload;
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
@@ -8,6 +9,19 @@ if (isDev) {
|
|||||||
}
|
}
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
const DOMAIN_FILE = path.join(__dirname, "domain.txt");
|
||||||
|
|
||||||
|
function readWsDomain() {
|
||||||
|
try {
|
||||||
|
const val = fs.readFileSync(DOMAIN_FILE, "utf8").trim();
|
||||||
|
if (val) return val;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== "ENOENT") {
|
||||||
|
console.warn("Failed to read domain.txt:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return process.env.WS_DOMAIN || "localhost:8080";
|
||||||
|
}
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
const liveReloadServer = livereload.createServer();
|
const liveReloadServer = livereload.createServer();
|
||||||
@@ -17,6 +31,10 @@ if (isDev) {
|
|||||||
|
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
|
|
||||||
|
app.get("/ws-domain", (_req, res) => {
|
||||||
|
res.json({ domain: readWsDomain() });
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "public", "index.html"));
|
res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ CC = clang -std=c23
|
|||||||
LINK = clang
|
LINK = clang
|
||||||
CFLAGS = -Wall -DDBG -ggdb -fsanitize=leak -I$(INC_DIR) -I$(SRC_DIR)
|
CFLAGS = -Wall -DDBG -ggdb -fsanitize=leak -I$(INC_DIR) -I$(SRC_DIR)
|
||||||
LDFLAGS = -lwebsockets
|
LDFLAGS = -lwebsockets
|
||||||
|
LDFLAGS += -lsqlite3
|
||||||
PRINT = echo -e
|
PRINT = echo -e
|
||||||
|
|
||||||
SRC_FILES = $(wildcard $(SRC_DIR)/*.c)
|
SRC_FILES = $(wildcard $(SRC_DIR)/*.c)
|
||||||
|
|||||||
@@ -6,13 +6,20 @@
|
|||||||
#include <yyjson.h>
|
#include <yyjson.h>
|
||||||
|
|
||||||
static const char* packet_type_strings[] = {
|
static const char* packet_type_strings[] = {
|
||||||
[PACKET_TYPE_JOIN] = "join", [PACKET_TYPE_WELCOME] = "welcome",
|
[PACKET_TYPE_JOIN] = "join",
|
||||||
[PACKET_TYPE_JOIN_EVT] = "join_evt", [PACKET_TYPE_MSG] = "msg",
|
[PACKET_TYPE_WELCOME] = "welcome",
|
||||||
[PACKET_TYPE_MSG_EVT] = "msg_evt", [PACKET_TYPE_NAME] = "name",
|
[PACKET_TYPE_JOIN_EVT] = "join_evt",
|
||||||
[PACKET_TYPE_NAME_EVT] = "name_evt", [PACKET_TYPE_LEAVE_EVT] = "leave_evt",
|
[PACKET_TYPE_MSG] = "msg",
|
||||||
[PACKET_TYPE_PING] = "ping", [PACKET_TYPE_PONG] = "pong",
|
[PACKET_TYPE_MSG_EVT] = "msg_evt",
|
||||||
[PACKET_TYPE_MSG_ACK] = "msg_ack", [PACKET_TYPE_NAME_ACK] = "name_ack",
|
[PACKET_TYPE_NAME] = "name",
|
||||||
|
[PACKET_TYPE_NAME_EVT] = "name_evt",
|
||||||
|
[PACKET_TYPE_PING] = "ping",
|
||||||
|
[PACKET_TYPE_PONG] = "pong",
|
||||||
|
[PACKET_TYPE_MSG_ACK] = "msg_ack",
|
||||||
|
[PACKET_TYPE_NAME_ACK] = "name_ack",
|
||||||
|
[PACKET_TYPE_HISTORY_REQ] = "history_req",
|
||||||
|
[PACKET_TYPE_HISTORY_RES] = "history_res",
|
||||||
|
[PACKET_TYPE_LEAVE_EVT] = "leave_evt",
|
||||||
};
|
};
|
||||||
|
|
||||||
PacketType packet_type_parse(const char* type) {
|
PacketType packet_type_parse(const char* type) {
|
||||||
@@ -138,6 +145,35 @@ pack_name_ack(yyjson_mut_doc* doc, yyjson_mut_val* data, PacketNameAck* ack) {
|
|||||||
yyjson_mut_obj_add_str(doc, data, "name", ack->name);
|
yyjson_mut_obj_add_str(doc, data, "name", ack->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void pack_history_res(
|
||||||
|
yyjson_mut_doc* doc, yyjson_mut_val* data, PacketHistoryRes* hist
|
||||||
|
) {
|
||||||
|
yyjson_mut_obj_add_str(doc, data, "room", hist->room);
|
||||||
|
yyjson_mut_obj_add_bool(doc, data, "has_more", hist->has_more);
|
||||||
|
yyjson_mut_obj_add_int(doc, data, "oldest_id", hist->oldest_id);
|
||||||
|
yyjson_mut_obj_add_int(doc, data, "historyc", hist->historyc);
|
||||||
|
yyjson_mut_val* history = yyjson_mut_arr(doc);
|
||||||
|
for (size_t i = 0; i < hist->historyc; i++) {
|
||||||
|
MsgData* m = hist->history[i];
|
||||||
|
if (!m) continue;
|
||||||
|
yyjson_mut_val* msg = yyjson_mut_obj(doc);
|
||||||
|
yyjson_mut_obj_add_uint(doc, msg, "id", m->id);
|
||||||
|
yyjson_mut_val* auth = yyjson_mut_obj(doc);
|
||||||
|
yyjson_mut_obj_add_uint(doc, auth, "id", m->author.id);
|
||||||
|
if (m->author.name)
|
||||||
|
yyjson_mut_obj_add_str(doc, auth, "name", *m->author.name);
|
||||||
|
else yyjson_mut_obj_add_str(doc, auth, "name", "");
|
||||||
|
yyjson_mut_obj_add_val(doc, msg, "author", auth);
|
||||||
|
if (m->parent != UINT64_MAX)
|
||||||
|
yyjson_mut_obj_add_uint(doc, msg, "parent", m->parent);
|
||||||
|
else yyjson_mut_obj_add_null(doc, msg, "parent");
|
||||||
|
yyjson_mut_obj_add_str(doc, msg, "content", m->content);
|
||||||
|
yyjson_mut_obj_add_int(doc, msg, "timestamp", m->timestamp);
|
||||||
|
yyjson_mut_arr_add_val(history, msg);
|
||||||
|
}
|
||||||
|
yyjson_mut_obj_add_val(doc, data, "history", history);
|
||||||
|
}
|
||||||
|
|
||||||
char* packet_string(Packet* packet, size_t* sz) {
|
char* packet_string(Packet* packet, size_t* sz) {
|
||||||
yyjson_mut_doc* doc = yyjson_mut_doc_new(NULL);
|
yyjson_mut_doc* doc = yyjson_mut_doc_new(NULL);
|
||||||
|
|
||||||
@@ -181,6 +217,9 @@ char* packet_string(Packet* packet, size_t* sz) {
|
|||||||
case PACKET_TYPE_NAME_ACK:
|
case PACKET_TYPE_NAME_ACK:
|
||||||
pack_name_ack(doc, data, (PacketNameAck*)packet->data);
|
pack_name_ack(doc, data, (PacketNameAck*)packet->data);
|
||||||
break;
|
break;
|
||||||
|
case PACKET_TYPE_HISTORY_RES:
|
||||||
|
pack_history_res(doc, data, (PacketHistoryRes*)packet->data);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
printf(
|
printf(
|
||||||
"Something's gone badly wrong. Trying to get string of "
|
"Something's gone badly wrong. Trying to get string of "
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "include/chat.h"
|
#include "include/chat.h"
|
||||||
#include "include/api.h"
|
#include "include/api.h"
|
||||||
#include "include/data.h"
|
#include "include/data.h"
|
||||||
|
#include "include/db.h"
|
||||||
#include "include/session.h"
|
#include "include/session.h"
|
||||||
|
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
@@ -10,33 +11,9 @@
|
|||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <yyjson.h>
|
#include <yyjson.h>
|
||||||
|
|
||||||
MsgData* chat_history[CHAT_HISTORY_SZ] = {NULL};
|
static const char DEFAULT_ROOM[] = "global";
|
||||||
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;
|
|
||||||
|
|
||||||
MsgData* chat_history_msg_add(MsgData* msg) {
|
// Legacy in-memory ring removed; history is now persisted in SQLite.
|
||||||
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.
|
// Parse a raw packet.
|
||||||
Packet* packet_parse(const char* in, size_t len) {
|
Packet* packet_parse(const char* in, size_t len) {
|
||||||
@@ -69,6 +46,9 @@ void do_join(Session* sess, Packet* packet) {
|
|||||||
session_set_name(sess, name);
|
session_set_name(sess, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define WELCOME_HISTORY_LIMIT 50
|
||||||
|
#define HISTORY_LIMIT_MAX 200
|
||||||
|
|
||||||
// Do a welcome packet.
|
// Do a welcome packet.
|
||||||
void do_welcome(Session* sess) {
|
void do_welcome(Session* sess) {
|
||||||
// Build list of online users (only sessions with a name).
|
// Build list of online users (only sessions with a name).
|
||||||
@@ -85,10 +65,20 @@ void do_welcome(Session* sess) {
|
|||||||
oi++;
|
oi++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build history list.
|
// Fetch latest messages from DB, then reverse to oldest-first.
|
||||||
MsgData** history = chat_history_nice();
|
MsgData** history = NULL;
|
||||||
size_t historyc = 0;
|
size_t historyc = 0;
|
||||||
while (historyc < CHAT_HISTORY_SZ && history[historyc]) historyc++;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
PacketWelcome data = {
|
PacketWelcome data = {
|
||||||
.id = session_get_id(sess),
|
.id = session_get_id(sess),
|
||||||
@@ -103,7 +93,84 @@ void do_welcome(Session* sess) {
|
|||||||
session_send(sess, packet);
|
session_send(sess, packet);
|
||||||
free(packet);
|
free(packet);
|
||||||
free(online);
|
free(online);
|
||||||
free(history);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do a welcome packet.
|
// Do a welcome packet.
|
||||||
@@ -188,15 +255,18 @@ void do_msg(Session* sess, Packet* packet) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MsgData* msg = malloc(sizeof(MsgData));
|
MsgData* msg = calloc(1, sizeof(MsgData));
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
lwsl_err("Failed to allocate MsgData.\n");
|
lwsl_err("Failed to allocate MsgData.\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg->id = next_msg_id++;
|
|
||||||
msg->author.id = session_get_id(sess);
|
msg->author.id = session_get_id(sess);
|
||||||
msg->author.name = session_get_name(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;
|
msg->parent = parent;
|
||||||
strncpy(msg->content, content, MSG_MAX_LENGTH - 1);
|
strncpy(msg->content, content, MSG_MAX_LENGTH - 1);
|
||||||
msg->content[MSG_MAX_LENGTH - 1] = '\0';
|
msg->content[MSG_MAX_LENGTH - 1] = '\0';
|
||||||
@@ -208,7 +278,14 @@ void do_msg(Session* sess, Packet* packet) {
|
|||||||
(msg->parent == UINT64_MAX ? "null" : "")
|
(msg->parent == UINT64_MAX ? "null" : "")
|
||||||
);
|
);
|
||||||
|
|
||||||
chat_history_msg_add(msg);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
ack.id = msg->id;
|
ack.id = msg->id;
|
||||||
Packet* ackp = packet_init(PACKET_TYPE_MSG_ACK, &ack);
|
Packet* ackp = packet_init(PACKET_TYPE_MSG_ACK, &ack);
|
||||||
@@ -218,6 +295,7 @@ void do_msg(Session* sess, Packet* packet) {
|
|||||||
Packet* evt = packet_init(PACKET_TYPE_MSG_EVT, msg);
|
Packet* evt = packet_init(PACKET_TYPE_MSG_EVT, msg);
|
||||||
session_send_all(evt);
|
session_send_all(evt);
|
||||||
free(evt);
|
free(evt);
|
||||||
|
free(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void do_ping(Session* sess) {
|
static void do_ping(Session* sess) {
|
||||||
@@ -349,6 +427,9 @@ int cb_chat(
|
|||||||
free(ackp);
|
free(ackp);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PACKET_TYPE_HISTORY_REQ:
|
||||||
|
do_history_req(sess, packet);
|
||||||
|
break;
|
||||||
case PACKET_TYPE_PONG:
|
case PACKET_TYPE_PONG:
|
||||||
// Client responded; nothing else to do (timer continues).
|
// Client responded; nothing else to do (timer continues).
|
||||||
if (sess) {
|
if (sess) {
|
||||||
|
|||||||
@@ -13,3 +13,10 @@ int msg_verify(const char* content) {
|
|||||||
if (strlen(content) > MSG_MAX_LENGTH) return 0;
|
if (strlen(content) > MSG_MAX_LENGTH) return 0;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int room_verify(const char* room) {
|
||||||
|
if (!room) return 0;
|
||||||
|
if (strlen(room) == 0) return 0;
|
||||||
|
if (strlen(room) > ROOM_MAX_LENGTH) return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|||||||
201
server/src/db.c
Normal file
201
server/src/db.c
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#define API__H
|
#define API__H
|
||||||
|
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
PACKET_TYPE_JOIN, // C -> S.
|
PACKET_TYPE_JOIN, // C -> S.
|
||||||
@@ -15,6 +16,8 @@ typedef enum {
|
|||||||
PACKET_TYPE_PONG, // C -> S. Liveness pong.
|
PACKET_TYPE_PONG, // C -> S. Liveness pong.
|
||||||
PACKET_TYPE_MSG_ACK, // S -> C. Acknowledge message submission.
|
PACKET_TYPE_MSG_ACK, // S -> C. Acknowledge message submission.
|
||||||
PACKET_TYPE_NAME_ACK, // S -> C. Acknowledge name change.
|
PACKET_TYPE_NAME_ACK, // S -> C. Acknowledge name change.
|
||||||
|
PACKET_TYPE_HISTORY_REQ, // C -> S. Request older history.
|
||||||
|
PACKET_TYPE_HISTORY_RES, // S -> C. Return history batch.
|
||||||
PACKET_TYPE_LEAVE_EVT, // S -> A. Broadcast when a user leaves.
|
PACKET_TYPE_LEAVE_EVT, // S -> A. Broadcast when a user leaves.
|
||||||
PACKET_TYPE_MAX = PACKET_TYPE_LEAVE_EVT,
|
PACKET_TYPE_MAX = PACKET_TYPE_LEAVE_EVT,
|
||||||
PACKET_TYPE_BAD,
|
PACKET_TYPE_BAD,
|
||||||
@@ -80,4 +83,18 @@ typedef struct {
|
|||||||
Name name; // The name that was applied (empty on failure).
|
Name name; // The name that was applied (empty on failure).
|
||||||
} PacketNameAck;
|
} PacketNameAck;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Room room;
|
||||||
|
MsgID before;
|
||||||
|
size_t limit;
|
||||||
|
} PacketHistoryReq;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Room room;
|
||||||
|
size_t historyc;
|
||||||
|
MsgData** history;
|
||||||
|
bool has_more;
|
||||||
|
MsgID oldest_id;
|
||||||
|
} PacketHistoryRes;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -7,19 +7,7 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdbool.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.
|
* cb_chat - libwebsockets protocol callback for COMS chat.
|
||||||
*
|
*
|
||||||
* This function is registered in the protocols array passed to the
|
* This function is registered in the protocols array passed to the
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#define NAME_MAX_LENGTH 16
|
#define NAME_MAX_LENGTH 16
|
||||||
#define MSG_MAX_LENGTH 1024
|
#define MSG_MAX_LENGTH 1024
|
||||||
|
#define ROOM_MAX_LENGTH 32
|
||||||
|
|
||||||
typedef uint64_t UserID;
|
typedef uint64_t UserID;
|
||||||
typedef uint64_t MsgID;
|
typedef uint64_t MsgID;
|
||||||
@@ -16,6 +17,9 @@ int name_verify(const char* name);
|
|||||||
typedef char MsgContent[MSG_MAX_LENGTH];
|
typedef char MsgContent[MSG_MAX_LENGTH];
|
||||||
int msg_verify(const char* content);
|
int msg_verify(const char* content);
|
||||||
|
|
||||||
|
typedef char Room[ROOM_MAX_LENGTH];
|
||||||
|
int room_verify(const char* room);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
UserID id;
|
UserID id;
|
||||||
Name* name;
|
Name* name;
|
||||||
@@ -24,6 +28,8 @@ typedef struct {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
MsgID id;
|
MsgID id;
|
||||||
UserData author;
|
UserData author;
|
||||||
|
Name author_name; // backing storage when author.name isn't set
|
||||||
|
Room room;
|
||||||
MsgID parent;
|
MsgID parent;
|
||||||
MsgContent content;
|
MsgContent content;
|
||||||
time_t timestamp;
|
time_t timestamp;
|
||||||
|
|||||||
25
server/src/include/db.h
Normal file
25
server/src/include/db.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#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,4 +1,5 @@
|
|||||||
#include "include/chat.h"
|
#include "include/chat.h"
|
||||||
|
#include "include/db.h"
|
||||||
#include "include/session.h"
|
#include "include/session.h"
|
||||||
|
|
||||||
#include <libwebsockets.h>
|
#include <libwebsockets.h>
|
||||||
@@ -29,6 +30,11 @@ static struct lws_protocols protocols[] = {
|
|||||||
int main(void) {
|
int main(void) {
|
||||||
signal(SIGINT, handle_sigint);
|
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.
|
// Create libws context.
|
||||||
struct lws_context_creation_info info;
|
struct lws_context_creation_info info;
|
||||||
memset(&info, 0, sizeof(info));
|
memset(&info, 0, sizeof(info));
|
||||||
@@ -49,6 +55,7 @@ int main(void) {
|
|||||||
|
|
||||||
// Cleanse.
|
// Cleanse.
|
||||||
lws_context_destroy(context);
|
lws_context_destroy(context);
|
||||||
|
db_close();
|
||||||
printf("Server shutting down.\n");
|
printf("Server shutting down.\n");
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user