mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
* [Client] Fix spurious server room join error The server replies RespContextError when a join command is received for a room that connection is already registered in. The client was sending such duplicate joins in benign situations - double-clicking to join a room, or clicking a room the selector was already auto-joining - and answered them with a modal telling users to restart the client. Joins for the same room are now deduplicated while one is in flight, and a remaining RespContextError is healed by leaving and rejoining the room so the tab appears without a client restart. Error dialogs are only shown for user-initiated joins, so failed auto-joins no longer spam critical popups. * [Client] Bound stale-membership room join heal to one attempt The RespContextError heal (leave + rejoin) previously recurred unconditionally, so a server that kept returning RespContextError for a reason other than stale membership would loop forever. Track room ids that already received a heal and surface the error dialog after one attempt instead of retrying indefinitely. * [Client] Scope room-join heal guard to one join attempt * [Client] Hoist room-join heal guard lookup out of response switch --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
295 lines
12 KiB
C++
295 lines
12 KiB
C++
#include "tab_server.h"
|
|
|
|
#include "../interface/widgets/server/user/user_list_widget.h"
|
|
#include "tab_supervisor.h"
|
|
|
|
#include <QHBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QInputDialog>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QTextEdit>
|
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
|
#include <libcockatrice/protocol/pb/event_list_rooms.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_server_message.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_join_room.pb.h>
|
|
#include <libcockatrice/protocol/pb/room_commands.pb.h>
|
|
#include <libcockatrice/protocol/pb/session_commands.pb.h>
|
|
#include <libcockatrice/protocol/pending_command.h>
|
|
|
|
RoomSelector::RoomSelector(AbstractClient *_client, QWidget *parent) : QGroupBox(parent), client(_client)
|
|
{
|
|
roomList = new QTreeWidget;
|
|
roomList->setRootIsDecorated(false);
|
|
roomList->setColumnCount(5);
|
|
roomList->header()->setStretchLastSection(false);
|
|
roomList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
|
roomList->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
|
roomList->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
|
roomList->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
|
|
|
joinButton = new QPushButton;
|
|
connect(joinButton, &QPushButton::clicked, this, &RoomSelector::joinClicked);
|
|
QHBoxLayout *buttonLayout = new QHBoxLayout;
|
|
buttonLayout->addStretch();
|
|
buttonLayout->addWidget(joinButton);
|
|
QVBoxLayout *vbox = new QVBoxLayout;
|
|
vbox->addWidget(roomList);
|
|
vbox->addLayout(buttonLayout);
|
|
|
|
retranslateUi();
|
|
setLayout(vbox);
|
|
|
|
connect(client, &AbstractClient::listRoomsEventReceived, this, &RoomSelector::processListRoomsEvent);
|
|
connect(roomList, &QTreeWidget::activated, this, &RoomSelector::joinClicked);
|
|
client->sendCommand(client->prepareSessionCommand(Command_ListRooms()));
|
|
}
|
|
|
|
void RoomSelector::retranslateUi()
|
|
{
|
|
setTitle(tr("Rooms"));
|
|
joinButton->setText(tr("Joi&n"));
|
|
|
|
QTreeWidgetItem *header = roomList->headerItem();
|
|
header->setText(0, tr("Room"));
|
|
header->setText(1, tr("Description"));
|
|
header->setText(2, tr("Permissions"));
|
|
header->setText(3, tr("Players"));
|
|
header->setText(4, tr("Games"));
|
|
header->setTextAlignment(2, Qt::AlignRight);
|
|
header->setTextAlignment(3, Qt::AlignRight);
|
|
header->setTextAlignment(4, Qt::AlignRight);
|
|
}
|
|
|
|
void RoomSelector::processListRoomsEvent(const Event_ListRooms &event)
|
|
{
|
|
const int roomListSize = event.room_list_size();
|
|
for (int i = 0; i < roomListSize; ++i) {
|
|
const ServerInfo_Room &room = event.room_list(i);
|
|
|
|
for (int j = 0; j < roomList->topLevelItemCount(); ++j) {
|
|
QTreeWidgetItem *twi = roomList->topLevelItem(j);
|
|
if (twi->data(0, Qt::UserRole).toInt() == room.room_id()) {
|
|
if (room.has_name()) {
|
|
twi->setData(0, Qt::DisplayRole, QString::fromStdString(room.name()));
|
|
}
|
|
if (room.has_description()) {
|
|
twi->setData(1, Qt::DisplayRole, QString::fromStdString(room.description()));
|
|
}
|
|
if (room.has_permissionlevel()) {
|
|
twi->setData(2, Qt::DisplayRole, getRoomPermissionDisplay(room));
|
|
}
|
|
if (room.has_player_count()) {
|
|
twi->setData(3, Qt::DisplayRole, room.player_count());
|
|
}
|
|
if (room.has_game_count()) {
|
|
twi->setData(4, Qt::DisplayRole, room.game_count());
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
QTreeWidgetItem *twi = new QTreeWidgetItem;
|
|
twi->setData(0, Qt::UserRole, room.room_id());
|
|
if (room.has_name()) {
|
|
twi->setData(0, Qt::DisplayRole, QString::fromStdString(room.name()));
|
|
}
|
|
if (room.has_description()) {
|
|
twi->setData(1, Qt::DisplayRole, QString::fromStdString(room.description()));
|
|
}
|
|
if (room.has_permissionlevel()) {
|
|
twi->setData(2, Qt::DisplayRole, getRoomPermissionDisplay(room));
|
|
}
|
|
twi->setData(3, Qt::DisplayRole, room.player_count());
|
|
twi->setData(4, Qt::DisplayRole, room.game_count());
|
|
twi->setTextAlignment(2, Qt::AlignRight);
|
|
twi->setTextAlignment(3, Qt::AlignRight);
|
|
twi->setTextAlignment(4, Qt::AlignRight);
|
|
|
|
roomList->addTopLevelItem(twi);
|
|
if (room.has_auto_join()) {
|
|
if (room.auto_join()) {
|
|
emit joinRoomRequest(room.room_id(), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QString RoomSelector::getRoomPermissionDisplay(const ServerInfo_Room &room)
|
|
{
|
|
/*
|
|
* A server room can have a permission level and a privilege level. How ever we want to display only the necessary
|
|
* information on the server tab needed to inform users of required permissions to enter a room. If the room has a
|
|
* privilege level the server tab will display the privilege level in the "permissions" column in the row however if
|
|
* the room contains a permissions level for the room the permissions level defined for the room will be displayed.
|
|
*/
|
|
|
|
QString roomPermissionDisplay = QString::fromStdString(room.privilegelevel()).toLower();
|
|
if (QString::fromStdString(room.permissionlevel()).toLower() != "none") {
|
|
roomPermissionDisplay = QString::fromStdString(room.permissionlevel()).toLower();
|
|
}
|
|
if (roomPermissionDisplay == "") { // catch all for misconfigured .ini room definitions
|
|
roomPermissionDisplay = "none";
|
|
}
|
|
|
|
return roomPermissionDisplay;
|
|
}
|
|
|
|
void RoomSelector::joinClicked()
|
|
{
|
|
QTreeWidgetItem *twi = roomList->currentItem();
|
|
if (!twi) {
|
|
return;
|
|
}
|
|
|
|
int id = twi->data(0, Qt::UserRole).toInt();
|
|
|
|
emit joinRoomRequest(id, true);
|
|
}
|
|
|
|
TabServer::TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
|
{
|
|
roomSelector = new RoomSelector(client);
|
|
serverInfoBox = new QTextBrowser;
|
|
serverInfoBox->setOpenExternalLinks(true);
|
|
|
|
connect(roomSelector, &RoomSelector::joinRoomRequest, this, &TabServer::joinRoom);
|
|
|
|
connect(client, &AbstractClient::serverMessageEventReceived, this, &TabServer::processServerMessageEvent);
|
|
|
|
QVBoxLayout *vbox = new QVBoxLayout;
|
|
vbox->addWidget(roomSelector);
|
|
vbox->addWidget(serverInfoBox);
|
|
|
|
retranslateUi();
|
|
|
|
QWidget *mainWidget = new QWidget(this);
|
|
mainWidget->setLayout(vbox);
|
|
setCentralWidget(mainWidget);
|
|
}
|
|
|
|
void TabServer::retranslateUi()
|
|
{
|
|
roomSelector->retranslateUi();
|
|
}
|
|
|
|
void TabServer::processServerMessageEvent(const Event_ServerMessage &event)
|
|
{
|
|
serverInfoBox->setHtml(QString::fromStdString(event.message()));
|
|
if (shouldEmitUpdate) {
|
|
// prevent the initial server message from taking attention from ping icon
|
|
emit userEvent();
|
|
} else {
|
|
shouldEmitUpdate = true;
|
|
}
|
|
}
|
|
|
|
void TabServer::joinRoom(int id, bool setCurrent)
|
|
{
|
|
TabRoom *room = tabSupervisor->getRoomTabs().value(id);
|
|
if (room) {
|
|
if (setCurrent) {
|
|
tabSupervisor->setCurrentWidget((QWidget *)room);
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto pendingIt = pendingRoomJoins.find(id);
|
|
if (pendingIt != pendingRoomJoins.end()) {
|
|
// A join for this room is already in flight: the room tab opens when its response
|
|
// arrives. Fold the new request into the pending one so that, for example, clicking
|
|
// a room the selector is auto-joining does not send a second Command_JoinRoom - the
|
|
// server would reject that duplicate with RespContextError.
|
|
if (setCurrent) {
|
|
pendingIt.value() = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
pendingRoomJoins.insert(id, setCurrent);
|
|
|
|
Command_JoinRoom cmd;
|
|
cmd.set_room_id(id);
|
|
|
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
|
pend->setExtraData(setCurrent);
|
|
connect(
|
|
pend, &PendingCommand::finished, this,
|
|
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) { joinRoomFinished(r, c, v, id); });
|
|
|
|
client->sendCommand(pend);
|
|
}
|
|
|
|
void TabServer::joinRoomFinished(const Response &r,
|
|
const CommandContainer & /*commandContainer*/,
|
|
const QVariant &extraData,
|
|
int roomId)
|
|
{
|
|
const bool setCurrent = pendingRoomJoins.value(roomId, extraData.toBool());
|
|
pendingRoomJoins.remove(roomId);
|
|
const bool healedJoin = healedRoomJoins.contains(roomId);
|
|
healedRoomJoins.remove(roomId);
|
|
|
|
switch (r.response_code()) {
|
|
case Response::RespOk:
|
|
break;
|
|
case Response::RespNameNotFound:
|
|
if (setCurrent) {
|
|
QMessageBox::critical(this, tr("Error"),
|
|
tr("Failed to join the server room: it doesn't exist on the server."));
|
|
}
|
|
emit roomJoinFailed(roomId);
|
|
return;
|
|
case Response::RespContextError:
|
|
if (healedJoin) {
|
|
// The rejoin below was already answered and the server still rejects the join, so
|
|
// the stale-membership heal cannot help: surface the error. The guard was already
|
|
// released above so a later user-initiated join may try a fresh heal.
|
|
if (setCurrent) {
|
|
QMessageBox::critical(
|
|
this, tr("Error"),
|
|
tr("The server thinks you are in the server room but your client is unable to display it. "
|
|
"Try restarting your client."));
|
|
}
|
|
emit roomJoinFailed(roomId);
|
|
return;
|
|
}
|
|
// The server already had us registered in the room even though no tab was open,
|
|
// usually because two join attempts for the same room overlapped. Leaving and
|
|
// rejoining makes the server reply with a fresh RespOk so the tab is displayed
|
|
// without requiring a client restart. The guard above covers exactly the rejoin that
|
|
// leaveAndRejoinRoom triggers, so a server that keeps replying with RespContextError
|
|
// gets one heal attempt per join instead of an endless recursion.
|
|
healedRoomJoins.insert(roomId);
|
|
leaveAndRejoinRoom(roomId, setCurrent);
|
|
return;
|
|
case Response::RespUserLevelTooLow:
|
|
if (setCurrent) {
|
|
QMessageBox::critical(this, tr("Error"),
|
|
tr("You do not have the required permission to join this server room."));
|
|
}
|
|
emit roomJoinFailed(roomId);
|
|
return;
|
|
default:
|
|
if (setCurrent) {
|
|
QMessageBox::critical(
|
|
this, tr("Error"),
|
|
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code()));
|
|
}
|
|
emit roomJoinFailed(roomId);
|
|
return;
|
|
}
|
|
|
|
const Response_JoinRoom &resp = r.GetExtension(Response_JoinRoom::ext);
|
|
emit roomJoined(resp.room_info(), setCurrent);
|
|
}
|
|
|
|
void TabServer::leaveAndRejoinRoom(int roomId, bool setCurrent)
|
|
{
|
|
// Clear the stale room membership server-side. The leave is sent before the rejoin below,
|
|
// so the server no longer considers us a member by the time the join arrives. The leave
|
|
// response is intentionally not awaited: commands are processed in send order on the
|
|
// connection, and a failed leave (RespNotInRoom) only means the membership was already gone.
|
|
client->sendCommand(client->prepareRoomCommand(Command_LeaveRoom(), roomId));
|
|
|
|
joinRoom(roomId, setCurrent);
|
|
}
|