diff --git a/.gitignore b/.gitignore index a58ddf74..35b68d81 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ mysql.cnf *.aps cmake-build-debug/ preferences +compile_commands.json +.vs/ diff --git a/cockatrice/src/chatview/chatview.cpp b/cockatrice/src/chatview/chatview.cpp index 3dc88061..72baed39 100644 --- a/cockatrice/src/chatview/chatview.cpp +++ b/cockatrice/src/chatview/chatview.cpp @@ -12,10 +12,17 @@ #include #include #include +#include #include const QColor DEFAULT_MENTION_COLOR = QColor(194, 31, 47); +UserMessagePosition::UserMessagePosition(QTextCursor &cursor) +{ + block = cursor.block(); + relativePosition = cursor.position() - block.position(); +} + ChatView::ChatView(const TabSupervisor *_tabSupervisor, const UserlistProxy *_userlistProxy, TabGame *_game, @@ -43,8 +50,8 @@ ChatView::ChatView(const TabSupervisor *_tabSupervisor, userContextMenu = new UserContextMenu(tabSupervisor, this, game); connect(userContextMenu, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool))); - userName = userlistProxy->getOwnUsername(); - mention = "@" + userName; + ownUserName = userlistProxy->getOwnUsername(); + mention = "@" + ownUserName; mentionFormat.setFontWeight(QFont::Bold); @@ -68,7 +75,7 @@ QTextCursor ChatView::prepareBlock(bool same) { lastSender.clear(); - QTextCursor cursor(document()->lastBlock()); + QTextCursor cursor(document()); cursor.movePosition(QTextCursor::End); if (same) { cursor.insertHtml("
"); @@ -145,76 +152,86 @@ void ChatView::appendUrlTag(QTextCursor &cursor, QString url) void ChatView::appendMessage(QString message, RoomMessageTypeFlags messageType, - QString sender, + const QString &userName, UserLevelFlags userLevel, QString UserPrivLevel, bool playerBold) { bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum(); - bool sameSender = (sender == lastSender) && !lastSender.isEmpty(); + // messageType should be Event_RoomSay::UserMessage though we don't actually check + bool isUserMessage = !(userName.toLower() == "servatrice" || userName.isEmpty()); + bool sameSender = isUserMessage && userName == lastSender; QTextCursor cursor = prepareBlock(sameSender); - lastSender = sender; + lastSender = userName; // timestamp - if (showTimestamps && (!sameSender || sender.toLower() == "servatrice") && !sender.isEmpty()) { + if (showTimestamps && ((!sameSender && isUserMessage) || userName.toLower() == "servatrice")) { QTextCharFormat timeFormat; timeFormat.setForeground(serverMessageColor); - if (sender.isEmpty()) - timeFormat.setFontWeight(QFont::Bold); + timeFormat.setFontWeight(QFont::Bold); cursor.setCharFormat(timeFormat); cursor.insertText(QDateTime::currentDateTime().toString("[hh:mm:ss] ")); } // nickname - if (sender.toLower() != "servatrice") { + if (isUserMessage) { QTextCharFormat senderFormat; - if (sender == userName) { + if (userName == ownUserName) { senderFormat.setForeground(QBrush(getCustomMentionColor())); senderFormat.setFontWeight(QFont::Bold); } else { senderFormat.setForeground(QBrush(otherUserColor)); - if (playerBold) + if (playerBold) { senderFormat.setFontWeight(QFont::Bold); + } } senderFormat.setAnchor(true); - senderFormat.setAnchorHref("user://" + QString::number(userLevel) + "_" + sender); + senderFormat.setAnchorHref("user://" + QString::number(userLevel) + "_" + userName); if (sameSender) { cursor.insertText(" "); } else { - if (!sender.isEmpty()) { - const int pixelSize = QFontInfo(cursor.charFormat().font()).pixelSize(); - bool isBuddy = userlistProxy->isUserBuddy(sender); - cursor.insertImage( - UserLevelPixmapGenerator::generatePixmap(pixelSize, userLevel, isBuddy, UserPrivLevel).toImage()); - cursor.insertText(" "); - } + const int pixelSize = QFontInfo(cursor.charFormat().font()).pixelSize(); + bool isBuddy = userlistProxy->isUserBuddy(userName); + cursor.insertImage( + UserLevelPixmapGenerator::generatePixmap(pixelSize, userLevel, isBuddy, UserPrivLevel).toImage()); + cursor.insertText(" "); cursor.setCharFormat(senderFormat); - if (!sender.isEmpty()) - sender.append(": "); - cursor.insertText(sender); + cursor.insertText(userName); + cursor.insertText(": "); + userMessagePositions[userName].append(cursor); } } // use different color for server messages defaultFormat = QTextCharFormat(); - if (sender.isEmpty()) { - switch (messageType) { - case Event_RoomSay::Welcome: - defaultFormat.setForeground(Qt::darkGreen); - defaultFormat.setFontWeight(QFont::Bold); - break; - case Event_RoomSay::ChatHistory: - defaultFormat.setForeground(Qt::gray); - defaultFormat.setFontWeight(QFont::Light); - defaultFormat.setFontItalic(true); - break; - default: - defaultFormat.setForeground(Qt::darkGreen); - defaultFormat.setFontWeight(QFont::Bold); + if (!isUserMessage) { + if (messageType == Event_RoomSay::ChatHistory) { + defaultFormat.setForeground(Qt::gray); // FIXME : hardcoded color + defaultFormat.setFontWeight(QFont::Light); + defaultFormat.setFontItalic(true); + static const QRegularExpression userNameRegex("^(\\[[^\\]]*\\]\\s)(\\S+):\\s"); + auto match = userNameRegex.match(message); + if (match.hasMatch()) { + cursor.setCharFormat(defaultFormat); + UserMessagePosition pos(cursor); + pos.relativePosition = match.captured(0).length(); // set message start + auto before = match.captured(1); + auto sentBy = match.captured(2); + cursor.insertText(before); // add message timestamp + QTextCharFormat senderFormat(defaultFormat); + senderFormat.setAnchor(true); + // this underscore is important, it is used to add the user level, but in this case the level is + // unknown, if the name contains an underscore it would split up the name + senderFormat.setAnchorHref("user://_" + sentBy); + cursor.setCharFormat(senderFormat); + cursor.insertText(sentBy); // add username with href so it shows the menu + userMessagePositions[sentBy].append(pos); // save message position + message.remove(0, pos.relativePosition - 2); // do not remove semicolon + } + } else { + defaultFormat.setForeground(Qt::darkGreen); // FIXME : hardcoded color + defaultFormat.setFontWeight(QFont::Bold); } - } else if (sender.toLower() == "servatrice") { - defaultFormat.setForeground(Qt::darkGreen); - defaultFormat.setFontWeight(QFont::Bold); } cursor.setCharFormat(defaultFormat); @@ -234,7 +251,7 @@ void ChatView::appendMessage(QString message, break; case '@': if (mentionEnabled) { - checkMention(cursor, message, sender, userLevel); + checkMention(cursor, message, userName, userLevel); } else { cursor.insertText(c, defaultFormat); message = message.mid(1); @@ -304,7 +321,7 @@ void ChatView::checkTag(QTextCursor &cursor, QString &message) checkWord(cursor, message); } -void ChatView::checkMention(QTextCursor &cursor, QString &message, QString &sender, UserLevelFlags userLevel) +void ChatView::checkMention(QTextCursor &cursor, QString &message, const QString &userName, UserLevelFlags userLevel) { const QRegExp notALetterOrNumber = QRegExp("[^a-zA-Z0-9]"); @@ -316,7 +333,7 @@ void ChatView::checkMention(QTextCursor &cursor, QString &message, QString &send const ServerInfo_User *onlineUser = userlistProxy->getOnlineUser(fullMentionUpToSpaceOrEnd); if (onlineUser) // Is there a user online named this? { - if (userName.toLower() == fullMentionUpToSpaceOrEnd.toLower()) // Is this user you? + if (ownUserName.toLower() == fullMentionUpToSpaceOrEnd.toLower()) // Is this user you? { // You have received a valid mention!! soundEngine->playSound("chat_mention"); @@ -325,7 +342,7 @@ void ChatView::checkMention(QTextCursor &cursor, QString &message, QString &send : QBrush(Qt::black)); cursor.insertText(mention, mentionFormat); message = message.mid(mention.size()); - showSystemPopup(sender); + showSystemPopup(userName); } else { QString correctUserName = QString::fromStdString(onlineUser->name()); mentionFormatOtherUser.setAnchorHref("user://" + QString::number(onlineUser->user_level()) + "_" + @@ -347,7 +364,7 @@ void ChatView::checkMention(QTextCursor &cursor, QString &message, QString &send : QBrush(Qt::black)); cursor.insertText("@" + fullMentionUpToSpaceOrEnd, mentionFormat); message = message.mid(fullMentionUpToSpaceOrEnd.size() + 1); - showSystemPopup(sender); + showSystemPopup(userName); cursor.setCharFormat(defaultFormat); return; @@ -445,12 +462,11 @@ void ChatView::actMessageClicked() emit messageClickedSignal(); } -void ChatView::showSystemPopup(QString &sender) +void ChatView::showSystemPopup(const QString &userName) { QApplication::alert(this); if (SettingsCache::instance().getShowMentionPopup()) { - QString ref = sender.left(sender.length() - 2); - emit showMentionPopup(ref); + emit showMentionPopup(userName); } } @@ -474,6 +490,28 @@ void ChatView::clearChat() lastSender = ""; } +void ChatView::redactMessages(const QString &userName, int amount) +{ + auto &messagePositions = userMessagePositions[userName]; + bool removedLastMessage = false; + QTextCursor cursor(document()); + for (; !messagePositions.isEmpty() && amount != 0; --amount) { + auto position = messagePositions.takeLast(); // go backwards from last message + cursor.setPosition(position.block.position()); // move to start of block, then continue to start of message + cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, position.relativePosition); + cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); // select until end of block + cursor.removeSelectedText(); + // if the cursor is at the end of the text it is possible to add text to this block still + removedLastMessage |= cursor.atEnd(); + // we will readd this position later + } + if (removedLastMessage) { + cursor.movePosition(QTextCursor::End); + messagePositions.append(cursor); + // note that this message might stay empty, this is not harmful as it will simply remove nothing the next time + } +} + void ChatView::enterEvent(QEvent * /*event*/) { setMouseTracking(true); @@ -540,7 +578,7 @@ void ChatView::mousePressEvent(QMouseEvent *event) switch (event->button()) { case Qt::RightButton: { UserLevelFlags userLevel(hoveredContent.left(delimiterIndex).toInt()); - userContextMenu->showContextMenu(event->globalPos(), userName, userLevel); + userContextMenu->showContextMenu(event->globalPos(), userName, userLevel, this); break; } case Qt::LeftButton: { diff --git a/cockatrice/src/chatview/chatview.h b/cockatrice/src/chatview/chatview.h index 24d04ec7..23659959 100644 --- a/cockatrice/src/chatview/chatview.h +++ b/cockatrice/src/chatview/chatview.h @@ -18,6 +18,17 @@ class QMouseEvent; class UserContextMenu; class TabGame; +class UserMessagePosition +{ +public: +#if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0)) + UserMessagePosition() = default; // older qt versions require a default constructor to use in containers +#endif + UserMessagePosition(QTextCursor &cursor); + int relativePosition; + QTextBlock block; +}; + class ChatView : public QTextBrowser { Q_OBJECT @@ -36,7 +47,7 @@ private: const UserlistProxy *const userlistProxy; UserContextMenu *userContextMenu; QString lastSender; - QString userName; + QString ownUserName; QString mention; QTextCharFormat mentionFormat; QTextCharFormat highlightFormat; @@ -48,16 +59,18 @@ private: HoveredItemType hoveredItemType; QString hoveredContent; QAction *messageClicked; + QMap> userMessagePositions; + QTextFragment getFragmentUnderMouse(const QPoint &pos) const; QTextCursor prepareBlock(bool same = false); void appendCardTag(QTextCursor &cursor, const QString &cardName); void appendUrlTag(QTextCursor &cursor, QString url); QColor getCustomMentionColor(); QColor getCustomHighlightColor(); - void showSystemPopup(QString &sender); + void showSystemPopup(const QString &userName); bool isModeratorSendingGlobal(QFlags userLevelFlag, QString message); void checkTag(QTextCursor &cursor, QString &message); - void checkMention(QTextCursor &cursor, QString &message, QString &sender, UserLevelFlags userLevel); + void checkMention(QTextCursor &cursor, QString &message, const QString &userName, UserLevelFlags userLevel); void checkWord(QTextCursor &cursor, QString &message); QString extractNextWord(QString &message, QString &rest); @@ -82,11 +95,12 @@ public: QString optionalFontColor = QString()); void appendMessage(QString message, RoomMessageTypeFlags messageType = {}, - QString sender = QString(), + const QString &userName = QString(), UserLevelFlags userLevel = UserLevelFlags(), QString UserPrivLevel = "NONE", bool playerBold = false); void clearChat(); + void redactMessages(const QString &userName, int amount); protected: void enterEvent(QEvent *event); @@ -101,7 +115,7 @@ signals: void deleteCardInfoPopup(QString cardName); void addMentionTag(QString mentionTag); void messageClickedSignal(); - void showMentionPopup(QString &sender); + void showMentionPopup(const QString &userName); }; #endif diff --git a/cockatrice/src/tab_room.cpp b/cockatrice/src/tab_room.cpp index fdeee397..7b125299 100644 --- a/cockatrice/src/tab_room.cpp +++ b/cockatrice/src/tab_room.cpp @@ -9,6 +9,7 @@ #include "pb/event_join_room.pb.h" #include "pb/event_leave_room.pb.h" #include "pb/event_list_games.pb.h" +#include "pb/event_remove_messages.pb.h" #include "pb/event_room_say.pb.h" #include "pb/room_commands.pb.h" #include "pb/serverinfo_room.pb.h" @@ -49,7 +50,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this, SIGNAL(openMessageDialog(const QString &, bool))); - chatView = new ChatView(tabSupervisor, tabSupervisor, 0, true); + chatView = new ChatView(tabSupervisor, tabSupervisor, nullptr, true, this); connect(chatView, SIGNAL(showMentionPopup(QString &)), this, SLOT(actShowMentionPopup(QString &))); connect(chatView, SIGNAL(messageClickedSignal()), this, SLOT(focusTab())); connect(chatView, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool))); @@ -252,6 +253,9 @@ void TabRoom::processRoomEvent(const RoomEvent &event) case RoomEvent::ROOM_SAY: processRoomSayEvent(event.GetExtension(Event_RoomSay::ext)); break; + case RoomEvent::REMOVE_MESSAGES: + processRemoveMessagesEvent(event.GetExtension(Event_RemoveMessages::ext)); + break; default:; } } @@ -312,6 +316,13 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event) emit userEvent(false); } +void TabRoom::processRemoveMessagesEvent(const Event_RemoveMessages &event) +{ + QString userName = QString::fromStdString(event.name()); + int amount = event.amount(); + chatView->redactMessages(userName, amount); +} + void TabRoom::refreshShortcuts() { aClearChat->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("tab_room/aClearChat")); diff --git a/cockatrice/src/tab_room.h b/cockatrice/src/tab_room.h index b1169b69..d048f26e 100644 --- a/cockatrice/src/tab_room.h +++ b/cockatrice/src/tab_room.h @@ -30,6 +30,7 @@ class Event_ListGames; class Event_JoinRoom; class Event_LeaveRoom; class Event_RoomSay; +class Event_RemoveMessages; class GameSelector; class Response; class PendingCommand; @@ -82,6 +83,7 @@ private slots: void processJoinRoomEvent(const Event_JoinRoom &event); void processLeaveRoomEvent(const Event_LeaveRoom &event); void processRoomSayEvent(const Event_RoomSay &event); + void processRemoveMessagesEvent(const Event_RemoveMessages &event); void refreshShortcuts(); public: diff --git a/cockatrice/src/user_context_menu.cpp b/cockatrice/src/user_context_menu.cpp index 76172111..18d0f42a 100644 --- a/cockatrice/src/user_context_menu.cpp +++ b/cockatrice/src/user_context_menu.cpp @@ -1,6 +1,7 @@ #include "user_context_menu.h" #include "abstractclient.h" +#include "chatview/chatview.h" #include "gameselector.h" #include "pb/command_kick_from_game.pb.h" #include "pb/commands.pb.h" @@ -253,6 +254,10 @@ void UserContextMenu::banUser_dialogFinished() cmd.set_reason(dlg->getReason().toStdString()); cmd.set_visible_reason(dlg->getVisibleReason().toStdString()); cmd.set_clientid(dlg->getBanId().toStdString()); + int removeAmount = dlg->getDeleteMessages(); + if (removeAmount != 0) { + cmd.set_remove_messages(removeAmount); + } client->sendCommand(client->prepareModeratorCommand(cmd)); } @@ -268,6 +273,10 @@ void UserContextMenu::warnUser_dialogFinished() cmd.set_user_name(dlg->getName().toStdString()); cmd.set_reason(dlg->getReason().toStdString()); cmd.set_clientid(dlg->getWarnID().toStdString()); + int removeAmount = dlg->getDeleteMessages(); + if (removeAmount != 0) { + cmd.set_remove_messages(removeAmount); + } client->sendCommand(client->prepareModeratorCommand(cmd)); } @@ -278,7 +287,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos, bool online, int playerId) { - showContextMenu(pos, userName, userLevel, online, playerId, QString()); + showContextMenu(pos, userName, userLevel, online, playerId, QString(), nullptr); +} + +void UserContextMenu::showContextMenu(const QPoint &pos, + const QString &userName, + UserLevelFlags userLevel, + ChatView *chatView) +{ + showContextMenu(pos, userName, userLevel, true, -1, QString(), chatView); } void UserContextMenu::showContextMenu(const QPoint &pos, @@ -286,9 +303,10 @@ void UserContextMenu::showContextMenu(const QPoint &pos, UserLevelFlags userLevel, bool online, int playerId, - const QString &deckHash) + const QString &deckHash, + ChatView *chatView) { - QAction *aCopyToClipBoard; + QAction *aCopyToClipBoard, *aRemoveMessages; aUserName->setText(userName); QMenu *menu = new QMenu(static_cast(parent())); @@ -303,14 +321,20 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addAction(aChat); if (userLevel.testFlag(ServerInfo_User::IsRegistered) && tabSupervisor->isOwnUserRegistered()) { menu->addSeparator(); - if (tabSupervisor->isUserBuddy(userName)) + if (tabSupervisor->isUserBuddy(userName)) { menu->addAction(aRemoveFromBuddyList); - else + } else { menu->addAction(aAddToBuddyList); - if (tabSupervisor->isUserIgnored(userName)) + } + if (tabSupervisor->isUserIgnored(userName)) { menu->addAction(aRemoveFromIgnoreList); - else + } else { menu->addAction(aAddToIgnoreList); + } + } + if (chatView != nullptr) { + aRemoveMessages = new QAction(tr("Remove this user's messages"), this); + menu->addAction(aRemoveMessages); } if (game && (game->isHost() || !tabSupervisor->getAdminLocked())) { menu->addSeparator(); @@ -360,7 +384,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos, aDemoteFromMod->setEnabled(anotherUser); QAction *actionClicked = menu->exec(pos); - if (actionClicked == aDetails) { + if (actionClicked == nullptr) { + } else if (actionClicked == aDetails) { UserInfoBox *infoWidget = new UserInfoBox(client, false, static_cast(parent()), Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint); @@ -456,6 +481,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos, } else if (actionClicked == aCopyToClipBoard) { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(deckHash); + } else if (actionClicked == aRemoveMessages) { + chatView->redactMessages(userName, -1); } delete menu; diff --git a/cockatrice/src/user_context_menu.h b/cockatrice/src/user_context_menu.h index b87dafc3..3612ea44 100644 --- a/cockatrice/src/user_context_menu.h +++ b/cockatrice/src/user_context_menu.h @@ -5,14 +5,16 @@ #include -class QAction; -class TabSupervisor; -class TabGame; -class QPoint; -class CommandContainer; -class Response; class AbstractClient; +class ChatView; +class CommandContainer; +class QAction; +class QMenu; +class QPoint; +class Response; class ServerInfo_User; +class TabGame; +class TabSupervisor; class UserContextMenu : public QObject { @@ -54,12 +56,14 @@ public: UserLevelFlags userLevel, bool online = true, int playerId = -1); + void showContextMenu(const QPoint &pos, const QString &userName, UserLevelFlags userLevel, ChatView *chatView); void showContextMenu(const QPoint &pos, const QString &userName, UserLevelFlags userLevel, bool online, int playerId, - const QString &deckHash); + const QString &deckHash, + ChatView *chatView = nullptr); }; #endif diff --git a/cockatrice/src/userlist.cpp b/cockatrice/src/userlist.cpp index fa39d69b..bbc6a010 100644 --- a/cockatrice/src/userlist.cpp +++ b/cockatrice/src/userlist.cpp @@ -97,6 +97,8 @@ BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(par new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person.")); visibleReasonEdit = new QPlainTextEdit; + deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms")); + QPushButton *okButton = new QPushButton(tr("&OK")); okButton->setAutoDefault(true); connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked())); @@ -115,6 +117,7 @@ BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(par vbox->addWidget(reasonEdit); vbox->addWidget(visibleReasonLabel); vbox->addWidget(visibleReasonEdit); + vbox->addWidget(deleteMessages); vbox->addLayout(buttonLayout); setLayout(vbox); @@ -130,6 +133,8 @@ WarningDialog::WarningDialog(const QString userName, const QString clientID, QWi warningOption = new QComboBox(); warningOption->addItem(""); + deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms")); + QPushButton *okButton = new QPushButton(tr("&OK")); okButton->setAutoDefault(true); connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked())); @@ -145,6 +150,7 @@ WarningDialog::WarningDialog(const QString userName, const QString clientID, QWi vbox->addWidget(descriptionLabel); vbox->addWidget(nameWarning); vbox->addWidget(warningOption); + vbox->addWidget(deleteMessages); vbox->addLayout(buttonLayout); setLayout(vbox); setWindowTitle(tr("Warn user for misconduct")); @@ -182,6 +188,11 @@ QString WarningDialog::getReason() const return warningOption->currentText().simplified(); } +int WarningDialog::getDeleteMessages() const +{ + return deleteMessages->isChecked() ? -1 : 0; +} + void WarningDialog::addWarningOption(const QString warning) { warningOption->addItem(warning); @@ -262,6 +273,11 @@ QString BanDialog::getVisibleReason() const return visibleReasonEdit->toPlainText(); } +int BanDialog::getDeleteMessages() const +{ + return deleteMessages->isChecked() ? -1 : 0; +} + UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent) { } diff --git a/cockatrice/src/userlist.h b/cockatrice/src/userlist.h index 2ddfc271..525fa279 100644 --- a/cockatrice/src/userlist.h +++ b/cockatrice/src/userlist.h @@ -28,7 +28,7 @@ class BanDialog : public QDialog Q_OBJECT private: QLabel *daysLabel, *hoursLabel, *minutesLabel; - QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox; + QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages; QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit; QSpinBox *daysEdit, *hoursEdit, *minutesEdit; QRadioButton *permanentRadio, *temporaryRadio; @@ -45,6 +45,7 @@ public: int getMinutes() const; QString getReason() const; QString getVisibleReason() const; + int getDeleteMessages() const; }; class WarningDialog : public QDialog @@ -55,6 +56,7 @@ private: QLineEdit *nameWarning; QComboBox *warningOption; QLineEdit *warnClientID; + QCheckBox *deleteMessages; private slots: void okClicked(); @@ -63,6 +65,7 @@ public: QString getName() const; QString getWarnID() const; QString getReason() const; + int getDeleteMessages() const; void addWarningOption(const QString warning); }; diff --git a/common/pb/CMakeLists.txt b/common/pb/CMakeLists.txt index ea1855d8..9c120b90 100644 --- a/common/pb/CMakeLists.txt +++ b/common/pb/CMakeLists.txt @@ -91,6 +91,7 @@ SET(PROTO_FILES event_reverse_turn.proto event_roll_die.proto event_room_say.proto + event_remove_messages.proto event_server_complete_list.proto event_server_identification.proto event_server_message.proto diff --git a/common/pb/event_remove_messages.proto b/common/pb/event_remove_messages.proto new file mode 100644 index 00000000..bb102346 --- /dev/null +++ b/common/pb/event_remove_messages.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +import "room_event.proto"; + +message Event_RemoveMessages { + extend RoomEvent { + optional Event_RemoveMessages ext = 1004; + } + optional string name = 1; + optional uint32 amount = 2; +} diff --git a/common/pb/moderator_commands.proto b/common/pb/moderator_commands.proto index cc2156b8..d300241b 100644 --- a/common/pb/moderator_commands.proto +++ b/common/pb/moderator_commands.proto @@ -21,6 +21,7 @@ message Command_BanFromServer { optional string reason = 4; optional string visible_reason = 5; optional string clientid = 6; + optional uint32 remove_messages = 7; } message Command_GetBanHistory { @@ -38,6 +39,7 @@ message Command_WarnUser { optional string user_name = 1; optional string reason = 2; optional string clientid = 3; + optional uint32 remove_messages = 4; } message Command_GetWarnHistory { diff --git a/common/pb/room_event.proto b/common/pb/room_event.proto index b0cb3702..9fa70e7e 100644 --- a/common/pb/room_event.proto +++ b/common/pb/room_event.proto @@ -5,6 +5,7 @@ message RoomEvent { JOIN_ROOM = 1001; ROOM_SAY = 1002; LIST_GAMES = 1003; + REMOVE_MESSAGES = 1004; } optional sint32 room_id = 1; extensions 100 to max; diff --git a/common/server.cpp b/common/server.cpp index 75538c3a..2a637e4e 100644 --- a/common/server.cpp +++ b/common/server.cpp @@ -389,6 +389,19 @@ void Server::externalRoomSay(int roomId, const QString &userName, const QString room->getId(), room->getName()); } +void Server::externalRoomRemoveMessages(int roomId, const QString &userName, int amount) +{ + // This function is always called from the main thread via signal/slot. + QReadLocker locker(&roomsLock); + + Server_Room *room = rooms.value(roomId); + if (room == nullptr) { + qDebug() << "externalRoomRemoveMessages: room id=" << roomId << "not found"; + return; + } + room->removeSaidMessages(userName, amount); +} + void Server::externalRoomGameListChanged(int roomId, const ServerInfo_Game &gameInfo) { // This function is always called from the main thread via signal/slot. diff --git a/common/server.h b/common/server.h index 8c06182a..259318ac 100644 --- a/common/server.h +++ b/common/server.h @@ -223,6 +223,7 @@ protected slots: void externalRoomUserJoined(int roomId, const ServerInfo_User &userInfo); void externalRoomUserLeft(int roomId, const QString &userName); void externalRoomSay(int roomId, const QString &userName, const QString &message); + void externalRoomRemoveMessages(int roomId, const QString &userName, int amount); void externalRoomGameListChanged(int roomId, const ServerInfo_Game &gameInfo); void externalJoinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId); diff --git a/common/server_room.cpp b/common/server_room.cpp index 9569a15e..82cb0ae2 100644 --- a/common/server_room.cpp +++ b/common/server_room.cpp @@ -4,6 +4,7 @@ #include "pb/event_join_room.pb.h" #include "pb/event_leave_room.pb.h" #include "pb/event_list_games.pb.h" +#include "pb/event_remove_messages.pb.h" #include "pb/event_room_say.pb.h" #include "pb/room_commands.pb.h" #include "pb/serverinfo_chat_message.pb.h" @@ -272,11 +273,11 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam return result; } -void Server_Room::say(const QString &userName, const QString &s, bool sendToIsl) +void Server_Room::say(const QString &userName, const QString &userMessage, bool sendToIsl) { Event_RoomSay event; event.set_name(userName.toStdString()); - event.set_message(s.toStdString()); + event.set_message(userMessage.toStdString()); sendRoomEvent(prepareRoomEvent(event), sendToIsl); if (chatHistorySize != 0) { @@ -285,13 +286,36 @@ void Server_Room::say(const QString &userName, const QString &s, bool sendToIsl) QString dateTimeString = dateTime.toString(); chatMessage.set_time(dateTimeString.toStdString()); chatMessage.set_sender_name(userName.toStdString()); - chatMessage.set_message(s.simplified().toStdString()); + chatMessage.set_message(userMessage.simplified().toStdString()); historyLock.lockForWrite(); - if (chatHistory.size() >= chatHistorySize) + if (chatHistory.size() >= chatHistorySize) { chatHistory.removeAt(0); + } - chatHistory << chatMessage; + chatHistory.push_back(std::move(chatMessage)); + historyLock.unlock(); + } +} + +void Server_Room::removeSaidMessages(const QString &userName, int amount, bool sendToIsl) +{ + Event_RemoveMessages event; + auto stdStringUserName = userName.toStdString(); + event.set_name(stdStringUserName); + event.set_amount(amount); + sendRoomEvent(prepareRoomEvent(event), sendToIsl); + + if (chatHistorySize != 0) { + int removed = 0; + historyLock.lockForWrite(); + // redact [amount] of the most recent messages from this user from history + for (auto message = chatHistory.rbegin(); message != chatHistory.rend() && removed != amount; ++message) { + if (message->sender_name() == stdStringUserName) { + message->clear_message(); + ++removed; + } + } historyLock.unlock(); } } diff --git a/common/server_room.h b/common/server_room.h index 7f55aee8..fafc6a18 100644 --- a/common/server_room.h +++ b/common/server_room.h @@ -132,6 +132,7 @@ public: Server_AbstractUserInterface *userInterface); void say(const QString &userName, const QString &s, bool sendToIsl = true); + void removeSaidMessages(const QString &userName, int amount, bool sendToIsl = true); void addGame(Server_Game *game); void removeGame(Server_Game *game); diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 1ba4780f..cc3722f7 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -6,6 +6,7 @@ #include "pb/event_join_room.pb.h" #include "pb/event_leave_room.pb.h" #include "pb/event_list_games.pb.h" +#include "pb/event_remove_messages.pb.h" #include "pb/event_room_say.pb.h" #include "pb/event_server_complete_list.pb.h" #include "pb/event_user_joined.pb.h" @@ -348,6 +349,11 @@ void IslInterface::roomEvent_ListGames(int roomId, const Event_ListGames &event) } } +void IslInterface::roomEvent_RemoveMessages(int roomId, const Event_RemoveMessages &event) +{ + emit externalRoomRemoveMessages(roomId, QString::fromStdString(event.name()), event.amount()); +} + void IslInterface::roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId) { emit joinGameCommandReceived(cmd, cmdId, roomId, serverId, sessionId); @@ -409,6 +415,9 @@ void IslInterface::processRoomEvent(const RoomEvent &event) case RoomEvent::LIST_GAMES: roomEvent_ListGames(event.room_id(), event.GetExtension(Event_ListGames::ext)); break; + case RoomEvent::REMOVE_MESSAGES: + roomEvent_RemoveMessages(event.room_id(), event.GetExtension(Event_RemoveMessages::ext)); + break; default:; } } diff --git a/servatrice/src/isl_interface.h b/servatrice/src/isl_interface.h index e9c7857d..19310e07 100644 --- a/servatrice/src/isl_interface.h +++ b/servatrice/src/isl_interface.h @@ -22,6 +22,7 @@ class Event_JoinRoom; class Event_LeaveRoom; class Event_RoomSay; class Event_ListGames; +class Event_RemoveMessages; class Command_JoinGame; class IslInterface : public QObject @@ -40,6 +41,7 @@ signals: void externalRoomUserLeft(int roomId, QString userName); void externalRoomSay(int roomId, QString userName, QString message); void externalRoomGameListChanged(int roomId, ServerInfo_Game gameInfo); + void externalRoomRemoveMessages(int roomId, QString userName, int amount); void joinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId); void gameCommandContainerReceived(const CommandContainer &cont, int playerId, int serverId, qint64 sessionId); void responseReceived(const Response &resp, qint64 sessionId); @@ -68,6 +70,7 @@ private: void roomEvent_UserLeft(int roomId, const Event_LeaveRoom &event); void roomEvent_Say(int roomId, const Event_RoomSay &event); void roomEvent_ListGames(int roomId, const Event_ListGames &event); + void roomEvent_RemoveMessages(int roomId, const Event_RemoveMessages &event); void roomCommand_JoinGame(const Command_JoinGame &cmd, int cmdId, int roomId, qint64 sessionId); diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index bfb8bc2f..26c3eab3 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -749,6 +749,8 @@ void Servatrice::addIslInterface(int serverId, IslInterface *interface) connect(interface, SIGNAL(externalRoomUserLeft(int, QString)), this, SLOT(externalRoomUserLeft(int, QString))); connect(interface, SIGNAL(externalRoomSay(int, QString, QString)), this, SLOT(externalRoomSay(int, QString, QString))); + connect(interface, SIGNAL(externalRoomRemoveMessages(int, QString, int)), this, + SLOT(externalRoomRemoveMessages(int, QString, int))); connect(interface, SIGNAL(externalRoomGameListChanged(int, ServerInfo_Game)), this, SLOT(externalRoomGameListChanged(int, ServerInfo_Game))); connect(interface, SIGNAL(joinGameCommandReceived(Command_JoinGame, int, int, int, qint64)), this, diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 7c5f2b47..a370cb8f 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -61,6 +61,7 @@ #include "server_logger.h" #include "server_player.h" #include "server_response_containers.h" +#include "server_room.h" #include "settingscache.h" #include "version_string.h" @@ -837,16 +838,28 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnHistory(const Co return Response::RespOk; } +void AbstractServerSocketInterface::removeSaidMessages(const QString &userName, int amount) +{ + for (auto *room : rooms.values()) { + room->removeSaidMessages(userName, amount); + } +} + Response::ResponseCode AbstractServerSocketInterface::cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer & /*rc*/) { - if (!sqlInterface->checkSql()) + if (!sqlInterface->checkSql()) { // sql database is required, without database there are no moderators anyway return Response::RespInternalError; + } QString userName = QString::fromStdString(cmd.user_name()).simplified(); QString warningReason = QString::fromStdString(cmd.reason()).simplified(); QString clientID = QString::fromStdString(cmd.clientid()).simplified(); QString sendingModerator = QString::fromStdString(userInfo->name()).simplified(); + int amountRemove = cmd.remove_messages(); + if (amountRemove != 0) { + removeSaidMessages(userName, amountRemove); + } if (sqlInterface->addWarning(userName, sendingModerator, warningReason, clientID)) { servatrice->clientsLock.lockForRead(); @@ -855,7 +868,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdWarnUser(const Command_ QList moderatorList = server->getOnlineModeratorList(); servatrice->clientsLock.unlock(); - if (user) { + if (user != nullptr) { Event_NotifyUser event; event.set_type(Event_NotifyUser::WARNING); event.set_warning_reason(cmd.reason()); @@ -891,6 +904,10 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com if (userName.isEmpty() && address.isEmpty() && clientID.isEmpty()) return Response::RespOk; + int amountRemove = cmd.remove_messages(); + if (amountRemove != 0) { + removeSaidMessages(userName, amountRemove); + } QString trustedSources = settingsCache->value("server/trusted_sources", "127.0.0.1,::1").toString(); int minutes = cmd.minutes(); if (trustedSources.contains(address, Qt::CaseInsensitive)) diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 8c4f57b4..9ade8b32 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -126,6 +126,7 @@ private: bool isPasswordLongEnough(const int passwordLength); static QString parseEmailAddress(const std::string &stdEmailAddress); + void removeSaidMessages(const QString &userName, int amount); public: AbstractServerSocketInterface(Servatrice *_server,