diff --git a/cockatrice/src/server/user/user_context_menu.cpp b/cockatrice/src/server/user/user_context_menu.cpp index 944796319..25026801c 100644 --- a/cockatrice/src/server/user/user_context_menu.cpp +++ b/cockatrice/src/server/user/user_context_menu.cpp @@ -11,6 +11,7 @@ #include "pb/commands.pb.h" #include "pb/moderator_commands.pb.h" #include "pb/response_ban_history.pb.h" +#include "pb/response_get_admin_notes.pb.h" #include "pb/response_get_games_of_user.pb.h" #include "pb/response_get_user_info.pb.h" #include "pb/response_warn_history.pb.h" @@ -47,6 +48,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, aDemoteFromMod = new QAction(QString(), this); aPromoteToJudge = new QAction(QString(), this); aDemoteFromJudge = new QAction(QString(), this); + aGetAdminNotes = new QAction(QString(), this); retranslateUi(); } @@ -69,6 +71,7 @@ void UserContextMenu::retranslateUi() aDemoteFromMod->setText(tr("Dem&ote user from moderator")); aPromoteToJudge->setText(tr("Promote user to &judge")); aDemoteFromJudge->setText(tr("Demote user from judge")); + aGetAdminNotes->setText(tr("View admin notes")); } void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer) @@ -221,6 +224,21 @@ void UserContextMenu::warnUserHistory_processResponse(const Response &resp) tr("Failed to collect warning information.")); } +void UserContextMenu::getAdminNotes_processResponse(const Response &resp) +{ + const Response_GetAdminNotes &response = resp.GetExtension(Response_GetAdminNotes::ext); + + if (resp.response_code() != Response::RespOk) { + QMessageBox::information(static_cast(parent()), tr("Failed"), tr("Failed to get admin notes.")); + return; + } + + auto *dlg = new AdminNotesDialog(QString::fromStdString(response.user_name()), + QString::fromStdString(response.notes()), static_cast(parent())); + connect(dlg, &AdminNotesDialog::accepted, this, &UserContextMenu::updateAdminNotes_dialogFinished); + dlg->show(); +} + void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const CommandContainer &commandContainer) { @@ -281,6 +299,17 @@ void UserContextMenu::warnUser_dialogFinished() client->sendCommand(client->prepareModeratorCommand(cmd)); } +void UserContextMenu::updateAdminNotes_dialogFinished() +{ + auto *dlg = static_cast(sender()); + + Command_UpdateAdminNotes cmd; + cmd.set_user_name(dlg->getName().toStdString()); + cmd.set_notes(dlg->getNotes().toStdString()); + + client->sendCommand(client->prepareModeratorCommand(cmd)); +} + void UserContextMenu::showContextMenu(const QPoint &pos, const QString &userName, UserLevelFlags userLevel, @@ -347,6 +376,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addSeparator(); menu->addAction(aBan); menu->addAction(aBanHistory); + menu->addSeparator(); + menu->addAction(aGetAdminNotes); menu->addSeparator(); if (userLevel.testFlag(ServerInfo_User::IsModerator) && @@ -380,6 +411,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos, aWarnHistory->setEnabled(anotherUser); aBan->setEnabled(anotherUser); aBanHistory->setEnabled(anotherUser); + aGetAdminNotes->setEnabled(anotherUser); aPromoteToMod->setEnabled(anotherUser); aDemoteFromMod->setEnabled(anotherUser); @@ -478,6 +510,14 @@ void UserContextMenu::showContextMenu(const QPoint &pos, connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(warnUserHistory_processResponse(Response))); client->sendCommand(pend); + } else if (actionClicked == aGetAdminNotes) { + Command_GetAdminNotes cmd; + cmd.set_user_name(userName.toStdString()); + auto *pend = client->prepareModeratorCommand(cmd); + connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, + SLOT(getAdminNotes_processResponse(Response))); + client->sendCommand(pend); + } else if (actionClicked == aCopyToClipBoard) { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(deckHash); diff --git a/cockatrice/src/server/user/user_context_menu.h b/cockatrice/src/server/user/user_context_menu.h index 848f9ecc8..ddbf47b45 100644 --- a/cockatrice/src/server/user/user_context_menu.h +++ b/cockatrice/src/server/user/user_context_menu.h @@ -35,6 +35,7 @@ private: QAction *aPromoteToMod, *aDemoteFromMod; QAction *aPromoteToJudge, *aDemoteFromJudge; QAction *aWarnUser, *aWarnHistory; + QAction *aGetAdminNotes; signals: void openMessageDialog(const QString &userName, bool focus); private slots: @@ -43,9 +44,11 @@ private slots: void warnUser_processUserInfoResponse(const Response &resp); void banUserHistory_processResponse(const Response &resp); void warnUserHistory_processResponse(const Response &resp); + void getAdminNotes_processResponse(const Response &resp); void adjustMod_processUserResponse(const Response &resp, const CommandContainer &commandContainer); void banUser_dialogFinished(); void warnUser_dialogFinished(); + void updateAdminNotes_dialogFinished(); void gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer); public: diff --git a/cockatrice/src/server/user/user_list.cpp b/cockatrice/src/server/user/user_list.cpp index 8ea6f341f..98b42362b 100644 --- a/cockatrice/src/server/user/user_list.cpp +++ b/cockatrice/src/server/user/user_list.cpp @@ -284,6 +284,32 @@ int BanDialog::getDeleteMessages() const return deleteMessages->isChecked() ? -1 : 0; } +AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent) + : QDialog(_parent), userName(_userName) +{ + setAttribute(Qt::WA_DeleteOnClose); + + auto *updateButton = new QPushButton(tr("Update Notes")); + updateButton->setEnabled(false); + connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept); + + notes = new QPlainTextEdit(_notes); + notes->setMinimumWidth(500); + connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); }); + + auto *vbox = new QVBoxLayout; + vbox->addWidget(notes); + vbox->addWidget(updateButton); + + setLayout(vbox); + setWindowTitle(tr("Admin Notes for %1").arg(_userName)); +} + +QString AdminNotesDialog::getNotes() const +{ + return notes->toPlainText(); +} + UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent) { } diff --git a/cockatrice/src/server/user/user_list.h b/cockatrice/src/server/user/user_list.h index 525fa279d..abf52ff52 100644 --- a/cockatrice/src/server/user/user_list.h +++ b/cockatrice/src/server/user/user_list.h @@ -8,6 +8,7 @@ #include #include #include +#include #include class QTreeWidget; @@ -69,6 +70,23 @@ public: void addWarningOption(const QString warning); }; +class AdminNotesDialog : public QDialog +{ + Q_OBJECT + +private: + QString userName; + QPlainTextEdit *notes; + +public: + explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr); + QString getName() const + { + return userName; + } + QString getNotes() const; +}; + class UserListItemDelegate : public QStyledItemDelegate { public: diff --git a/common/pb/CMakeLists.txt b/common/pb/CMakeLists.txt index fbd6c90d0..e8af9ef46 100644 --- a/common/pb/CMakeLists.txt +++ b/common/pb/CMakeLists.txt @@ -134,6 +134,7 @@ set(PROTO_FILES response_viewlog_history.proto response_warn_history.proto response_warn_list.proto + response_get_admin_notes.proto response.proto room_commands.proto room_event.proto diff --git a/common/pb/moderator_commands.proto b/common/pb/moderator_commands.proto index 0b9d35f4f..9d01b51d2 100644 --- a/common/pb/moderator_commands.proto +++ b/common/pb/moderator_commands.proto @@ -9,6 +9,8 @@ message ModeratorCommand { VIEWLOG_HISTORY = 1005; GRANT_REPLAY_ACCESS = 1006; FORCE_ACTIVATE_USER = 1007; + GET_ADMIN_NOTES = 1008; + UPDATE_ADMIN_NOTES = 1009; } extensions 100 to max; } @@ -88,4 +90,19 @@ message Command_ForceActivateUser { } optional string username_to_activate = 1; optional string moderator_name = 2; -} \ No newline at end of file +} + +message Command_GetAdminNotes { + extend ModeratorCommand { + optional Command_GetAdminNotes ext = 1008; + } + optional string user_name = 1; +} + +message Command_UpdateAdminNotes { + extend ModeratorCommand { + optional Command_UpdateAdminNotes ext = 1009; + } + optional string user_name = 1; + optional string notes = 2; +} diff --git a/common/pb/response.proto b/common/pb/response.proto index 8ff96c1c3..18f4249c4 100644 --- a/common/pb/response.proto +++ b/common/pb/response.proto @@ -62,6 +62,7 @@ message Response { VIEW_LOG = 1015; FORGOT_PASSWORD_REQUEST = 1016; PASSWORD_SALT = 1017; + GET_ADMIN_NOTES = 1018; REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; } diff --git a/common/pb/response_get_admin_notes.proto b/common/pb/response_get_admin_notes.proto new file mode 100644 index 000000000..f08bce5d5 --- /dev/null +++ b/common/pb/response_get_admin_notes.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_GetAdminNotes { + extend Response { + optional Response_GetAdminNotes ext = 1018; + } + optional string user_name = 1; + optional string notes = 2; +} diff --git a/servatrice/migrations/servatrice_0029_to_0030.sql b/servatrice/migrations/servatrice_0029_to_0030.sql new file mode 100644 index 000000000..37f32ccb0 --- /dev/null +++ b/servatrice/migrations/servatrice_0029_to_0030.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 29 to version 30 + +ALTER TABLE cockatrice_users ADD COLUMN adminnotes mediumtext NOT NULL; + +UPDATE cockatrice_schema_version SET version=30 WHERE version=29; diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index f2ef256d0..27905ce9a 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( PRIMARY KEY (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -INSERT INTO cockatrice_schema_version VALUES(29); +INSERT INTO cockatrice_schema_version VALUES(30); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( @@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_users` ( `active` tinyint(1) NOT NULL, `token` binary(16), `clientid` varchar(15) NOT NULL, + `adminnotes` mediumtext NOT NULL, `privlevel` enum("NONE","VIP","DONATOR") NOT NULL, `privlevelStartDate` datetime NOT NULL, `privlevelEndDate` datetime NOT NULL, diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index f4bbf5dfc..65199d61d 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -9,7 +9,7 @@ #include #include -#define DATABASE_SCHEMA_VERSION 29 +#define DATABASE_SCHEMA_VERSION 30 class Servatrice; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index c87226610..730738e92 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -46,6 +46,7 @@ #include "pb/response_deck_list.pb.h" #include "pb/response_deck_upload.pb.h" #include "pb/response_forgotpasswordrequest.pb.h" +#include "pb/response_get_admin_notes.pb.h" #include "pb/response_password_salt.pb.h" #include "pb/response_register.pb.h" #include "pb/response_replay_download.pb.h" @@ -228,6 +229,10 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo return cmdGrantReplayAccess(cmd.GetExtension(Command_GrantReplayAccess::ext), rc); case ModeratorCommand::FORCE_ACTIVATE_USER: return cmdForceActivateUser(cmd.GetExtension(Command_ForceActivateUser::ext), rc); + case ModeratorCommand::GET_ADMIN_NOTES: + return cmdGetAdminNotes(cmd.GetExtension(Command_GetAdminNotes::ext), rc); + case ModeratorCommand::UPDATE_ADMIN_NOTES: + return cmdUpdateAdminNotes(cmd.GetExtension(Command_UpdateAdminNotes::ext), rc); default: return Response::RespFunctionNotAllowed; } @@ -1736,6 +1741,48 @@ Response::ResponseCode AbstractServerSocketInterface::cmdForceActivateUser(const return cmdActivateAccount(cmdActivate, rc); } +Response::ResponseCode AbstractServerSocketInterface::cmdGetAdminNotes(const Command_GetAdminNotes &cmd, + ResponseContainer &rc) +{ + auto *getAdminNotesQuery = sqlInterface->prepareQuery("select adminnotes from {prefix}_users WHERE name = :name"); + getAdminNotesQuery->bindValue(":name", QString::fromStdString(cmd.user_name())); + if (!sqlInterface->execSqlQuery(getAdminNotesQuery)) { + // Internal server error + return Response::RespInternalError; + } + if (!getAdminNotesQuery->next()) { + // User doesn't exist + return Response::RespNameNotFound; + } + const auto &adminNotes = getAdminNotesQuery->value(0).toString(); + + Response_GetAdminNotes *re = new Response_GetAdminNotes; + re->set_user_name(cmd.user_name()); + re->set_notes(adminNotes.toStdString()); + rc.setResponseExtension(re); + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdUpdateAdminNotes(const Command_UpdateAdminNotes &cmd, + ResponseContainer & /*rc*/) +{ + auto *updateAdminNotesQuery = + sqlInterface->prepareQuery("update {prefix}_users set adminnotes = :adminnotes where name = :name"); + updateAdminNotesQuery->bindValue(":adminnotes", QString::fromStdString(cmd.notes())); + updateAdminNotesQuery->bindValue(":name", QString::fromStdString(cmd.user_name())); + + if (!sqlInterface->execSqlQuery(updateAdminNotesQuery)) { + return Response::RespInternalError; + } + + if (updateAdminNotesQuery->numRowsAffected() == 0) { + return Response::RespNameNotFound; + } + + return Response::RespOk; +} + TcpServerSocketInterface::TcpServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface, QObject *parent) diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index bda6ca699..88d0fc549 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -129,6 +129,9 @@ private: Response::ResponseCode cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, ResponseContainer &rc); Response::ResponseCode cmdForceActivateUser(const Command_ForceActivateUser &cmd, ResponseContainer &rc); + Response::ResponseCode cmdGetAdminNotes(const Command_GetAdminNotes &cmd, ResponseContainer &rc); + Response::ResponseCode cmdUpdateAdminNotes(const Command_UpdateAdminNotes &cmd, ResponseContainer &rc); + bool addAdminFlagToUser(const QString &user, int flag); bool removeAdminFlagFromUser(const QString &user, int flag);