XMPP/ChatServer.cpp
2026-03-26 20:36:05 +02:00

201 lines
11 KiB
C++

#include "ChatServer.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QDateTime>
#include <QDebug>
// ─────────────────────────────────────────────────────────────────────────────
// Constructor
// ─────────────────────────────────────────────────────────────────────────────
ChatServer::ChatServer(QObject *parent) : QTcpServer(parent)
{
}
// ─────────────────────────────────────────────────────────────────────────────
// Qt calls this automatically when a new client connects
// ─────────────────────────────────────────────────────────────────────────────
void ChatServer::incomingConnection(qintptr socketDescriptor)
{
auto *socket = new QTcpSocket(this);
// Qt docs recommend checking setSocketDescriptor actually succeeded
if (!socket->setSocketDescriptor(socketDescriptor)) {
qWarning() << "[Server] Failed to set descriptor:" << socket->errorString();
delete socket;
return;
}
// wire up this socket's signals to our slots
connect(socket, &QTcpSocket::readyRead, this, &ChatServer::onReadyRead);
connect(socket, &QTcpSocket::disconnected, this, &ChatServer::onDisconnected);
m_clients[socket] = ""; // socket known, username not yet
qInfo() << "[Server] New connection from" << socket->peerAddress().toString();
sendSystemMessage(socket, QString("Connected to server. %1 user(s) online.")
.arg(m_clients.size()));
}
// ─────────────────────────────────────────────────────────────────────────────
// Fires when data arrives from any client
// ─────────────────────────────────────────────────────────────────────────────
void ChatServer::onReadyRead()
{
auto *socket = qobject_cast<QTcpSocket *>(sender());
if (!socket) return;
// append whatever arrived to this client's buffer
m_buffers[socket] += socket->readAll();
// guard against a client sending huge amounts of data with no newlines
if (m_buffers[socket].size() > MAX_BUFFER_SIZE) {
qWarning() << "[Server] Buffer overflow from"
<< m_clients.value(socket, "unknown") << "— disconnecting.";
socket->disconnectFromHost();
return;
}
// extract and process complete messages (delimited by \n)
while (m_buffers[socket].contains('\n')) {
int idx = m_buffers[socket].indexOf('\n');
QByteArray line = m_buffers[socket].left(idx).trimmed();
m_buffers[socket] = m_buffers[socket].mid(idx + 1);
if (line.isEmpty()) continue;
processFrame(socket, line);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Fires when a client disconnects
// ─────────────────────────────────────────────────────────────────────────────
void ChatServer::onDisconnected()
{
// Qt docs: use deleteLater() when deleting sender() of disconnected signal
auto *socket = qobject_cast<QTcpSocket *>(sender());
if (!socket) return;
QString username = m_clients.value(socket, "Unknown");
m_clients.remove(socket);
m_buffers.remove(socket);
socket->deleteLater();
qInfo() << "[Server]" << username << "disconnected.";
broadcast(nullptr, buildFrame("system", "Server",
username + " has left the chat.", ""));
broadcastUserList();
}
// ─────────────────────────────────────────────────────────────────────────────
// Handle one complete JSON message from a client
// ─────────────────────────────────────────────────────────────────────────────
void ChatServer::processFrame(QTcpSocket *socket, const QByteArray &data)
{
// parse raw bytes into a JSON document
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "[Server] Bad JSON from client:" << err.errorString();
return;
}
QJsonObject obj = doc.object();
QString type = obj["type"].toString();
QString username = obj["username"].toString().trimmed();
// ── JOIN ──────────────────────────────────────────────────────────────────
if (type == "join") {
if (username.isEmpty()) {
sendSystemMessage(socket, "Username cannot be empty.");
return;
}
// check for duplicate username (case insensitive)
for (auto it = m_clients.begin(); it != m_clients.end(); ++it) {
if (it.value().toLower() == username.toLower() && it.key() != socket) {
sendSystemMessage(socket, "Username '" + username + "' is already taken.");
return;
}
}
m_clients[socket] = username;
qInfo() << "[Server]" << username << "joined.";
sendSystemMessage(socket, "Welcome, " + username + "!");
broadcast(socket, buildFrame("system", "Server",
username + " joined the chat.", ""));
broadcastUserList();
return;
}
// ── GUARD: must have a username before doing anything else ────────────────
if (m_clients.value(socket).isEmpty()) {
sendSystemMessage(socket, "Please join with a username first.");
return;
}
// ── MESSAGE ───────────────────────────────────────────────────────────────
if (type == "message") {
QString text = obj["text"].toString().trimmed();
if (text.isEmpty()) return;
QString ts = QDateTime::currentDateTime().toString("hh:mm");
qInfo() << "[Server]" << username << ":" << text;
broadcast(nullptr, buildFrame("message", username, text, ts));
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Package data into a JSON frame ready to send
// ─────────────────────────────────────────────────────────────────────────────
QByteArray ChatServer::buildFrame(const QString &type, const QString &from,
const QString &text, const QString &ts)
{
QJsonObject obj;
obj["type"] = type;
obj["from"] = from;
obj["text"] = text;
obj["ts"] = ts;
// Compact = no spaces, everything on one line. \n is the message delimiter.
return QJsonDocument(obj).toJson(QJsonDocument::Compact) + "\n";
}
// ─────────────────────────────────────────────────────────────────────────────
// Send a frame to all clients, optionally excluding one
// ─────────────────────────────────────────────────────────────────────────────
void ChatServer::broadcast(QTcpSocket *exclude, const QByteArray &frame)
{
for (auto *s : m_clients.keys()) {
if (s != exclude && s->state() == QAbstractSocket::ConnectedState)
s->write(frame);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Send a system notification to one specific client
// ─────────────────────────────────────────────────────────────────────────────
void ChatServer::sendSystemMessage(QTcpSocket *socket, const QString &text)
{
socket->write(buildFrame("system", "Server", text, ""));
}
// ─────────────────────────────────────────────────────────────────────────────
// Send the current online user list to all clients
// ─────────────────────────────────────────────────────────────────────────────
void ChatServer::broadcastUserList()
{
// QJsonArray is explicitly included — don't rely on indirect includes
QJsonArray names;
for (const QString &name : m_clients.values())
if (!name.isEmpty()) names.append(name);
QJsonObject obj;
obj["type"] = "userlist";
obj["users"] = names;
QByteArray frame = QJsonDocument(obj).toJson(QJsonDocument::Compact) + "\n";
for (auto *s : m_clients.keys())
s->write(frame);
}