#include "ChatServer.h" #include #include #include #include #include // ───────────────────────────────────────────────────────────────────────────── // 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(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(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); }