Major Directory Refactoring (#5118)

* refactored cardzone.cpp, added doc and changed if to switch case

* started moving every files into different folders

* remove undercase to match with other files naming convention

* refactored dialog files

* ran format.sh

* refactored client/tabs folder

* refactored client/tabs folder

* refactored client/tabs folder

* refactored client folder

* refactored carddbparser

* refactored dialogs

* Create sonar-project.properties

temporary file for lint

* Create build.yml

temporary file for lint

* removed all files from root directory

* removed all files from root directory

* added current branch to workflow

* fixed most broken import

* fixed issues while renaming files

* fixed oracle importer

* fixed dbconverter

* updated translations

* made sub-folders for client

* removed linter

* removed linter folder

* fixed oracle import

* revert card_zone documentation

* renamed db parser files name and deck_view imports

* fixed dlg file issue

* ran format file and fixed test file

* fixed carddb test files

* moved player folder in game

* updated translations and format files

* fixed peglib import

* format cmake files

* removing vcpkg to try to add it back later

* tried fixing vcpkg file

* renamed filter to filters and moved database parser to cards folder

* reverted translation files

* reverted oracle translated

* Update cockatrice/src/dialogs/dlg_register.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* Update cockatrice/src/client/ui/window_main.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* removed empty line at file start

* removed useless include from tab_supervisor.cpp

* refactored cardzone.cpp, added doc and changed if to switch case

* started moving every files into different folders

* remove undercase to match with other files naming convention

* refactored dialog files

* ran format.sh

* refactored client/tabs folder

* refactored client folder

* refactored carddbparser

* refactored dialogs

* removed all files from root directory

* Create sonar-project.properties

temporary file for lint

* Create build.yml

temporary file for lint

* added current branch to workflow

* fixed most broken import

* fixed issues while renaming files

* fixed oracle importer

* fixed dbconverter

* updated translations

* made sub-folders for client

* removed linter

* removed linter folder

* fixed oracle import

* revert card_zone documentation

* renamed db parser files name and deck_view imports

* fixed dlg file issue

* ran format file and fixed test file

* fixed carddb test files

* moved player folder in game

* updated translations and format files

* fixed peglib import

* reverted translation files

* format cmake files

* removing vcpkg to try to add it back later

* tried fixing vcpkg file

* pre-updating of cockatrice changes

* removed empty line at file start

* reverted oracle translated

* Update cockatrice/src/dialogs/dlg_register.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* Update cockatrice/src/client/ui/window_main.cpp

Co-authored-by: tooomm <tooomm@users.noreply.github.com>

* removed useless include from tab_supervisor.cpp

---------

Co-authored-by: tooomm <tooomm@users.noreply.github.com>
This commit is contained in:
LunaticCat 2024-10-20 16:11:35 +02:00 committed by GitHub
parent d1e0f9dfc5
commit fa999880ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
261 changed files with 735 additions and 729 deletions

View file

@ -0,0 +1,635 @@
#include "chat_view.h"
#include "../../client/sound_engine.h"
#include "../../client/tabs/tab_account.h"
#include "../../client/ui/pixel_map_generator.h"
#include "../../settings/cache_settings.h"
#include "../user/user_context_menu.h"
#include "user_level.h"
#include <QApplication>
#include <QDateTime>
#include <QDesktopServices>
#include <QMouseEvent>
#include <QScrollBar>
const QColor DEFAULT_MENTION_COLOR = QColor(194, 31, 47);
UserMessagePosition::UserMessagePosition(QTextCursor &cursor)
{
block = cursor.block();
relativePosition = cursor.position() - block.position();
}
ChatView::ChatView(TabSupervisor *_tabSupervisor,
const UserlistProxy *_userlistProxy,
TabGame *_game,
bool _showTimestamps,
QWidget *parent)
: QTextBrowser(parent), tabSupervisor(_tabSupervisor), game(_game), userlistProxy(_userlistProxy), evenNumber(true),
showTimestamps(_showTimestamps), hoveredItemType(HoveredNothing)
{
if (palette().windowText().color().lightness() > 200) {
document()->setDefaultStyleSheet(R"(
a { text-decoration: none; color: rgb(71,158,252); }
.blue { color: rgb(71,158,252); }
)");
serverMessageColor = QColor(0xFF, 0x73, 0x83);
otherUserColor = otherUserColor.lighter(150);
linkColor = QColor(71, 158, 252);
} else {
document()->setDefaultStyleSheet(R"(
a { text-decoration: none; color: blue; }
.blue { color: blue }
)");
linkColor = palette().link().color();
}
userContextMenu = new UserContextMenu(tabSupervisor, this, game);
connect(userContextMenu, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool)));
ownUserName = userlistProxy->getOwnUsername();
mention = "@" + ownUserName;
mentionFormat.setFontWeight(QFont::Bold);
mentionFormatOtherUser.setFontWeight(QFont::Bold);
mentionFormatOtherUser.setForeground(linkColor);
mentionFormatOtherUser.setAnchor(true);
viewport()->setCursor(Qt::IBeamCursor);
setReadOnly(true);
setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
setOpenLinks(false);
connect(this, SIGNAL(anchorClicked(const QUrl &)), this, SLOT(openLink(const QUrl &)));
}
void ChatView::retranslateUi()
{
userContextMenu->retranslateUi();
}
QTextCursor ChatView::prepareBlock(bool same)
{
lastSender.clear();
QTextCursor cursor(document());
cursor.movePosition(QTextCursor::End);
if (same) {
cursor.insertHtml("<br>");
} else {
QTextBlockFormat blockFormat;
if ((evenNumber = !evenNumber))
blockFormat.setBackground(palette().window());
else
blockFormat.setBackground(palette().base());
blockFormat.setForeground(palette().text());
blockFormat.setBottomMargin(4);
cursor.insertBlock(blockFormat);
}
return cursor;
}
void ChatView::appendHtml(const QString &html)
{
bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum();
prepareBlock().insertHtml(html);
if (atBottom)
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor)
{
bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum();
QString htmlText =
"<font color=" + ((optionalFontColor.size() > 0) ? optionalFontColor : serverMessageColor.name()) + ">" +
QDateTime::currentDateTime().toString("[hh:mm:ss] ") + html + "</font>";
if (optionalIsBold)
htmlText = "<b>" + htmlText + "</b>";
prepareBlock().insertHtml(htmlText);
if (atBottom)
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName)
{
QTextCharFormat oldFormat = cursor.charFormat();
QTextCharFormat anchorFormat = oldFormat;
anchorFormat.setForeground(linkColor);
anchorFormat.setAnchor(true);
anchorFormat.setAnchorHref("card://" + cardName);
anchorFormat.setFontItalic(true);
cursor.setCharFormat(anchorFormat);
cursor.insertText(cardName);
cursor.setCharFormat(oldFormat);
}
void ChatView::appendUrlTag(QTextCursor &cursor, QString url)
{
if (!url.contains("://"))
url.prepend("https://");
QTextCharFormat oldFormat = cursor.charFormat();
QTextCharFormat anchorFormat = oldFormat;
anchorFormat.setForeground(linkColor);
anchorFormat.setAnchor(true);
anchorFormat.setAnchorHref(url);
anchorFormat.setUnderlineColor(linkColor);
anchorFormat.setFontUnderline(true);
cursor.setCharFormat(anchorFormat);
cursor.insertText(url);
cursor.setCharFormat(oldFormat);
}
void ChatView::appendMessage(QString message,
RoomMessageTypeFlags messageType,
const QString &userName,
UserLevelFlags userLevel,
QString UserPrivLevel,
bool playerBold)
{
bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum();
// 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 = userName;
// timestamp
if (showTimestamps && ((!sameSender && isUserMessage) || userName.toLower() == "servatrice")) {
QTextCharFormat timeFormat;
timeFormat.setForeground(serverMessageColor);
timeFormat.setFontWeight(QFont::Bold);
cursor.setCharFormat(timeFormat);
cursor.insertText(QDateTime::currentDateTime().toString("[hh:mm:ss] "));
}
// nickname
if (isUserMessage) {
QTextCharFormat senderFormat;
if (userName == ownUserName) {
senderFormat.setForeground(QBrush(getCustomMentionColor()));
senderFormat.setFontWeight(QFont::Bold);
} else {
senderFormat.setForeground(QBrush(otherUserColor));
if (playerBold) {
senderFormat.setFontWeight(QFont::Bold);
}
}
senderFormat.setAnchor(true);
senderFormat.setAnchorHref("user://" + QString::number(userLevel) + "_" + userName);
if (sameSender) {
cursor.insertText(" ");
} else {
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);
cursor.insertText(userName);
cursor.insertText(": ");
userMessagePositions[userName].append(cursor);
}
}
// use different color for server messages
defaultFormat = QTextCharFormat();
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);
}
}
cursor.setCharFormat(defaultFormat);
bool mentionEnabled = SettingsCache::instance().getChatMention();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
highlightedWords = SettingsCache::instance().getHighlightWords().split(' ', Qt::SkipEmptyParts);
#else
highlightedWords = SettingsCache::instance().getHighlightWords().split(' ', QString::SkipEmptyParts);
#endif
// parse the message
while (message.size()) {
QChar c = message.at(0);
switch (c.toLatin1()) {
case '[':
checkTag(cursor, message);
break;
case '@':
if (mentionEnabled) {
checkMention(cursor, message, userName, userLevel);
} else {
cursor.insertText(c, defaultFormat);
message = message.mid(1);
}
break;
case ' ':
cursor.insertText(c, defaultFormat);
message = message.mid(1);
break;
default:
if (c.isLetterOrNumber()) {
checkWord(cursor, message);
} else {
cursor.insertText(c, defaultFormat);
message = message.mid(1);
}
break;
}
}
if (atBottom)
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void ChatView::checkTag(QTextCursor &cursor, QString &message)
{
if (message.startsWith("[card]")) {
message = message.mid(6);
int closeTagIndex = message.indexOf("[/card]");
QString cardName = message.left(closeTagIndex);
if (closeTagIndex == -1)
message.clear();
else
message = message.mid(closeTagIndex + 7);
appendCardTag(cursor, cardName);
return;
}
if (message.startsWith("[[")) {
message = message.mid(2);
int closeTagIndex = message.indexOf("]]");
QString cardName = message.left(closeTagIndex);
if (closeTagIndex == -1)
message.clear();
else
message = message.mid(closeTagIndex + 2);
appendCardTag(cursor, cardName);
return;
}
if (message.startsWith("[url]")) {
message = message.mid(5);
int closeTagIndex = message.indexOf("[/url]");
QString url = message.left(closeTagIndex);
if (closeTagIndex == -1)
message.clear();
else
message = message.mid(closeTagIndex + 6);
appendUrlTag(cursor, url);
return;
}
// no valid tag found
checkWord(cursor, message);
}
void ChatView::checkMention(QTextCursor &cursor, QString &message, const QString &userName, UserLevelFlags userLevel)
{
const static auto notALetterOrNumber = QRegularExpression("[^a-zA-Z0-9]");
int firstSpace = message.indexOf(' ');
QString fullMentionUpToSpaceOrEnd = (firstSpace == -1) ? message.mid(1) : message.mid(1, firstSpace - 1);
QString mentionIntact = fullMentionUpToSpaceOrEnd;
while (fullMentionUpToSpaceOrEnd.size()) {
const ServerInfo_User *onlineUser = userlistProxy->getOnlineUser(fullMentionUpToSpaceOrEnd);
if (onlineUser) // Is there a user online named this?
{
if (ownUserName.toLower() == fullMentionUpToSpaceOrEnd.toLower()) // Is this user you?
{
// You have received a valid mention!!
soundEngine->playSound("chat_mention");
mentionFormat.setBackground(QBrush(getCustomMentionColor()));
mentionFormat.setForeground(SettingsCache::instance().getChatMentionForeground() ? QBrush(Qt::white)
: QBrush(Qt::black));
cursor.insertText(mention, mentionFormat);
message = message.mid(mention.size());
showSystemPopup(userName);
} else {
QString correctUserName = QString::fromStdString(onlineUser->name());
mentionFormatOtherUser.setAnchorHref("user://" + QString::number(onlineUser->user_level()) + "_" +
correctUserName);
cursor.insertText("@" + correctUserName, mentionFormatOtherUser);
message = message.mid(correctUserName.size() + 1);
}
cursor.setCharFormat(defaultFormat);
return;
}
if (isModeratorSendingGlobal(userLevel, fullMentionUpToSpaceOrEnd)) {
// Moderator Sending Global Message
soundEngine->playSound("all_mention");
mentionFormat.setBackground(QBrush(getCustomMentionColor()));
mentionFormat.setForeground(SettingsCache::instance().getChatMentionForeground() ? QBrush(Qt::white)
: QBrush(Qt::black));
cursor.insertText("@" + fullMentionUpToSpaceOrEnd, mentionFormat);
message = message.mid(fullMentionUpToSpaceOrEnd.size() + 1);
showSystemPopup(userName);
cursor.setCharFormat(defaultFormat);
return;
}
if (fullMentionUpToSpaceOrEnd.right(1).indexOf(notALetterOrNumber) == -1 ||
fullMentionUpToSpaceOrEnd.size() < 2) {
cursor.insertText("@" + mentionIntact, defaultFormat);
message = message.mid(mentionIntact.size() + 1);
cursor.setCharFormat(defaultFormat);
return;
}
fullMentionUpToSpaceOrEnd.chop(1);
}
// no valid mention found
checkWord(cursor, message);
}
void ChatView::checkWord(QTextCursor &cursor, QString &message)
{
// extract the first word
QString rest;
QString fullWordUpToSpaceOrEnd = extractNextWord(message, rest);
// check urls
if (fullWordUpToSpaceOrEnd.startsWith("http://", Qt::CaseInsensitive) ||
fullWordUpToSpaceOrEnd.startsWith("https://", Qt::CaseInsensitive) ||
fullWordUpToSpaceOrEnd.startsWith("www.", Qt::CaseInsensitive)) {
QUrl qUrl(fullWordUpToSpaceOrEnd);
if (qUrl.isValid()) {
appendUrlTag(cursor, fullWordUpToSpaceOrEnd);
cursor.insertText(rest, defaultFormat);
return;
}
}
// check word mentions
foreach (QString word, highlightedWords) {
if (fullWordUpToSpaceOrEnd.compare(word, Qt::CaseInsensitive) == 0) {
// You have received a valid mention of custom word!!
highlightFormat.setBackground(QBrush(getCustomHighlightColor()));
highlightFormat.setForeground(SettingsCache::instance().getChatHighlightForeground() ? QBrush(Qt::white)
: QBrush(Qt::black));
cursor.insertText(fullWordUpToSpaceOrEnd, highlightFormat);
cursor.insertText(rest, defaultFormat);
QApplication::alert(this);
return;
}
}
// not a special word; just print it
cursor.insertText(fullWordUpToSpaceOrEnd + rest, defaultFormat);
}
QString ChatView::extractNextWord(QString &message, QString &rest)
{
// get the first next space and extract the word
QString word;
int firstSpace = message.indexOf(' ');
if (firstSpace == -1) {
word = message;
message.clear();
} else {
word = message.mid(0, firstSpace);
message = message.mid(firstSpace);
}
// remove any punctuation from the end and pass it separately
for (int len = word.size() - 1; len >= 0; --len) {
if (word.at(len).isLetterOrNumber()) {
rest = word.mid(len + 1);
return word.mid(0, len + 1);
}
}
rest = word;
return QString();
}
bool ChatView::isModeratorSendingGlobal(QFlags<ServerInfo_User::UserLevelFlag> userLevelFlag, QString message)
{
int userLevel = QString::number(userLevelFlag).toInt();
QStringList getAttentionList;
getAttentionList << "/all"; // Send a message to all users
return (getAttentionList.contains(message) &&
(userLevel & ServerInfo_User::IsModerator || userLevel & ServerInfo_User::IsAdmin));
}
void ChatView::actMessageClicked()
{
emit messageClickedSignal();
}
void ChatView::showSystemPopup(const QString &userName)
{
QApplication::alert(this);
if (SettingsCache::instance().getShowMentionPopup()) {
emit showMentionPopup(userName);
}
}
QColor ChatView::getCustomMentionColor()
{
#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
QColor customColor = QColor::fromString("#" + SettingsCache::instance().getChatMentionColor());
#else
QColor customColor;
customColor.setNamedColor("#" + SettingsCache::instance().getChatMentionColor());
#endif
return customColor.isValid() ? customColor : DEFAULT_MENTION_COLOR;
}
QColor ChatView::getCustomHighlightColor()
{
#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
QColor customColor = QColor::fromString("#" + SettingsCache::instance().getChatMentionColor());
#else
QColor customColor;
customColor.setNamedColor("#" + SettingsCache::instance().getChatMentionColor());
#endif
return customColor.isValid() ? customColor : DEFAULT_MENTION_COLOR;
}
void ChatView::clearChat()
{
document()->clear();
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
}
}
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void ChatView::enterEvent(QEnterEvent * /*event*/)
#else
void ChatView::enterEvent(QEvent * /*event*/)
#endif
{
setMouseTracking(true);
}
void ChatView::leaveEvent(QEvent * /*event*/)
{
setMouseTracking(false);
}
QTextFragment ChatView::getFragmentUnderMouse(const QPoint &pos) const
{
QTextCursor cursor(cursorForPosition(pos));
QTextBlock block(cursor.block());
QTextBlock::iterator it;
for (it = block.begin(); !(it.atEnd()); ++it) {
QTextFragment frag = it.fragment();
if (frag.contains(cursor.position()))
return frag;
}
return QTextFragment();
}
void ChatView::mouseMoveEvent(QMouseEvent *event)
{
QString anchorHref = getFragmentUnderMouse(event->pos()).charFormat().anchorHref();
if (!anchorHref.isEmpty()) {
const int delimiterIndex = anchorHref.indexOf("://");
if (delimiterIndex != -1) {
const QString scheme = anchorHref.left(delimiterIndex);
hoveredContent = anchorHref.mid(delimiterIndex + 3);
if (scheme == "card") {
hoveredItemType = HoveredCard;
emit cardNameHovered(hoveredContent);
} else if (scheme == "user")
hoveredItemType = HoveredUser;
else
hoveredItemType = HoveredUrl;
viewport()->setCursor(Qt::PointingHandCursor);
} else {
hoveredItemType = HoveredNothing;
viewport()->setCursor(Qt::IBeamCursor);
}
} else {
hoveredItemType = HoveredNothing;
viewport()->setCursor(Qt::IBeamCursor);
}
QTextBrowser::mouseMoveEvent(event);
}
void ChatView::mousePressEvent(QMouseEvent *event)
{
switch (hoveredItemType) {
case HoveredCard: {
if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton))
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
emit showCardInfoPopup(event->globalPosition().toPoint(), hoveredContent);
#else
emit showCardInfoPopup(event->globalPos(), hoveredContent);
#endif
break;
}
case HoveredUser: {
if (event->button() != Qt::MiddleButton) {
const int delimiterIndex = hoveredContent.indexOf("_");
const QString userName = hoveredContent.mid(delimiterIndex + 1);
switch (event->button()) {
case Qt::RightButton: {
UserLevelFlags userLevel(hoveredContent.left(delimiterIndex).toInt());
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
userContextMenu->showContextMenu(event->globalPosition().toPoint(), userName, userLevel, this);
#else
userContextMenu->showContextMenu(event->globalPos(), userName, userLevel, this);
#endif
break;
}
case Qt::LeftButton: {
if (event->modifiers() == Qt::ControlModifier) {
emit openMessageDialog(userName, true);
} else
emit addMentionTag("@" + userName);
break;
}
default:
break;
}
}
break;
}
default: {
QTextBrowser::mousePressEvent(event);
}
}
}
void ChatView::mouseReleaseEvent(QMouseEvent *event)
{
if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton))
emit deleteCardInfoPopup(QString("_"));
QTextBrowser::mouseReleaseEvent(event);
}
void ChatView::openLink(const QUrl &link)
{
if ((link.scheme() == "card") || (link.scheme() == "user"))
return;
QDesktopServices::openUrl(link);
}

View file

@ -0,0 +1,125 @@
#ifndef CHATVIEW_H
#define CHATVIEW_H
#include "../../client/tabs/tab_supervisor.h"
#include "../user/user_list.h"
#include "room_message_type.h"
#include "user_level.h"
#include "user_list_proxy.h"
#include <QAction>
#include <QColor>
#include <QTextBrowser>
#include <QTextCursor>
#include <QTextFragment>
class QTextTable;
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
protected:
TabSupervisor *const tabSupervisor;
TabGame *const game;
private:
enum HoveredItemType
{
HoveredNothing,
HoveredUrl,
HoveredCard,
HoveredUser
};
const UserlistProxy *const userlistProxy;
UserContextMenu *userContextMenu;
QString lastSender;
QString ownUserName;
QString mention;
QTextCharFormat mentionFormat;
QTextCharFormat highlightFormat;
QTextCharFormat mentionFormatOtherUser;
QTextCharFormat defaultFormat;
QStringList highlightedWords;
bool evenNumber;
bool showTimestamps;
HoveredItemType hoveredItemType;
QString hoveredContent;
QAction *messageClicked;
QMap<QString, QVector<UserMessagePosition>> 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);
static QColor getCustomMentionColor();
static QColor getCustomHighlightColor();
void showSystemPopup(const QString &userName);
bool isModeratorSendingGlobal(QFlags<ServerInfo_User::UserLevelFlag> userLevelFlag, QString message);
void checkTag(QTextCursor &cursor, QString &message);
void checkMention(QTextCursor &cursor, QString &message, const QString &userName, UserLevelFlags userLevel);
void checkWord(QTextCursor &cursor, QString &message);
QString extractNextWord(QString &message, QString &rest);
QColor otherUserColor = QColor(0, 65, 255); // dark blue
QColor serverMessageColor = QColor(0x85, 0x15, 0x15);
QColor linkColor;
private slots:
void openLink(const QUrl &link);
void actMessageClicked();
public:
ChatView(TabSupervisor *_tabSupervisor,
const UserlistProxy *_userlistProxy,
TabGame *_game,
bool _showTimestamps,
QWidget *parent = nullptr);
void retranslateUi();
void appendHtml(const QString &html);
void virtual appendHtmlServerMessage(const QString &html,
bool optionalIsBold = false,
QString optionalFontColor = QString());
void appendMessage(QString message,
RoomMessageTypeFlags messageType = {},
const QString &userName = QString(),
UserLevelFlags userLevel = UserLevelFlags(),
QString UserPrivLevel = "NONE",
bool playerBold = false);
void clearChat();
void redactMessages(const QString &userName, int amount);
protected:
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void enterEvent(QEnterEvent *event);
#else
void enterEvent(QEvent *event);
#endif
void leaveEvent(QEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
signals:
void openMessageDialog(const QString &userName, bool focus);
void cardNameHovered(QString cardName);
void showCardInfoPopup(QPoint pos, QString cardName);
void deleteCardInfoPopup(QString cardName);
void addMentionTag(QString mentionTag);
void messageClickedSignal();
void showMentionPopup(const QString &userName);
};
#endif

View file

@ -0,0 +1,21 @@
#ifndef COCKATRICE_USERLISTPROXY_H
#define COCKATRICE_USERLISTPROXY_H
class QString;
class ServerInfo_User;
/**
* Responsible for providing a bare-bones minimal interface into userlist information,
* including your current connection to the server as well as buddy/ignore/alluser lists.
*/
class UserlistProxy
{
public:
virtual bool isOwnUserRegistered() const = 0;
virtual QString getOwnUsername() const = 0;
virtual bool isUserBuddy(const QString &userName) const = 0;
virtual bool isUserIgnored(const QString &userName) const = 0;
virtual const ServerInfo_User *getOnlineUser(const QString &userName) const = 0; // Can return nullptr
};
#endif // COCKATRICE_USERLISTPROXY_H

View file

@ -0,0 +1,110 @@
#include "handle_public_servers.h"
#include "../settings/cache_settings.h"
#include <QJsonDocument>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrl>
#define PUBLIC_SERVERS_JSON "https://cockatrice.github.io/public-servers.json"
HandlePublicServers::HandlePublicServers(QObject *parent)
: QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr)
{
}
void HandlePublicServers::downloadPublicServers()
{
QUrl url(QString(PUBLIC_SERVERS_JSON));
reply = nam->get(QNetworkRequest(url));
connect(reply, SIGNAL(finished()), this, SLOT(actFinishParsingDownloadedData()));
}
void HandlePublicServers::actFinishParsingDownloadedData()
{
reply = dynamic_cast<QNetworkReply *>(sender());
QNetworkReply::NetworkError errorCode = reply->error();
if (errorCode == QNetworkReply::NoError) {
// Get current saved hosts
UserConnection_Information uci;
savedHostList = uci.getServerInfo();
// Downloaded data from GitHub
QJsonParseError parseError{};
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
if (parseError.error == QJsonParseError::NoError) {
QVariantMap jsonMap = jsonResponse.toVariant().toMap();
updateServerINISettings(jsonMap);
} else {
qDebug() << "[PUBLIC SERVER HANDLER]"
<< "JSON Parsing Error:" << parseError.errorString();
emit sigPublicServersDownloadedUnsuccessfully(errorCode);
}
} else {
qDebug() << "[PUBLIC SERVER HANDLER]"
<< "Error Downloading Public Servers" << errorCode;
emit sigPublicServersDownloadedUnsuccessfully(errorCode);
}
reply->deleteLater(); // After an emit() occurs, this object will be deleted
}
void HandlePublicServers::updateServerINISettings(QMap<QString, QVariant> jsonMap)
{
// Servers available
auto publicServersJSONList = jsonMap["servers"].toList();
for (const auto &server : publicServersJSONList) {
// Data inside one server at a time
// server: [{ ... }, ..., { ... }]
const auto serverMap = server.toMap();
QString serverAddress = serverMap["host"].toString();
if (serverMap["isInactive"].toBool()) {
publicServersToRemove.append(serverAddress);
continue;
}
QString serverName = serverMap["name"].toString();
QString serverPort = serverMap["port"].toString();
QString serverSite = serverMap["site"].toString();
if (serverMap.contains("websocketPort")) {
serverPort = serverMap["websocketPort"].toString();
}
bool serverFound = false;
for (const auto &iter : savedHostList) {
// If the URL/IP matches
if (iter.second.getServer() == serverAddress) {
serverFound = true;
break;
}
}
if (serverFound) {
SettingsCache::instance().servers().updateExistingServerWithoutLoss(serverName, serverAddress, serverPort,
serverSite);
} else {
SettingsCache::instance().servers().addNewServer(serverName, serverAddress, serverPort, "", "", false,
serverSite);
}
}
// If a server was removed from the public list,
// we will delete it from the local system.
// Will not delete "unofficial" servers
for (const auto &pair : savedHostList) {
QString serverAddr = pair.first;
if (publicServersToRemove.indexOf(serverAddr) != -1) {
SettingsCache::instance().servers().removeServer(serverAddr);
}
}
emit sigPublicServersDownloadedSuccessfully();
}

View file

@ -0,0 +1,40 @@
#ifndef COCKATRICE_HANDLE_PUBLIC_SERVERS_H
#define COCKATRICE_HANDLE_PUBLIC_SERVERS_H
#include "user/user_info_connection.h"
class QNetworkReply;
class QNetworkAccessManager;
/**
* This class is used to update the servers.ini file and ensure
* the list of public servers has up-to-date information.
* Servers that are added manually by users are not modified.
*/
class HandlePublicServers : public QObject
{
Q_OBJECT
signals:
void sigPublicServersDownloadedSuccessfully();
void sigPublicServersDownloadedUnsuccessfully(int);
public:
explicit HandlePublicServers(QObject *parent = nullptr);
~HandlePublicServers() override = default;
public slots:
void downloadPublicServers();
private slots:
void actFinishParsingDownloadedData();
private:
void updateServerINISettings(QMap<QString, QVariant>);
QStringList publicServersToRemove;
QMap<QString, std::pair<QString, UserConnection_Information>> savedHostList;
QNetworkAccessManager *nam;
QNetworkReply *reply;
};
#endif // COCKATRICE_HANDLE_PUBLIC_SERVERS_H

View file

@ -0,0 +1,36 @@
#include "local_client.h"
#include "local_server_interface.h"
#include "pb/session_commands.pb.h"
LocalClient::LocalClient(LocalServerInterface *_lsi,
const QString &_playerName,
const QString &_clientId,
QObject *parent)
: AbstractClient(parent), lsi(_lsi)
{
connect(lsi, SIGNAL(itemToClient(const ServerMessage &)), this, SLOT(itemFromServer(const ServerMessage &)));
Command_Login loginCmd;
loginCmd.set_user_name(_playerName.toStdString());
loginCmd.set_clientid(_clientId.toStdString());
sendCommand(prepareSessionCommand(loginCmd));
Command_JoinRoom joinCmd;
joinCmd.set_room_id(0);
sendCommand(prepareSessionCommand(joinCmd));
}
LocalClient::~LocalClient()
{
}
void LocalClient::sendCommandContainer(const CommandContainer &cont)
{
lsi->itemFromClient(cont);
}
void LocalClient::itemFromServer(const ServerMessage &item)
{
processProtocolItem(item);
}

View file

@ -0,0 +1,26 @@
#ifndef LOCALCLIENT_H
#define LOCALCLIENT_H
#include "../client/game_logic/abstract_client.h"
class LocalServerInterface;
class LocalClient : public AbstractClient
{
Q_OBJECT
private:
LocalServerInterface *lsi;
public:
LocalClient(LocalServerInterface *_lsi,
const QString &_playerName,
const QString &_clientId,
QObject *parent = nullptr);
~LocalClient();
void sendCommandContainer(const CommandContainer &cont);
private slots:
void itemFromServer(const ServerMessage &item);
};
#endif

View file

@ -0,0 +1,50 @@
#include "local_server.h"
#include "local_server_interface.h"
#include "server_room.h"
LocalServer::LocalServer(QObject *parent) : Server(parent)
{
setDatabaseInterface(new LocalServer_DatabaseInterface(this));
addRoom(new Server_Room(0, 0, QString(), QString(), QString(), QString(), false, QString(), QStringList(), this));
}
LocalServer::~LocalServer()
{
// LocalServer is single threaded so it doesn't need locks on this
for (auto *client : clients) {
client->prepareDestroy();
}
prepareDestroy();
}
LocalServerInterface *LocalServer::newConnection()
{
LocalServerInterface *lsi = new LocalServerInterface(this, getDatabaseInterface());
addClient(lsi);
return lsi;
}
LocalServer_DatabaseInterface::LocalServer_DatabaseInterface(LocalServer *_localServer)
: Server_DatabaseInterface(_localServer), localServer(_localServer)
{
}
ServerInfo_User LocalServer_DatabaseInterface::getUserData(const QString &name, bool /*withId*/)
{
ServerInfo_User result;
result.set_name(name.toStdString());
return result;
}
AuthenticationResult LocalServer_DatabaseInterface::checkUserPassword(Server_ProtocolHandler * /* handler */,
const QString & /* user */,
const QString & /* password */,
const QString & /* clientId */,
QString & /* reasonStr */,
int & /* banSecondsLeft */,
bool /* passwordNeedsHash */)
{
return UnknownUser;
}

View file

@ -0,0 +1,52 @@
#ifndef LOCALSERVER_H
#define LOCALSERVER_H
#include "server.h"
#include "server_database_interface.h"
class LocalServerInterface;
class LocalServer : public Server
{
Q_OBJECT
public:
LocalServer(QObject *parent = nullptr);
~LocalServer();
LocalServerInterface *newConnection();
};
class LocalServer_DatabaseInterface : public Server_DatabaseInterface
{
Q_OBJECT
private:
LocalServer *localServer;
protected:
ServerInfo_User getUserData(const QString &name, bool withId = false);
public:
LocalServer_DatabaseInterface(LocalServer *_localServer);
~LocalServer_DatabaseInterface() = default;
AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler,
const QString &user,
const QString &password,
const QString &clientId,
QString &reasonStr,
int &secondsLeft,
bool passwordNeedsHash);
int getNextGameId()
{
return localServer->getNextLocalGameId();
}
int getNextReplayId()
{
return -1;
}
int getActiveUserCount(QString /* connectionType */)
{
return 0;
}
};
#endif

View file

@ -0,0 +1,24 @@
#include "local_server_interface.h"
#include "local_server.h"
#include <QDebug>
LocalServerInterface::LocalServerInterface(LocalServer *_server, Server_DatabaseInterface *_databaseInterface)
: Server_ProtocolHandler(_server, _databaseInterface, _server)
{
}
LocalServerInterface::~LocalServerInterface()
{
}
void LocalServerInterface::transmitProtocolItem(const ServerMessage &item)
{
emit itemToClient(item);
}
void LocalServerInterface::itemFromClient(const CommandContainer &item)
{
processCommandContainer(item);
}

View file

@ -0,0 +1,30 @@
#ifndef LOCALSERVERINTERFACE_H
#define LOCALSERVERINTERFACE_H
#include "server_protocolhandler.h"
class LocalServer;
class LocalServerInterface : public Server_ProtocolHandler
{
Q_OBJECT
public:
LocalServerInterface(LocalServer *_server, Server_DatabaseInterface *_databaseInterface);
~LocalServerInterface();
QString getAddress() const
{
return QString();
}
QString getConnectionType() const
{
return "local";
};
void transmitProtocolItem(const ServerMessage &item);
signals:
void itemToClient(const ServerMessage &item);
public slots:
void itemFromClient(const CommandContainer &item);
};
#endif

View file

@ -0,0 +1,871 @@
#include "message_log_widget.h"
#include "../client/sound_engine.h"
#include "../client/translate_counter_name.h"
#include "../game/cards/card_item.h"
#include "../game/phase.h"
#include "../game/player/player.h"
#include "../game/zones/card_zone.h"
#include "pb/context_move_card.pb.h"
#include "pb/context_mulligan.pb.h"
#include "pb/serverinfo_user.pb.h"
#include <utility>
const QString &MessageLogWidget::tableConstant() const
{
static const QString constant("table");
return constant;
}
const QString &MessageLogWidget::graveyardConstant() const
{
static const QString constant("grave");
return constant;
}
const QString &MessageLogWidget::exileConstant() const
{
static const QString constant("rfg");
return constant;
}
const QString &MessageLogWidget::handConstant() const
{
static const QString constant("hand");
return constant;
}
const QString &MessageLogWidget::deckConstant() const
{
static const QString constant("deck");
return constant;
}
const QString &MessageLogWidget::sideboardConstant() const
{
static const QString constant("sb");
return constant;
}
const QString &MessageLogWidget::stackConstant() const
{
static const QString constant("stack");
return constant;
}
QString MessageLogWidget::sanitizeHtml(QString dirty) const
{
return dirty.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
QString MessageLogWidget::cardLink(const QString cardName) const
{
return QString("<i><a href=\"card://%1\">%2</a></i>").arg(cardName).arg(cardName);
}
QPair<QString, QString>
MessageLogWidget::getFromStr(CardZone *zone, QString cardName, int position, bool ownerChange) const
{
bool cardNameContainsStartZone = false;
QString fromStr;
QString zoneName = zone->getName();
if (zoneName == tableConstant()) {
fromStr = tr(" from play");
} else if (zoneName == graveyardConstant()) {
fromStr = tr(" from their graveyard");
} else if (zoneName == exileConstant()) {
fromStr = tr(" from exile");
} else if (zoneName == handConstant()) {
fromStr = tr(" from their hand");
} else if (zoneName == deckConstant()) {
if (position == 0) {
if (cardName.isEmpty()) {
if (ownerChange) {
cardName = tr("the top card of %1's library").arg(zone->getPlayer()->getName());
} else {
cardName = tr("the top card of their library");
}
cardNameContainsStartZone = true;
} else {
if (ownerChange) {
fromStr = tr(" from the top of %1's library").arg(zone->getPlayer()->getName());
} else {
fromStr = tr(" from the top of their library");
}
}
} else if (position >= zone->getCards().size() - 1) {
if (cardName.isEmpty()) {
if (ownerChange) {
cardName = tr("the bottom card of %1's library").arg(zone->getPlayer()->getName());
} else {
cardName = tr("the bottom card of their library");
}
cardNameContainsStartZone = true;
} else {
if (ownerChange) {
fromStr = tr(" from the bottom of %1's library").arg(zone->getPlayer()->getName());
} else {
fromStr = tr(" from the bottom of their library");
}
}
} else {
if (ownerChange) {
fromStr = tr(" from %1's library").arg(zone->getPlayer()->getName());
} else {
fromStr = tr(" from their library");
}
}
} else if (zoneName == sideboardConstant()) {
fromStr = tr(" from sideboard");
} else if (zoneName == stackConstant()) {
fromStr = tr(" from the stack");
}
if (!cardNameContainsStartZone) {
cardName.clear();
}
return QPair<QString, QString>(cardName, fromStr);
}
void MessageLogWidget::containerProcessingDone()
{
currentContext = MessageContext_None;
messageSuffix = messagePrefix = QString();
}
void MessageLogWidget::containerProcessingStarted(const GameEventContext &context)
{
if (context.HasExtension(Context_MoveCard::ext)) {
currentContext = MessageContext_MoveCard;
} else if (context.HasExtension(Context_Mulligan::ext)) {
currentContext = MessageContext_Mulligan;
}
}
void MessageLogWidget::logAlwaysRevealTopCard(Player *player, CardZone *zone, bool reveal)
{
appendHtmlServerMessage((reveal ? tr("%1 is now keeping the top card %2 revealed.")
: tr("%1 is not revealing the top card %2 any longer."))
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseTopCardsOfZone)));
}
void MessageLogWidget::logAlwaysLookAtTopCard(Player *player, CardZone *zone, bool reveal)
{
appendHtmlServerMessage((reveal ? tr("%1 can now look at top card %2 at any time.")
: tr("%1 no longer can look at top card %2 at any time."))
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseTopCardsOfZone)));
}
void MessageLogWidget::logAttachCard(Player *player, QString cardName, Player *targetPlayer, QString targetCardName)
{
appendHtmlServerMessage(tr("%1 attaches %2 to %3's %4.")
.arg(sanitizeHtml(player->getName()))
.arg(cardLink(std::move(cardName)))
.arg(sanitizeHtml(targetPlayer->getName()))
.arg(cardLink(std::move(targetCardName))));
}
void MessageLogWidget::logConcede(Player *player)
{
soundEngine->playSound("player_concede");
appendHtmlServerMessage(tr("%1 has conceded the game.").arg(sanitizeHtml(player->getName())), true);
}
void MessageLogWidget::logUnconcede(Player *player)
{
soundEngine->playSound("player_concede");
appendHtmlServerMessage(tr("%1 has unconceded the game.").arg(sanitizeHtml(player->getName())), true);
}
void MessageLogWidget::logConnectionStateChanged(Player *player, bool connectionState)
{
if (connectionState) {
soundEngine->playSound("player_reconnect");
appendHtmlServerMessage(tr("%1 has restored connection to the game.").arg(sanitizeHtml(player->getName())),
true);
} else {
soundEngine->playSound("player_disconnect");
appendHtmlServerMessage(tr("%1 has lost connection to the game.").arg(sanitizeHtml(player->getName())), true);
}
}
void MessageLogWidget::logCreateArrow(Player *player,
Player *startPlayer,
QString startCard,
Player *targetPlayer,
QString targetCard,
bool playerTarget)
{
startCard = cardLink(startCard);
targetCard = cardLink(targetCard);
QString str;
if (playerTarget) {
if (player == startPlayer && player == targetPlayer) {
str = tr("%1 points from their %2 to themselves.");
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getName())).arg(startCard));
} else if (player == startPlayer) {
str = tr("%1 points from their %2 to %3.");
appendHtmlServerMessage(
str.arg(sanitizeHtml(player->getName())).arg(startCard).arg(sanitizeHtml(targetPlayer->getName())));
} else if (player == targetPlayer) {
str = tr("%1 points from %2's %3 to themselves.");
appendHtmlServerMessage(
str.arg(sanitizeHtml(player->getName())).arg(sanitizeHtml(startPlayer->getName())).arg(startCard));
} else {
str = tr("%1 points from %2's %3 to %4.");
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getName()))
.arg(sanitizeHtml(startPlayer->getName()))
.arg(startCard)
.arg(sanitizeHtml(targetPlayer->getName())));
}
} else {
if (player == startPlayer && player == targetPlayer) {
str = tr("%1 points from their %2 to their %3.");
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getName())).arg(startCard).arg(targetCard));
} else if (player == startPlayer) {
str = tr("%1 points from their %2 to %3's %4.");
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getName()))
.arg(startCard)
.arg(sanitizeHtml(targetPlayer->getName()))
.arg(targetCard));
} else if (player == targetPlayer) {
str = tr("%1 points from %2's %3 to their own %4.");
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getName()))
.arg(sanitizeHtml(startPlayer->getName()))
.arg(startCard)
.arg(targetCard));
} else {
str = tr("%1 points from %2's %3 to %4's %5.");
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getName()))
.arg(sanitizeHtml(startPlayer->getName()))
.arg(startCard)
.arg(sanitizeHtml(targetPlayer->getName()))
.arg(targetCard));
}
}
}
void MessageLogWidget::logCreateToken(Player *player, QString cardName, QString pt)
{
appendHtmlServerMessage(tr("%1 creates token: %2%3.")
.arg(sanitizeHtml(player->getName()))
.arg(cardLink(std::move(cardName)))
.arg(pt.isEmpty() ? QString() : QString(" (%1)").arg(sanitizeHtml(pt))));
}
void MessageLogWidget::logDeckSelect(Player *player, QString deckHash, int sideboardSize)
{
if (sideboardSize < 0) {
appendHtmlServerMessage(tr("%1 has loaded a deck (%2).").arg(sanitizeHtml(player->getName())).arg(deckHash));
} else {
appendHtmlServerMessage(tr("%1 has loaded a deck with %2 sideboard cards (%3).")
.arg(sanitizeHtml(player->getName()))
.arg("<font class=\"blue\">" + QString::number(sideboardSize) + "</font>")
.arg(deckHash));
}
}
void MessageLogWidget::logDestroyCard(Player *player, QString cardName)
{
appendHtmlServerMessage(
tr("%1 destroys %2.").arg(sanitizeHtml(player->getName())).arg(cardLink(std::move(cardName))));
}
void MessageLogWidget::logMoveCard(Player *player,
CardItem *card,
CardZone *startZone,
int oldX,
CardZone *targetZone,
int newX)
{
if (currentContext == MessageContext_Mulligan) {
return;
}
QString startZoneName = startZone->getName();
QString targetZoneName = targetZone->getName();
bool ownerChanged = startZone->getPlayer() != targetZone->getPlayer();
// do not log if moved within the same zone
if ((startZoneName == tableConstant() && targetZoneName == tableConstant() && !ownerChanged) ||
(startZoneName == handConstant() && targetZoneName == handConstant()) ||
(startZoneName == exileConstant() && targetZoneName == exileConstant())) {
return;
}
QString cardName = card->getName();
QPair<QString, QString> nameFrom = getFromStr(startZone, cardName, oldX, ownerChanged);
if (!nameFrom.first.isEmpty()) {
cardName = nameFrom.first;
}
QString cardStr;
if (!nameFrom.first.isEmpty()) {
cardStr = cardName;
} else if (cardName.isEmpty()) {
cardStr = tr("a card");
} else {
cardStr = cardLink(cardName);
}
if (ownerChanged && (startZone->getPlayer() == player)) {
appendHtmlServerMessage(tr("%1 gives %2 control over %3.")
.arg(sanitizeHtml(player->getName()))
.arg(sanitizeHtml(targetZone->getPlayer()->getName()))
.arg(cardStr));
return;
}
QString finalStr;
bool usesNewX = false;
if (targetZoneName == tableConstant()) {
soundEngine->playSound("play_card");
if (card->getFaceDown()) {
finalStr = tr("%1 puts %2 into play%3 face down.");
} else {
finalStr = tr("%1 puts %2 into play%3.");
}
} else if (targetZoneName == graveyardConstant()) {
finalStr = tr("%1 puts %2%3 into their graveyard.");
} else if (targetZoneName == exileConstant()) {
finalStr = tr("%1 exiles %2%3.");
} else if (targetZoneName == handConstant()) {
finalStr = tr("%1 moves %2%3 to their hand.");
} else if (targetZoneName == deckConstant()) {
if (newX == -1) {
finalStr = tr("%1 puts %2%3 into their library.");
} else if (newX >= targetZone->getCards().size()) {
finalStr = tr("%1 puts %2%3 onto the bottom of their library.");
} else if (newX == 0) {
finalStr = tr("%1 puts %2%3 on top of their library.");
} else {
++newX;
usesNewX = true;
finalStr = tr("%1 puts %2%3 into their library %4 cards from the top.");
}
} else if (targetZoneName == sideboardConstant()) {
finalStr = tr("%1 moves %2%3 to sideboard.");
} else if (targetZoneName == stackConstant()) {
soundEngine->playSound("play_card");
finalStr = tr("%1 plays %2%3.");
}
if (usesNewX) {
appendHtmlServerMessage(
finalStr.arg(sanitizeHtml(player->getName())).arg(cardStr).arg(nameFrom.second).arg(newX));
} else {
appendHtmlServerMessage(finalStr.arg(sanitizeHtml(player->getName())).arg(cardStr).arg(nameFrom.second));
}
}
void MessageLogWidget::logDrawCards(Player *player, int number, bool deckIsEmpty)
{
soundEngine->playSound("draw_card");
if (currentContext == MessageContext_Mulligan) {
logMulligan(player, number);
} else {
if (deckIsEmpty && number == 0) {
appendHtmlServerMessage(tr("%1 tries to draw from an empty library").arg(sanitizeHtml(player->getName())));
} else {
appendHtmlServerMessage(tr("%1 draws %2 card(s).", "", number)
.arg(sanitizeHtml(player->getName()))
.arg("<font class=\"blue\">" + QString::number(number) + "</font>"));
}
}
}
void MessageLogWidget::logDumpZone(Player *player, CardZone *zone, int numberCards)
{
if (numberCards == -1) {
appendHtmlServerMessage(tr("%1 is looking at %2.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(zone->getPlayer() == player, CaseLookAtZone)));
} else {
appendHtmlServerMessage(
tr("%1 is looking at the top %3 card(s) %2.", "top card for singular, top %3 cards for plural", numberCards)
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(zone->getPlayer() == player, CaseTopCardsOfZone))
.arg("<font class=\"blue\">" + QString::number(numberCards) + "</font>"));
}
}
void MessageLogWidget::logFlipCard(Player *player, QString cardName, bool faceDown)
{
if (faceDown) {
appendHtmlServerMessage(
tr("%1 turns %2 face-down.").arg(sanitizeHtml(player->getName())).arg(cardLink(cardName)));
} else {
appendHtmlServerMessage(
tr("%1 turns %2 face-up.").arg(sanitizeHtml(player->getName())).arg(cardLink(cardName)));
}
}
void MessageLogWidget::logGameClosed()
{
appendHtmlServerMessage(tr("The game has been closed."));
}
void MessageLogWidget::logGameStart()
{
appendHtmlServerMessage(tr("The game has started."));
}
void MessageLogWidget::logJoin(Player *player)
{
soundEngine->playSound("player_join");
appendHtmlServerMessage(tr("%1 has joined the game.").arg(sanitizeHtml(player->getName())));
}
void MessageLogWidget::logJoinSpectator(QString name)
{
soundEngine->playSound("spectator_join");
appendHtmlServerMessage(tr("%1 is now watching the game.").arg(sanitizeHtml(std::move(name))));
}
void MessageLogWidget::logKicked()
{
appendHtmlServerMessage(tr("You have been kicked out of the game."), true);
}
void MessageLogWidget::logLeave(Player *player, QString reason)
{
soundEngine->playSound("player_leave");
appendHtmlServerMessage(
tr("%1 has left the game (%2).").arg(sanitizeHtml(player->getName()), sanitizeHtml(std::move(reason))), true);
}
void MessageLogWidget::logLeaveSpectator(QString name, QString reason)
{
soundEngine->playSound("spectator_leave");
appendHtmlServerMessage(tr("%1 is not watching the game any more (%2).")
.arg(sanitizeHtml(std::move(name)), sanitizeHtml(std::move(reason))));
}
void MessageLogWidget::logNotReadyStart(Player *player)
{
appendHtmlServerMessage(tr("%1 is not ready to start the game any more.").arg(sanitizeHtml(player->getName())));
}
void MessageLogWidget::logMulligan(Player *player, int number)
{
if (!player) {
return;
}
if (number > 0) {
appendHtmlServerMessage(tr("%1 shuffles their deck and draws a new hand of %2 card(s).", "", number)
.arg(sanitizeHtml(player->getName()))
.arg(number));
} else {
appendHtmlServerMessage(
tr("%1 shuffles their deck and draws a new hand.").arg(sanitizeHtml(player->getName())));
}
}
void MessageLogWidget::logReplayStarted(int gameId)
{
appendHtmlServerMessage(tr("You are watching a replay of game #%1.").arg(gameId));
}
void MessageLogWidget::logReadyStart(Player *player)
{
appendHtmlServerMessage(tr("%1 is ready to start the game.").arg(sanitizeHtml(player->getName())));
}
void MessageLogWidget::logRevealCards(Player *player,
CardZone *zone,
int cardId,
QString cardName,
Player *otherPlayer,
bool faceDown,
int amount,
bool isLentToAnotherPlayer)
{
// getFromStr uses cardname.empty() to check if it should contain the start zone, it's not actually used
QPair<QString, QString> temp = getFromStr(zone, amount == 1 ? cardName : QString::number(amount), cardId, false);
bool cardNameContainsStartZone = false;
if (!temp.first.isEmpty()) {
cardNameContainsStartZone = true;
cardName = temp.first;
}
QString fromStr = temp.second;
QString cardStr;
if (cardNameContainsStartZone) {
cardStr = cardName;
} else if (cardName.isEmpty()) {
if (amount == 0) {
cardStr = tr("cards", "an unknown amount of cards");
} else {
cardStr = tr("%1 card(s)", "a card for singular, %1 cards for plural", amount)
.arg("<font class=\"blue\">" + QString::number(amount) + "</font>");
}
} else {
cardStr = cardLink(cardName);
}
if (cardId == -1) {
if (otherPlayer) {
if (isLentToAnotherPlayer) {
appendHtmlServerMessage(tr("%1 lends %2 to %3.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseRevealZone))
.arg(sanitizeHtml(otherPlayer->getName())));
} else {
appendHtmlServerMessage(tr("%1 reveals %2 to %3.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseRevealZone))
.arg(sanitizeHtml(otherPlayer->getName())));
}
} else {
appendHtmlServerMessage(tr("%1 reveals %2.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseRevealZone)));
}
} else if (cardId == -2) {
if (otherPlayer) {
appendHtmlServerMessage(tr("%1 randomly reveals %2%3 to %4.")
.arg(sanitizeHtml(player->getName()))
.arg(cardStr)
.arg(fromStr)
.arg(sanitizeHtml(otherPlayer->getName())));
} else {
appendHtmlServerMessage(
tr("%1 randomly reveals %2%3.").arg(sanitizeHtml(player->getName())).arg(cardStr).arg(fromStr));
}
} else {
if (faceDown && player == otherPlayer) {
if (cardName.isEmpty()) {
appendHtmlServerMessage(
tr("%1 peeks at face down card #%2.").arg(sanitizeHtml(player->getName())).arg(cardId));
} else {
appendHtmlServerMessage(tr("%1 peeks at face down card #%2: %3.")
.arg(sanitizeHtml(player->getName()))
.arg(cardId)
.arg(cardStr));
}
} else if (otherPlayer) {
appendHtmlServerMessage(tr("%1 reveals %2%3 to %4.")
.arg(sanitizeHtml(player->getName()))
.arg(cardStr)
.arg(fromStr)
.arg(sanitizeHtml(otherPlayer->getName())));
} else {
appendHtmlServerMessage(
tr("%1 reveals %2%3.").arg(sanitizeHtml(player->getName())).arg(cardStr).arg(fromStr));
}
}
}
void MessageLogWidget::logReverseTurn(Player *player, bool reversed)
{
appendHtmlServerMessage(tr("%1 reversed turn order, now it's %2.")
.arg(sanitizeHtml(player->getName()))
.arg(reversed ? tr("reversed") : tr("normal")));
}
void MessageLogWidget::logRollDie(Player *player, int sides, const QList<uint> &rolls)
{
if (rolls.length() == 1) {
const auto roll = rolls.at(0);
if (sides == 2) {
QString coinOptions[2] = {tr("Heads") + " (1)", tr("Tails") + " (2)"};
appendHtmlServerMessage(tr("%1 flipped a coin. It landed as %2.")
.arg(sanitizeHtml(player->getName()))
.arg("<font class=\"blue\">" + coinOptions[roll - 1] + "</font>"));
} else {
appendHtmlServerMessage(tr("%1 rolls a %2 with a %3-sided die.")
.arg(sanitizeHtml(player->getName()))
.arg("<font class=\"blue\">" + QString::number(roll) + "</font>")
.arg("<font class=\"blue\">" + QString::number(sides) + "</font>"));
}
} else {
if (sides == 2) {
appendHtmlServerMessage(tr("%1 flips %2 coins. There are %3 heads and %4 tails.")
.arg(sanitizeHtml(player->getName()))
.arg("<font class=\"blue\">" + QString::number(rolls.length()) + "</font>")
.arg("<font class=\"blue\">" + QString::number(rolls.count(1)) + "</font>")
.arg("<font class=\"blue\">" + QString::number(rolls.count(2)) + "</font>"));
} else {
QStringList rollsStrings;
for (const auto &roll : rolls) {
rollsStrings.append(QString::number(roll));
}
appendHtmlServerMessage(tr("%1 rolls a %2-sided dice %3 times: %4.")
.arg(sanitizeHtml(player->getName()))
.arg("<font class=\"blue\">" + QString::number(sides) + "</font>")
.arg("<font class=\"blue\">" + QString::number(rolls.length()) + "</font>")
.arg("<font class=\"blue\">" + rollsStrings.join(", ") + "</font>"));
}
}
soundEngine->playSound("roll_dice");
}
void MessageLogWidget::logSay(Player *player, QString message)
{
appendMessage(std::move(message), {}, player->getName(), UserLevelFlags(player->getUserInfo()->user_level()),
QString::fromStdString(player->getUserInfo()->privlevel()), true);
}
void MessageLogWidget::logSetActivePhase(int phaseNumber)
{
Phase phase = Phases::getPhase(phaseNumber);
soundEngine->playSound(phase.soundFileName);
appendHtml("<font color=\"" + phase.color + "\"><b>" + QDateTime::currentDateTime().toString("[hh:mm:ss] ") +
phase.name + "</b></font>");
}
void MessageLogWidget::logSetActivePlayer(Player *player)
{
appendHtml("<br><font color=\"green\"><b>" + QDateTime::currentDateTime().toString("[hh:mm:ss] ") +
QString(tr("%1's turn.")).arg(player->getName()) + "</b></font><br>");
}
void MessageLogWidget::logSetAnnotation(Player *player, CardItem *card, QString newAnnotation)
{
appendHtmlServerMessage(
QString(tr("%1 sets annotation of %2 to %3."))
.arg(sanitizeHtml(player->getName()))
.arg(cardLink(card->getName()))
.arg(QString("&quot;<font class=\"blue\">%1</font>&quot;").arg(sanitizeHtml(std::move(newAnnotation)))));
}
void MessageLogWidget::logSetCardCounter(Player *player, QString cardName, int counterId, int value, int oldValue)
{
QString finalStr;
int delta = abs(oldValue - value);
if (value > oldValue) {
finalStr = tr("%1 places %2 %3 on %4 (now %5).");
} else {
finalStr = tr("%1 removes %2 %3 from %4 (now %5).");
}
QString colorStr;
switch (counterId) {
case 0:
colorStr = tr("red counter(s)", "", delta);
break;
case 1:
colorStr = tr("yellow counter(s)", "", delta);
break;
case 2:
colorStr = tr("green counter(s)", "", delta);
break;
default:;
}
appendHtmlServerMessage(finalStr.arg(sanitizeHtml(player->getName()))
.arg("<font class=\"blue\">" + QString::number(delta) + "</font>")
.arg(colorStr)
.arg(cardLink(std::move(cardName)))
.arg(value));
}
void MessageLogWidget::logSetCounter(Player *player, QString counterName, int value, int oldValue)
{
if (counterName == "life") {
soundEngine->playSound("life_change");
}
QString counterDisplayName = TranslateCounterName::getDisplayName(counterName);
appendHtmlServerMessage(tr("%1 sets counter %2 to %3 (%4%5).")
.arg(sanitizeHtml(player->getName()))
.arg(QString("<font class=\"blue\">%1</font>").arg(sanitizeHtml(counterDisplayName)))
.arg(QString("<font class=\"blue\">%1</font>").arg(value))
.arg(value > oldValue ? "+" : "")
.arg(value - oldValue));
}
void MessageLogWidget::logSetDoesntUntap(Player *player, CardItem *card, bool doesntUntap)
{
QString str;
if (doesntUntap) {
str = tr("%1 sets %2 to not untap normally.");
} else {
str = tr("%1 sets %2 to untap normally.");
}
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getName())).arg(cardLink(card->getName())));
}
void MessageLogWidget::logSetPT(Player *player, CardItem *card, QString newPT)
{
if (currentContext == MessageContext_MoveCard) {
return;
}
QString name = card->getName();
if (name.isEmpty()) {
name = QString("<font class=\"blue\">card #%1</font>").arg(sanitizeHtml(QString::number(card->getId())));
} else {
name = cardLink(name);
}
QString playerName = sanitizeHtml(player->getName());
if (newPT.isEmpty()) {
appendHtmlServerMessage(tr("%1 removes the PT of %2.").arg(playerName).arg(name));
} else {
QString oldPT = card->getPT();
if (oldPT.isEmpty()) {
appendHtmlServerMessage(
tr("%1 changes the PT of %2 from nothing to %4.").arg(playerName).arg(name).arg(newPT));
} else {
appendHtmlServerMessage(
tr("%1 changes the PT of %2 from %3 to %4.").arg(playerName).arg(name).arg(oldPT).arg(newPT));
}
}
}
void MessageLogWidget::logSetSideboardLock(Player *player, bool locked)
{
if (locked) {
appendHtmlServerMessage(tr("%1 has locked their sideboard.").arg(sanitizeHtml(player->getName())));
} else {
appendHtmlServerMessage(tr("%1 has unlocked their sideboard.").arg(sanitizeHtml(player->getName())));
}
}
void MessageLogWidget::logSetTapped(Player *player, CardItem *card, bool tapped)
{
if (currentContext == MessageContext_MoveCard) {
return;
}
if (tapped) {
soundEngine->playSound("tap_card");
} else {
soundEngine->playSound("untap_card");
}
QString str;
if (!card) {
appendHtmlServerMessage((tapped ? tr("%1 taps their permanents.") : tr("%1 untaps their permanents."))
.arg(sanitizeHtml(player->getName())));
} else {
appendHtmlServerMessage((tapped ? tr("%1 taps %2.") : tr("%1 untaps %2."))
.arg(sanitizeHtml(player->getName()))
.arg(cardLink(card->getName())));
}
}
void MessageLogWidget::logShuffle(Player *player, CardZone *zone, int start, int end)
{
if (currentContext == MessageContext_Mulligan) {
return;
}
soundEngine->playSound("shuffle");
// start and end are indexes into the portion of the deck that was shuffled
// with negitive numbers counging from the bottom up.
if (start == 0 && end == -1) {
appendHtmlServerMessage(tr("%1 shuffles %2.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseShuffleZone)));
} else if (start < 0 && end == -1) {
appendHtmlServerMessage(tr("%1 shuffles the bottom %3 cards of %2.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseShuffleZone))
.arg(-start));
} else if (start < 0 && end > 0) {
appendHtmlServerMessage(tr("%1 shuffles the top %3 cards of %2.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseShuffleZone))
.arg(end + 1));
} else {
appendHtmlServerMessage(tr("%1 shuffles cards %3 - %4 of %2.")
.arg(sanitizeHtml(player->getName()))
.arg(zone->getTranslatedName(true, CaseShuffleZone))
.arg(start)
.arg(end));
}
}
void MessageLogWidget::logSpectatorSay(QString spectatorName,
UserLevelFlags spectatorUserLevel,
QString userPrivLevel,
QString message)
{
appendMessage(std::move(message), {}, spectatorName, spectatorUserLevel, userPrivLevel, false);
}
void MessageLogWidget::logUnattachCard(Player *player, QString cardName)
{
appendHtmlServerMessage(
tr("%1 unattaches %2.").arg(sanitizeHtml(player->getName())).arg(cardLink(std::move(cardName))));
}
void MessageLogWidget::logUndoDraw(Player *player, QString cardName)
{
if (cardName.isEmpty()) {
appendHtmlServerMessage(tr("%1 undoes their last draw.").arg(sanitizeHtml(player->getName())));
} else {
appendHtmlServerMessage(
tr("%1 undoes their last draw (%2).")
.arg(sanitizeHtml(player->getName()))
.arg(QString("<a href=\"card://%1\">%2</a>").arg(sanitizeHtml(cardName)).arg(sanitizeHtml(cardName))));
}
}
void MessageLogWidget::setContextJudgeName(QString name)
{
messagePrefix = QString("<span style=\"color:black\">");
messageSuffix = QString("</span> [<img height=12 src=\"theme:icons/scales\"> %1]").arg(sanitizeHtml(name));
}
void MessageLogWidget::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor)
{
ChatView::appendHtmlServerMessage(messagePrefix + html + messageSuffix, optionalIsBold, optionalFontColor);
}
void MessageLogWidget::connectToPlayer(Player *player)
{
connect(player, SIGNAL(logSay(Player *, QString)), this, SLOT(logSay(Player *, QString)));
connect(player, &Player::logShuffle, this, &MessageLogWidget::logShuffle);
connect(player, SIGNAL(logRollDie(Player *, int, const QList<uint> &)), this,
SLOT(logRollDie(Player *, int, const QList<uint> &)));
connect(player, SIGNAL(logCreateArrow(Player *, Player *, QString, Player *, QString, bool)), this,
SLOT(logCreateArrow(Player *, Player *, QString, Player *, QString, bool)));
connect(player, SIGNAL(logCreateToken(Player *, QString, QString)), this,
SLOT(logCreateToken(Player *, QString, QString)));
connect(player, SIGNAL(logSetCounter(Player *, QString, int, int)), this,
SLOT(logSetCounter(Player *, QString, int, int)));
connect(player, SIGNAL(logSetCardCounter(Player *, QString, int, int, int)), this,
SLOT(logSetCardCounter(Player *, QString, int, int, int)));
connect(player, SIGNAL(logSetTapped(Player *, CardItem *, bool)), this,
SLOT(logSetTapped(Player *, CardItem *, bool)));
connect(player, SIGNAL(logSetDoesntUntap(Player *, CardItem *, bool)), this,
SLOT(logSetDoesntUntap(Player *, CardItem *, bool)));
connect(player, SIGNAL(logSetPT(Player *, CardItem *, QString)), this,
SLOT(logSetPT(Player *, CardItem *, QString)));
connect(player, SIGNAL(logSetAnnotation(Player *, CardItem *, QString)), this,
SLOT(logSetAnnotation(Player *, CardItem *, QString)));
connect(player, SIGNAL(logMoveCard(Player *, CardItem *, CardZone *, int, CardZone *, int)), this,
SLOT(logMoveCard(Player *, CardItem *, CardZone *, int, CardZone *, int)));
connect(player, SIGNAL(logFlipCard(Player *, QString, bool)), this, SLOT(logFlipCard(Player *, QString, bool)));
connect(player, SIGNAL(logDestroyCard(Player *, QString)), this, SLOT(logDestroyCard(Player *, QString)));
connect(player, SIGNAL(logAttachCard(Player *, QString, Player *, QString)), this,
SLOT(logAttachCard(Player *, QString, Player *, QString)));
connect(player, SIGNAL(logUnattachCard(Player *, QString)), this, SLOT(logUnattachCard(Player *, QString)));
connect(player, SIGNAL(logDumpZone(Player *, CardZone *, int)), this, SLOT(logDumpZone(Player *, CardZone *, int)));
connect(player, SIGNAL(logDrawCards(Player *, int, bool)), this, SLOT(logDrawCards(Player *, int, bool)));
connect(player, SIGNAL(logUndoDraw(Player *, QString)), this, SLOT(logUndoDraw(Player *, QString)));
connect(player, SIGNAL(logRevealCards(Player *, CardZone *, int, QString, Player *, bool, int, bool)), this,
SLOT(logRevealCards(Player *, CardZone *, int, QString, Player *, bool, int, bool)));
connect(player, SIGNAL(logAlwaysRevealTopCard(Player *, CardZone *, bool)), this,
SLOT(logAlwaysRevealTopCard(Player *, CardZone *, bool)));
connect(player, SIGNAL(logAlwaysLookAtTopCard(Player *, CardZone *, bool)), this,
SLOT(logAlwaysLookAtTopCard(Player *, CardZone *, bool)));
}
MessageLogWidget::MessageLogWidget(TabSupervisor *_tabSupervisor,
const UserlistProxy *_userlistProxy,
TabGame *_game,
QWidget *parent)
: ChatView(_tabSupervisor, _userlistProxy, _game, true, parent), mulliganNumber(0),
currentContext(MessageContext_None)
{
}

View file

@ -0,0 +1,112 @@
#ifndef MESSAGELOGWIDGET_H
#define MESSAGELOGWIDGET_H
#include "../client/translation.h"
#include "chat_view/chat_view.h"
#include "user_level.h"
class Player;
class CardZone;
class GameEventContext;
class CardItem;
class MessageLogWidget : public ChatView
{
Q_OBJECT
private:
enum MessageContext
{
MessageContext_None,
MessageContext_MoveCard,
MessageContext_Mulligan
};
int mulliganNumber;
Player *mulliganPlayer;
MessageContext currentContext;
QString messagePrefix, messageSuffix;
const QString &tableConstant() const;
const QString &graveyardConstant() const;
const QString &exileConstant() const;
const QString &handConstant() const;
const QString &deckConstant() const;
const QString &sideboardConstant() const;
const QString &stackConstant() const;
QString sanitizeHtml(QString dirty) const;
QString cardLink(QString cardName) const;
QPair<QString, QString> getFromStr(CardZone *zone, QString cardName, int position, bool ownerChange) const;
public slots:
void containerProcessingDone();
void containerProcessingStarted(const GameEventContext &context);
void logAlwaysRevealTopCard(Player *player, CardZone *zone, bool reveal);
void logAlwaysLookAtTopCard(Player *player, CardZone *zone, bool reveal);
void logAttachCard(Player *player, QString cardName, Player *targetPlayer, QString targetCardName);
void logConcede(Player *player);
void logUnconcede(Player *player);
void logConnectionStateChanged(Player *player, bool connectionState);
void logCreateArrow(Player *player,
Player *startPlayer,
QString startCard,
Player *targetPlayer,
QString targetCard,
bool playerTarget);
void logCreateToken(Player *player, QString cardName, QString pt);
void logDeckSelect(Player *player, QString deckHash, int sideboardSize);
void logDestroyCard(Player *player, QString cardName);
void logDrawCards(Player *player, int number, bool deckIsEmpty);
void logDumpZone(Player *player, CardZone *zone, int numberCards);
void logFlipCard(Player *player, QString cardName, bool faceDown);
void logGameClosed();
void logGameStart();
void logJoin(Player *player);
void logJoinSpectator(QString name);
void logKicked();
void logLeave(Player *player, QString reason);
void logLeaveSpectator(QString name, QString reason);
void logNotReadyStart(Player *player);
void logMoveCard(Player *player, CardItem *card, CardZone *startZone, int oldX, CardZone *targetZone, int newX);
void logMulligan(Player *player, int number);
void logReplayStarted(int gameId);
void logReadyStart(Player *player);
void logRevealCards(Player *player,
CardZone *zone,
int cardId,
QString cardName,
Player *otherPlayer,
bool faceDown,
int amount,
bool isLentToAnotherPlayer);
void logReverseTurn(Player *player, bool reversed);
void logRollDie(Player *player, int sides, const QList<uint> &rolls);
void logSay(Player *player, QString message);
void logSetActivePhase(int phase);
void logSetActivePlayer(Player *player);
void logSetAnnotation(Player *player, CardItem *card, QString newAnnotation);
void logSetCardCounter(Player *player, QString cardName, int counterId, int value, int oldValue);
void logSetCounter(Player *player, QString counterName, int value, int oldValue);
void logSetDoesntUntap(Player *player, CardItem *card, bool doesntUntap);
void logSetPT(Player *player, CardItem *card, QString newPT);
void logSetSideboardLock(Player *player, bool locked);
void logSetTapped(Player *player, CardItem *card, bool tapped);
void logShuffle(Player *player, CardZone *zone, int start, int end);
void
logSpectatorSay(QString spectatorName, UserLevelFlags spectatorUserLevel, QString userPrivLevel, QString message);
void logUnattachCard(Player *player, QString cardName);
void logUndoDraw(Player *player, QString cardName);
void setContextJudgeName(QString player);
void appendHtmlServerMessage(const QString &html,
bool optionalIsBold = false,
QString optionalFontColor = QString()) override;
public:
void connectToPlayer(Player *player);
MessageLogWidget(TabSupervisor *_tabSupervisor,
const UserlistProxy *_userlistProxy,
TabGame *_game,
QWidget *parent = nullptr);
};
#endif

View file

@ -0,0 +1,32 @@
#include "pending_command.h"
PendingCommand::PendingCommand(const CommandContainer &_commandContainer, QVariant _extraData)
: commandContainer(_commandContainer), extraData(_extraData), ticks(0)
{
}
CommandContainer &PendingCommand::getCommandContainer()
{
return commandContainer;
}
void PendingCommand::setExtraData(const QVariant &_extraData)
{
extraData = _extraData;
}
QVariant PendingCommand::getExtraData() const
{
return extraData;
}
void PendingCommand::processResponse(const Response &response)
{
emit finished(response, commandContainer, extraData);
emit finished(response.response_code());
}
int PendingCommand::tick()
{
return ++ticks;
}

View file

@ -0,0 +1,30 @@
#ifndef PENDING_COMMAND_H
#define PENDING_COMMAND_H
#include "pb/commands.pb.h"
#include "pb/response.pb.h"
#include <QVariant>
class PendingCommand : public QObject
{
Q_OBJECT
signals:
void finished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
void finished(Response::ResponseCode respCode);
private:
CommandContainer commandContainer;
QVariant extraData;
int ticks;
public:
PendingCommand(const CommandContainer &_commandContainer, QVariant _extraData = QVariant());
CommandContainer &getCommandContainer();
void setExtraData(const QVariant &_extraData);
QVariant getExtraData() const;
void processResponse(const Response &response);
int tick();
};
#endif

View file

@ -0,0 +1,729 @@
#include "remote_client.h"
#include "../../main.h"
#include "../../settings/cache_settings.h"
#include "../pending_command.h"
#include "debug_pb_message.h"
#include "passwordhasher.h"
#include "pb/event_server_identification.pb.h"
#include "pb/response_activate.pb.h"
#include "pb/response_forgotpasswordrequest.pb.h"
#include "pb/response_login.pb.h"
#include "pb/response_password_salt.pb.h"
#include "pb/response_register.pb.h"
#include "pb/server_message.pb.h"
#include "pb/session_commands.pb.h"
#include "version_string.h"
#include <QCryptographicHash>
#include <QDebug>
#include <QHostAddress>
#include <QHostInfo>
#include <QList>
#include <QThread>
#include <QTimer>
#include <QWebSocket>
static const unsigned int protocolVersion = 14;
RemoteClient::RemoteClient(QObject *parent)
: AbstractClient(parent), timeRunning(0), lastDataReceived(0), messageInProgress(false), handshakeStarted(false),
usingWebSocket(false), messageLength(0), hashedPassword()
{
clearNewClientFeatures();
maxTimeout = SettingsCache::instance().getTimeOut();
int keepalive = SettingsCache::instance().getKeepAlive();
timer = new QTimer(this);
timer->setInterval(keepalive * 1000);
connect(timer, SIGNAL(timeout()), this, SLOT(ping()));
socket = new QTcpSocket(this);
socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
connect(socket, SIGNAL(connected()), this, SLOT(slotConnected()));
connect(socket, SIGNAL(readyRead()), this, SLOT(readData()));
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
connect(socket, &QTcpSocket::errorOccurred, this, &RemoteClient::slotSocketError);
#else
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this,
SLOT(slotSocketError(QAbstractSocket::SocketError)));
#endif
websocket = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this);
connect(websocket, &QWebSocket::binaryMessageReceived, this, &RemoteClient::websocketMessageReceived);
connect(websocket, &QWebSocket::connected, this, &RemoteClient::slotConnected);
connect(websocket, SIGNAL(error(QAbstractSocket::SocketError)), this,
SLOT(slotWebSocketError(QAbstractSocket::SocketError)));
connect(this, SIGNAL(serverIdentificationEventReceived(const Event_ServerIdentification &)), this,
SLOT(processServerIdentificationEvent(const Event_ServerIdentification &)));
connect(this, SIGNAL(connectionClosedEventReceived(Event_ConnectionClosed)), this,
SLOT(processConnectionClosedEvent(Event_ConnectionClosed)));
connect(this, SIGNAL(sigConnectToServer(QString, unsigned int, QString, QString)), this,
SLOT(doConnectToServer(QString, unsigned int, QString, QString)));
connect(this, SIGNAL(sigDisconnectFromServer()), this, SLOT(doDisconnectFromServer()));
connect(this, SIGNAL(sigRegisterToServer(QString, unsigned int, QString, QString, QString, QString, QString)), this,
SLOT(doRegisterToServer(QString, unsigned int, QString, QString, QString, QString, QString)));
connect(this, SIGNAL(sigActivateToServer(QString)), this, SLOT(doActivateToServer(QString)));
connect(this, SIGNAL(sigRequestForgotPasswordToServer(QString, unsigned int, QString)), this,
SLOT(doRequestForgotPasswordToServer(QString, unsigned int, QString)));
connect(this, SIGNAL(sigSubmitForgotPasswordResetToServer(QString, unsigned int, QString, QString, QString)), this,
SLOT(doSubmitForgotPasswordResetToServer(QString, unsigned int, QString, QString, QString)));
connect(this, SIGNAL(sigSubmitForgotPasswordChallengeToServer(QString, unsigned int, QString, QString)), this,
SLOT(doSubmitForgotPasswordChallengeToServer(QString, unsigned int, QString, QString)));
}
RemoteClient::~RemoteClient()
{
doDisconnectFromServer();
thread()->quit();
}
void RemoteClient::slotSocketError(QAbstractSocket::SocketError /*error*/)
{
QString errorString = socket->errorString();
doDisconnectFromServer();
emit socketError(errorString);
}
void RemoteClient::slotWebSocketError(QAbstractSocket::SocketError /*error*/)
{
QString errorString = websocket->errorString();
if (getStatus() != ClientStatus::StatusDisconnected) {
doDisconnectFromServer();
emit socketError(errorString);
}
}
void RemoteClient::slotConnected()
{
timeRunning = lastDataReceived = 0;
timer->start();
if (!usingWebSocket) {
// dirty hack to be compatible with v14 server
sendCommandContainer(CommandContainer());
getNewCmdId();
// end of hack
}
}
void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentification &event)
{
if (event.protocol_version() != protocolVersion) {
emit protocolVersionMismatch(protocolVersion, event.protocol_version());
setStatus(StatusDisconnecting);
return;
}
serverSupportsPasswordHash = event.server_options() & Event_ServerIdentification::SupportsPasswordHash;
if (getStatus() == StatusRequestingForgotPassword) {
Command_ForgotPasswordRequest cmdForgotPasswordRequest;
cmdForgotPasswordRequest.set_user_name(userName.toStdString());
cmdForgotPasswordRequest.set_clientid(getSrvClientID(lastHostname).toStdString());
PendingCommand *pend = prepareSessionCommand(cmdForgotPasswordRequest);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(requestForgotPasswordResponse(Response)));
sendCommand(pend);
return;
}
if (getStatus() == StatusSubmitForgotPasswordReset) {
Command_ForgotPasswordReset cmdForgotPasswordReset;
cmdForgotPasswordReset.set_user_name(userName.toStdString());
cmdForgotPasswordReset.set_clientid(getSrvClientID(lastHostname).toStdString());
cmdForgotPasswordReset.set_token(token.toStdString());
if (!password.isEmpty() && serverSupportsPasswordHash) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdForgotPasswordReset.set_hashed_new_password(hashedPassword.toStdString());
} else if (!password.isEmpty()) {
qWarning() << "using plain text password to reset password";
cmdForgotPasswordReset.set_new_password(password.toStdString());
}
PendingCommand *pend = prepareSessionCommand(cmdForgotPasswordReset);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(submitForgotPasswordResetResponse(Response)));
sendCommand(pend);
return;
}
if (getStatus() == StatusSubmitForgotPasswordChallenge) {
Command_ForgotPasswordChallenge cmdForgotPasswordChallenge;
cmdForgotPasswordChallenge.set_user_name(userName.toStdString());
cmdForgotPasswordChallenge.set_clientid(getSrvClientID(lastHostname).toStdString());
cmdForgotPasswordChallenge.set_email(email.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdForgotPasswordChallenge);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(submitForgotPasswordChallengeResponse(Response)));
sendCommand(pend);
return;
}
if (getStatus() == StatusRegistering) {
Command_Register cmdRegister;
cmdRegister.set_user_name(userName.toStdString());
if (!password.isEmpty() && serverSupportsPasswordHash) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdRegister.set_hashed_password(hashedPassword.toStdString());
} else if (!password.isEmpty()) {
qWarning() << "using plain text password to register";
cmdRegister.set_password(password.toStdString());
}
cmdRegister.set_email(email.toStdString());
cmdRegister.set_country(country.toStdString());
cmdRegister.set_real_name(realName.toStdString());
cmdRegister.set_clientid(getSrvClientID(lastHostname).toStdString());
PendingCommand *pend = prepareSessionCommand(cmdRegister);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(registerResponse(Response)));
sendCommand(pend);
return;
}
if (getStatus() == StatusActivating) {
Command_Activate cmdActivate;
cmdActivate.set_user_name(userName.toStdString());
cmdActivate.set_token(token.toStdString());
cmdActivate.set_clientid(getSrvClientID(lastHostname).toStdString());
PendingCommand *pend = prepareSessionCommand(cmdActivate);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(activateResponse(Response)));
sendCommand(pend);
return;
}
doLogin();
}
void RemoteClient::doRequestPasswordSalt()
{
setStatus(StatusGettingPasswordSalt);
Command_RequestPasswordSalt cmdRqSalt;
cmdRqSalt.set_user_name(userName.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdRqSalt);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(passwordSaltResponse(Response)));
sendCommand(pend);
}
Command_Login RemoteClient::generateCommandLogin()
{
Command_Login cmdLogin;
cmdLogin.set_user_name(userName.toStdString());
cmdLogin.set_clientid(getSrvClientID(lastHostname).toStdString());
cmdLogin.set_clientver(VERSION_STRING);
if (!clientFeatures.isEmpty()) {
QMap<QString, bool>::iterator i;
for (i = clientFeatures.begin(); i != clientFeatures.end(); ++i)
cmdLogin.add_clientfeatures(i.key().toStdString().c_str());
}
return cmdLogin;
}
void RemoteClient::doLogin()
{
if (!password.isEmpty() && serverSupportsPasswordHash) {
// TODO store and log in using stored hashed password
if (hashedPassword.isEmpty()) {
doRequestPasswordSalt(); // ask salt to create hashedPassword, then log in
} else {
doHashedLogin(); // log in using hashed password instead
}
} else {
// TODO add setting for client to reject unhashed logins
setStatus(StatusLoggingIn);
Command_Login cmdLogin = generateCommandLogin();
if (!password.isEmpty()) {
qWarning() << "using plain text password to log in";
cmdLogin.set_password(password.toStdString());
}
PendingCommand *pend = prepareSessionCommand(cmdLogin);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(loginResponse(Response)));
sendCommand(pend);
}
}
void RemoteClient::doHashedLogin()
{
setStatus(StatusLoggingIn);
Command_Login cmdLogin = generateCommandLogin();
cmdLogin.set_hashed_password(hashedPassword.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdLogin);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(loginResponse(Response)));
sendCommand(pend);
}
void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/)
{
doDisconnectFromServer();
}
void RemoteClient::passwordSaltResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
const Response_PasswordSalt &resp = response.GetExtension(Response_PasswordSalt::ext);
auto passwordSalt = QString::fromStdString(resp.password_salt());
if (passwordSalt.isEmpty()) { // the server does not recognize the user but allows them to enter unregistered
password.clear(); // the password will not be used
doLogin();
} else {
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
doHashedLogin();
}
} else if (response.response_code() != Response::RespNotConnected) {
emit loginError(response.response_code(), {}, 0, {});
}
}
void RemoteClient::loginResponse(const Response &response)
{
const Response_Login &resp = response.GetExtension(Response_Login::ext);
QString possibleMissingFeatures;
if (resp.missing_features_size() > 0) {
for (int i = 0; i < resp.missing_features_size(); ++i)
possibleMissingFeatures.append("," + QString::fromStdString(resp.missing_features(i)));
}
if (response.response_code() == Response::RespOk) {
setStatus(StatusLoggedIn);
emit userInfoChanged(resp.user_info());
QList<ServerInfo_User> buddyList;
for (int i = resp.buddy_list_size() - 1; i >= 0; --i)
buddyList.append(resp.buddy_list(i));
emit buddyListReceived(buddyList);
QList<ServerInfo_User> ignoreList;
for (int i = resp.ignore_list_size() - 1; i >= 0; --i)
ignoreList.append(resp.ignore_list(i));
emit ignoreListReceived(ignoreList);
if (newMissingFeatureFound(possibleMissingFeatures) && resp.missing_features_size() > 0 &&
SettingsCache::instance().getNotifyAboutUpdates()) {
SettingsCache::instance().setKnownMissingFeatures(possibleMissingFeatures);
emit notifyUserAboutUpdate();
}
} else if (response.response_code() != Response::RespNotConnected) {
QList<QString> missingFeatures;
if (resp.missing_features_size() > 0) {
for (int i = 0; i < resp.missing_features_size(); ++i)
missingFeatures << QString::fromStdString(resp.missing_features(i));
}
emit loginError(response.response_code(), QString::fromStdString(resp.denied_reason_str()),
static_cast<quint32>(resp.denied_end_time()), missingFeatures);
setStatus(StatusDisconnecting);
}
}
void RemoteClient::registerResponse(const Response &response)
{
const Response_Register &resp = response.GetExtension(Response_Register::ext);
switch (response.response_code()) {
case Response::RespRegistrationAccepted:
emit registerAccepted();
doLogin();
break;
case Response::RespRegistrationAcceptedNeedsActivation:
emit registerAcceptedNeedsActivate();
doLogin();
break;
default:
emit registerError(response.response_code(), QString::fromStdString(resp.denied_reason_str()),
static_cast<quint32>(resp.denied_end_time()));
setStatus(StatusDisconnecting);
doDisconnectFromServer();
break;
}
}
void RemoteClient::activateResponse(const Response &response)
{
if (response.response_code() == Response::RespActivationAccepted) {
emit activateAccepted();
doLogin();
} else {
emit activateError();
}
}
void RemoteClient::readData()
{
lastDataReceived = timeRunning;
QByteArray data = socket->readAll();
inputBuffer.append(data);
do {
if (!messageInProgress) {
if (inputBuffer.size() >= 4) {
// dirty hack to be compatible with v14 server that sends 60 bytes of garbage at the beginning
if (!handshakeStarted) {
handshakeStarted = true;
if (inputBuffer.startsWith("<?xm")) {
messageInProgress = true;
messageLength = 60;
}
} else {
// end of hack
messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) +
(((quint32)(unsigned char)inputBuffer[1]) << 16) +
(((quint32)(unsigned char)inputBuffer[2]) << 8) +
((quint32)(unsigned char)inputBuffer[3]);
inputBuffer.remove(0, 4);
messageInProgress = true;
}
} else
return;
}
if (inputBuffer.size() < messageLength)
return;
ServerMessage newServerMessage;
newServerMessage.ParseFromArray(inputBuffer.data(), messageLength);
#ifdef QT_DEBUG
qDebug().noquote() << "IN" << getSafeDebugString(newServerMessage);
#endif
inputBuffer.remove(0, messageLength);
messageInProgress = false;
processProtocolItem(newServerMessage);
if (getStatus() == StatusDisconnecting) // use thread-safe getter
doDisconnectFromServer();
} while (!inputBuffer.isEmpty());
}
void RemoteClient::websocketMessageReceived(const QByteArray &message)
{
lastDataReceived = timeRunning;
ServerMessage newServerMessage;
newServerMessage.ParseFromArray(message.data(), message.length());
#ifdef QT_DEBUG
qDebug().noquote() << "IN" << getSafeDebugString(newServerMessage);
#endif
processProtocolItem(newServerMessage);
}
void RemoteClient::sendCommandContainer(const CommandContainer &cont)
{
#if GOOGLE_PROTOBUF_VERSION > 3001000
auto size = static_cast<unsigned int>(cont.ByteSizeLong());
#else
auto size = static_cast<unsigned int>(cont.ByteSize());
#endif
#ifdef QT_DEBUG
qDebug().noquote() << "OUT" << getSafeDebugString(cont);
#endif
QByteArray buf;
if (usingWebSocket) {
buf.resize(size);
cont.SerializeToArray(buf.data(), size);
websocket->sendBinaryMessage(buf);
} else {
buf.resize(size + 4);
cont.SerializeToArray(buf.data() + 4, size);
buf.data()[3] = (unsigned char)size;
buf.data()[2] = (unsigned char)(size >> 8);
buf.data()[1] = (unsigned char)(size >> 16);
buf.data()[0] = (unsigned char)(size >> 24);
socket->write(buf);
}
}
void RemoteClient::connectToHost(const QString &hostname, unsigned int port)
{
usingWebSocket = port == 443 || port == 80 || port == 4748 || port == 8080;
if (usingWebSocket) {
QUrl url(QString("%1://%2:%3/servatrice").arg(port == 443 ? "wss" : "ws").arg(hostname).arg(port));
websocket->open(url);
} else {
socket->connectToHost(hostname, static_cast<quint16>(port));
}
}
void RemoteClient::doConnectToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_password)
{
doDisconnectFromServer();
userName = _userName;
password = _password;
lastHostname = hostname;
lastPort = port;
hashedPassword.clear();
connectToHost(hostname, port);
setStatus(StatusConnecting);
}
void RemoteClient::doRegisterToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_password,
const QString &_email,
const QString &_country,
const QString &_realname)
{
doDisconnectFromServer();
userName = _userName;
password = _password;
email = _email;
country = _country;
realName = _realname;
lastHostname = hostname;
lastPort = port;
hashedPassword.clear();
connectToHost(hostname, port);
setStatus(StatusRegistering);
}
void RemoteClient::doActivateToServer(const QString &_token)
{
doDisconnectFromServer();
token = _token.trimmed();
connectToHost(lastHostname, lastPort);
setStatus(StatusActivating);
}
void RemoteClient::doDisconnectFromServer()
{
timer->stop();
messageInProgress = false;
handshakeStarted = false;
messageLength = 0;
QList<PendingCommand *> pc = pendingCommands.values();
for (const auto &i : pc) {
Response response;
response.set_response_code(Response::RespNotConnected);
response.set_cmd_id(i->getCommandContainer().cmd_id());
i->processResponse(response);
delete i;
}
pendingCommands.clear();
setStatus(StatusDisconnected);
if (websocket->isValid())
websocket->close();
socket->close();
}
void RemoteClient::ping()
{
QMutableMapIterator<int, PendingCommand *> i(pendingCommands);
while (i.hasNext()) {
PendingCommand *pend = i.next().value();
if (pend->tick() > maxTimeout) {
i.remove();
pend->deleteLater();
}
}
int maxTime = timeRunning - lastDataReceived;
emit maxPingTime(maxTime, maxTimeout);
if (maxTime >= maxTimeout) {
disconnectFromServer();
emit serverTimeout();
} else {
sendCommand(prepareSessionCommand(Command_Ping()));
++timeRunning;
}
}
void RemoteClient::connectToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_password)
{
emit sigConnectToServer(hostname, port, _userName, _password);
}
void RemoteClient::registerToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_password,
const QString &_email,
const QString &_country,
const QString &_realname)
{
emit sigRegisterToServer(hostname, port, _userName, _password, _email, _country, _realname);
}
void RemoteClient::activateToServer(const QString &_token)
{
emit sigActivateToServer(_token.trimmed());
}
void RemoteClient::disconnectFromServer()
{
SettingsCache::instance().servers().setAutoConnect(false);
emit sigDisconnectFromServer();
}
QString RemoteClient::getSrvClientID(const QString &_hostname)
{
QString srvClientID = SettingsCache::instance().getClientID();
QHostInfo hostInfo = QHostInfo::fromName(_hostname);
if (!hostInfo.error()) {
QHostAddress hostAddress = hostInfo.addresses().first();
srvClientID += hostAddress.toString();
} else {
qWarning() << "ClientID generation host lookup failure [" << hostInfo.errorString() << "]";
srvClientID += _hostname;
}
QString uniqueServerClientID =
QCryptographicHash::hash(srvClientID.toUtf8(), QCryptographicHash::Sha1).toHex().right(15);
return uniqueServerClientID;
}
bool RemoteClient::newMissingFeatureFound(const QString &_serversMissingFeatures)
{
bool newMissingFeature = false;
QStringList serversMissingFeaturesList = _serversMissingFeatures.split(",");
for (const QString &feature : serversMissingFeaturesList) {
if (!feature.isEmpty()) {
if (!SettingsCache::instance().getKnownMissingFeatures().contains(feature))
return true;
}
}
return newMissingFeature;
}
void RemoteClient::clearNewClientFeatures()
{
QString newKnownMissingFeatures;
QStringList existingKnownMissingFeatures = SettingsCache::instance().getKnownMissingFeatures().split(",");
for (const QString &existingKnownFeature : existingKnownMissingFeatures) {
if (!existingKnownFeature.isEmpty()) {
if (!clientFeatures.contains(existingKnownFeature))
newKnownMissingFeatures.append("," + existingKnownFeature);
}
}
SettingsCache::instance().setKnownMissingFeatures(newKnownMissingFeatures);
}
void RemoteClient::requestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName)
{
emit sigRequestForgotPasswordToServer(hostname, port, _userName);
}
void RemoteClient::submitForgotPasswordResetToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_token,
const QString &_newpassword)
{
emit sigSubmitForgotPasswordResetToServer(hostname, port, _userName, _token.trimmed(), _newpassword);
}
void RemoteClient::doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName)
{
doDisconnectFromServer();
userName = _userName;
lastHostname = hostname;
lastPort = port;
connectToHost(lastHostname, lastPort);
setStatus(StatusRequestingForgotPassword);
}
void RemoteClient::requestForgotPasswordResponse(const Response &response)
{
const Response_ForgotPasswordRequest &resp = response.GetExtension(Response_ForgotPasswordRequest::ext);
if (response.response_code() == Response::RespOk) {
if (resp.challenge_email()) {
emit sigPromptForForgotPasswordChallenge();
} else
emit sigPromptForForgotPasswordReset();
} else
emit sigForgotPasswordError();
doDisconnectFromServer();
}
void RemoteClient::doSubmitForgotPasswordResetToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_token,
const QString &_newpassword)
{
doDisconnectFromServer();
userName = _userName;
lastHostname = hostname;
lastPort = port;
token = _token.trimmed();
password = _newpassword;
hashedPassword.clear();
connectToHost(lastHostname, lastPort);
setStatus(StatusSubmitForgotPasswordReset);
}
void RemoteClient::submitForgotPasswordResetResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
emit sigForgotPasswordSuccess();
} else
emit sigForgotPasswordError();
doDisconnectFromServer();
}
void RemoteClient::submitForgotPasswordChallengeToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_email)
{
emit sigSubmitForgotPasswordChallengeToServer(hostname, port, _userName, _email);
}
void RemoteClient::doSubmitForgotPasswordChallengeToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_email)
{
doDisconnectFromServer();
userName = _userName;
lastHostname = hostname;
lastPort = port;
email = _email;
connectToHost(lastHostname, lastPort);
setStatus(StatusSubmitForgotPasswordChallenge);
}
void RemoteClient::submitForgotPasswordChallengeResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
emit sigPromptForForgotPasswordReset();
} else
emit sigForgotPasswordError();
doDisconnectFromServer();
}

View file

@ -0,0 +1,148 @@
#ifndef REMOTECLIENT_H
#define REMOTECLIENT_H
#include "../../client/game_logic/abstract_client.h"
#include "pb/commands.pb.h"
#include <QTcpSocket>
#include <QWebSocket>
class QTimer;
class RemoteClient : public AbstractClient
{
Q_OBJECT
signals:
void maxPingTime(int seconds, int maxSeconds);
void serverTimeout();
void loginError(Response::ResponseCode resp, QString reasonStr, quint32 endTime, QList<QString> missingFeatures);
void registerError(Response::ResponseCode resp, QString reasonStr, quint32 endTime);
void activateError();
void socketError(const QString &errorString);
void protocolVersionMismatch(int clientVersion, int serverVersion);
void
sigConnectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password);
void sigRegisterToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_password,
const QString &_email,
const QString &_country,
const QString &_realname);
void sigActivateToServer(const QString &_token);
void sigDisconnectFromServer();
void notifyUserAboutUpdate();
void sigRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
void sigForgotPasswordSuccess();
void sigForgotPasswordError();
void sigPromptForForgotPasswordReset();
void sigSubmitForgotPasswordResetToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_token,
const QString &_newpassword);
void sigPromptForForgotPasswordChallenge();
void sigSubmitForgotPasswordChallengeToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_email);
private slots:
void slotConnected();
void readData();
void websocketMessageReceived(const QByteArray &message);
void slotSocketError(QAbstractSocket::SocketError error);
void slotWebSocketError(QAbstractSocket::SocketError error);
void ping();
void processServerIdentificationEvent(const Event_ServerIdentification &event);
void processConnectionClosedEvent(const Event_ConnectionClosed &event);
void passwordSaltResponse(const Response &response);
void loginResponse(const Response &response);
void registerResponse(const Response &response);
void activateResponse(const Response &response);
void
doConnectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password);
void doRegisterToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_password,
const QString &_email,
const QString &_country,
const QString &_realname);
void doRequestPasswordSalt();
void doLogin();
void doHashedLogin();
Command_Login generateCommandLogin();
void doDisconnectFromServer();
void doActivateToServer(const QString &_token);
void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
void requestForgotPasswordResponse(const Response &response);
void doSubmitForgotPasswordResetToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_token,
const QString &_newpassword);
void submitForgotPasswordResetResponse(const Response &response);
void doSubmitForgotPasswordChallengeToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_email);
void submitForgotPasswordChallengeResponse(const Response &response);
private:
int maxTimeout;
int timeRunning, lastDataReceived;
QByteArray inputBuffer;
bool messageInProgress;
bool handshakeStarted;
bool usingWebSocket;
int messageLength;
QTimer *timer;
QTcpSocket *socket;
QWebSocket *websocket;
QString lastHostname;
unsigned int lastPort;
QString hashedPassword;
QString getSrvClientID(const QString &_hostname);
bool newMissingFeatureFound(const QString &_serversMissingFeatures);
void clearNewClientFeatures();
void connectToHost(const QString &hostname, unsigned int port);
protected slots:
void sendCommandContainer(const CommandContainer &cont) override;
public:
explicit RemoteClient(QObject *parent = nullptr);
~RemoteClient() override;
QString peerName() const
{
if (usingWebSocket) {
return websocket->peerName();
} else {
return socket->peerName();
}
}
void
connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password);
void registerToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_password,
const QString &_email,
const QString &_country,
const QString &_realname);
void activateToServer(const QString &_token);
void disconnectFromServer();
void requestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
void submitForgotPasswordResetToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_token,
const QString &_newpassword);
void submitForgotPasswordChallengeToServer(const QString &hostname,
unsigned int port,
const QString &_userName,
const QString &_email);
};
#endif

View file

@ -0,0 +1,353 @@
#include "remote_decklist_tree_widget.h"
#include "../../client/game_logic/abstract_client.h"
#include "../pending_command.h"
#include "pb/command_deck_list.pb.h"
#include "pb/response_deck_list.pb.h"
#include "pb/serverinfo_deckstorage.pb.h"
#include <QFileIconProvider>
#include <QHeaderView>
#include <QSortFilterProxyModel>
RemoteDeckList_TreeModel::DirectoryNode::DirectoryNode(const QString &_name,
RemoteDeckList_TreeModel::DirectoryNode *_parent)
: RemoteDeckList_TreeModel::Node(_name, _parent)
{
}
RemoteDeckList_TreeModel::DirectoryNode::~DirectoryNode()
{
clearTree();
}
void RemoteDeckList_TreeModel::DirectoryNode::clearTree()
{
for (int i = 0; i < size(); ++i)
delete at(i);
clear();
}
QString RemoteDeckList_TreeModel::DirectoryNode::getPath() const
{
if (parent) {
QString parentPath = parent->getPath();
if (parentPath.isEmpty())
return name;
else
return parentPath + "/" + name;
} else
return name;
}
RemoteDeckList_TreeModel::DirectoryNode *RemoteDeckList_TreeModel::DirectoryNode::getNodeByPath(QStringList path)
{
QString pathItem;
if (parent) {
if (path.isEmpty())
return this;
pathItem = path.takeFirst();
if (pathItem.isEmpty() && name.isEmpty())
return this;
}
for (int i = 0; i < size(); ++i) {
DirectoryNode *node = dynamic_cast<DirectoryNode *>(at(i));
if (!node)
continue;
if (node->getName() == pathItem)
return node->getNodeByPath(path);
}
return 0;
}
RemoteDeckList_TreeModel::FileNode *RemoteDeckList_TreeModel::DirectoryNode::getNodeById(int id) const
{
for (int i = 0; i < size(); ++i) {
DirectoryNode *node = dynamic_cast<DirectoryNode *>(at(i));
if (node) {
FileNode *result = node->getNodeById(id);
if (result)
return result;
} else {
FileNode *file = dynamic_cast<FileNode *>(at(i));
if (file->getId() == id)
return file;
}
}
return 0;
}
RemoteDeckList_TreeModel::RemoteDeckList_TreeModel(AbstractClient *_client, QObject *parent)
: QAbstractItemModel(parent), client(_client)
{
QFileIconProvider fip;
dirIcon = fip.icon(QFileIconProvider::Folder);
fileIcon = fip.icon(QFileIconProvider::File);
root = new DirectoryNode;
refreshTree();
}
RemoteDeckList_TreeModel::~RemoteDeckList_TreeModel()
{
delete root;
}
int RemoteDeckList_TreeModel::rowCount(const QModelIndex &parent) const
{
DirectoryNode *node = getNode<DirectoryNode *>(parent);
if (node)
return node->size();
else
return 0;
}
int RemoteDeckList_TreeModel::columnCount(const QModelIndex & /*parent*/) const
{
return 3;
}
QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.column() >= 3)
return QVariant();
Node *temp = static_cast<Node *>(index.internalPointer());
FileNode *file = dynamic_cast<FileNode *>(temp);
if (!file) {
DirectoryNode *node = dynamic_cast<DirectoryNode *>(temp);
switch (role) {
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return node->getName();
default:
return QVariant();
}
}
case Qt::DecorationRole:
return index.column() == 0 ? dirIcon : QVariant();
default:
return QVariant();
}
} else {
switch (role) {
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return file->getName();
case 1:
return file->getId();
case 2:
return file->getUploadTime();
default:
return QVariant();
}
}
case Qt::DecorationRole:
return index.column() == 0 ? fileIcon : QVariant();
case Qt::TextAlignmentRole:
return index.column() == 1 ? Qt::AlignRight : Qt::AlignLeft;
default:
return QVariant();
}
}
}
QVariant RemoteDeckList_TreeModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal)
return QVariant();
switch (role) {
case Qt::TextAlignmentRole:
return section == 1 ? Qt::AlignRight : Qt::AlignLeft;
case Qt::DisplayRole: {
switch (section) {
case 0:
return tr("Name");
case 1:
return tr("ID");
case 2:
return tr("Upload time");
default:
return QVariant();
}
}
default:
return QVariant();
}
}
QModelIndex RemoteDeckList_TreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
DirectoryNode *parentNode = getNode<DirectoryNode *>(parent);
if (row >= parentNode->size())
return QModelIndex();
return createIndex(row, column, parentNode->at(row));
}
QModelIndex RemoteDeckList_TreeModel::parent(const QModelIndex &ind) const
{
if (!ind.isValid())
return QModelIndex();
return nodeToIndex(static_cast<Node *>(ind.internalPointer())->getParent());
}
Qt::ItemFlags RemoteDeckList_TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QModelIndex RemoteDeckList_TreeModel::nodeToIndex(Node *node) const
{
if (node == nullptr || node == root)
return QModelIndex();
return createIndex(node->getParent()->indexOf(node), 0, node);
}
void RemoteDeckList_TreeModel::addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent)
{
const ServerInfo_DeckStorage_File &fileInfo = file.file();
QDateTime time;
time.setSecsSinceEpoch(fileInfo.creation_time());
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent));
endInsertRows();
}
void RemoteDeckList_TreeModel::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent)
{
DirectoryNode *newItem = addNamedFolderToTree(QString::fromStdString(folder.name()), parent);
const ServerInfo_DeckStorage_Folder &folderInfo = folder.folder();
const int folderItemsSize = folderInfo.items_size();
for (int i = 0; i < folderItemsSize; ++i) {
const ServerInfo_DeckStorage_TreeItem &subItem = folderInfo.items(i);
if (subItem.has_folder())
addFolderToTree(subItem, newItem);
else
addFileToTree(subItem, newItem);
}
}
RemoteDeckList_TreeModel::DirectoryNode *RemoteDeckList_TreeModel::addNamedFolderToTree(const QString &name,
DirectoryNode *parent)
{
DirectoryNode *newItem = new DirectoryNode(name, parent);
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
parent->append(newItem);
endInsertRows();
return newItem;
}
void RemoteDeckList_TreeModel::removeNode(RemoteDeckList_TreeModel::Node *node)
{
int ind = node->getParent()->indexOf(node);
beginRemoveRows(nodeToIndex(node->getParent()), ind, ind);
node->getParent()->removeAt(ind);
endRemoveRows();
delete node;
}
void RemoteDeckList_TreeModel::refreshTree()
{
PendingCommand *pend = client->prepareSessionCommand(Command_DeckList());
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(deckListFinished(const Response &)));
client->sendCommand(pend);
}
void RemoteDeckList_TreeModel::deckListFinished(const Response &r)
{
const Response_DeckList &resp = r.GetExtension(Response_DeckList::ext);
beginResetModel();
root->clearTree();
endResetModel();
ServerInfo_DeckStorage_TreeItem tempRoot;
tempRoot.set_id(0);
tempRoot.mutable_folder()->CopyFrom(resp.root());
addFolderToTree(tempRoot, root);
emit treeRefreshed();
}
RemoteDeckList_TreeWidget::RemoteDeckList_TreeWidget(AbstractClient *_client, QWidget *parent) : QTreeView(parent)
{
treeModel = new RemoteDeckList_TreeModel(_client, this);
proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(treeModel);
proxyModel->setDynamicSortFilter(true);
proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
setModel(proxyModel);
connect(treeModel, SIGNAL(treeRefreshed()), this, SLOT(expandAll()));
header()->setSectionResizeMode(QHeaderView::ResizeToContents);
setUniformRowHeights(true);
setSortingEnabled(true);
proxyModel->sort(0, Qt::AscendingOrder);
header()->setSortIndicator(0, Qt::AscendingOrder);
}
RemoteDeckList_TreeModel::Node *RemoteDeckList_TreeWidget::getNode(const QModelIndex &ind) const
{
return treeModel->getNode<RemoteDeckList_TreeModel::Node *>(proxyModel->mapToSource(ind));
}
RemoteDeckList_TreeModel::Node *RemoteDeckList_TreeWidget::getCurrentItem() const
{
return getNode(selectionModel()->currentIndex());
}
RemoteDeckList_TreeModel::DirectoryNode *RemoteDeckList_TreeWidget::getNodeByPath(const QString &path) const
{
return treeModel->getRoot()->getNodeByPath(path.split("/"));
}
RemoteDeckList_TreeModel::FileNode *RemoteDeckList_TreeWidget::getNodeById(int id) const
{
return treeModel->getRoot()->getNodeById(id);
}
void RemoteDeckList_TreeWidget::addFileToTree(const ServerInfo_DeckStorage_TreeItem &file,
RemoteDeckList_TreeModel::DirectoryNode *parent)
{
treeModel->addFileToTree(file, parent);
}
void RemoteDeckList_TreeWidget::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder,
RemoteDeckList_TreeModel::DirectoryNode *parent)
{
treeModel->addFolderToTree(folder, parent);
}
void RemoteDeckList_TreeWidget::addFolderToTree(const QString &name, RemoteDeckList_TreeModel::DirectoryNode *parent)
{
treeModel->addNamedFolderToTree(name, parent);
}
void RemoteDeckList_TreeWidget::removeNode(RemoteDeckList_TreeModel::Node *node)
{
treeModel->removeNode(node);
}
void RemoteDeckList_TreeWidget::refreshTree()
{
treeModel->refreshTree();
}

View file

@ -0,0 +1,131 @@
#ifndef REMOTEDECKLIST_TREEWIDGET_H
#define REMOTEDECKLIST_TREEWIDGET_H
#include <QAbstractItemModel>
#include <QDateTime>
#include <QTreeView>
class Response;
class AbstractClient;
class QSortFilterProxyModel;
class ServerInfo_DeckStorage_TreeItem;
class RemoteDeckList_TreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
class DirectoryNode;
class FileNode;
class Node
{
protected:
DirectoryNode *parent;
QString name;
public:
Node(const QString &_name, DirectoryNode *_parent = nullptr) : parent(_parent), name(_name)
{
}
virtual ~Node(){};
DirectoryNode *getParent() const
{
return parent;
}
QString getName() const
{
return name;
}
};
class DirectoryNode : public Node, public QList<Node *>
{
public:
DirectoryNode(const QString &_name = QString(), DirectoryNode *_parent = nullptr);
~DirectoryNode();
void clearTree();
QString getPath() const;
DirectoryNode *getNodeByPath(QStringList path);
FileNode *getNodeById(int id) const;
};
class FileNode : public Node
{
private:
int id;
QDateTime uploadTime;
public:
FileNode(const QString &_name, int _id, const QDateTime &_uploadTime, DirectoryNode *_parent = nullptr)
: Node(_name, _parent), id(_id), uploadTime(_uploadTime)
{
}
int getId() const
{
return id;
}
QDateTime getUploadTime() const
{
return uploadTime;
}
};
template <typename T> T getNode(const QModelIndex &index) const
{
if (!index.isValid())
return dynamic_cast<T>(root);
return dynamic_cast<T>(static_cast<Node *>(index.internalPointer()));
}
private:
AbstractClient *client;
DirectoryNode *root;
QIcon fileIcon, dirIcon;
QModelIndex nodeToIndex(Node *node) const;
signals:
void treeRefreshed();
private slots:
void deckListFinished(const Response &r);
public:
RemoteDeckList_TreeModel(AbstractClient *_client, QObject *parent = nullptr);
~RemoteDeckList_TreeModel();
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
QModelIndex parent(const QModelIndex &index) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
DirectoryNode *getRoot() const
{
return root;
}
void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent);
void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent);
DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent);
void removeNode(Node *node);
void refreshTree();
};
class RemoteDeckList_TreeWidget : public QTreeView
{
private:
RemoteDeckList_TreeModel *treeModel;
QSortFilterProxyModel *proxyModel;
public:
RemoteDeckList_TreeWidget(AbstractClient *_client, QWidget *parent = nullptr);
RemoteDeckList_TreeModel::Node *getNode(const QModelIndex &ind) const;
RemoteDeckList_TreeModel::Node *getCurrentItem() const;
RemoteDeckList_TreeModel::DirectoryNode *getNodeByPath(const QString &path) const;
RemoteDeckList_TreeModel::FileNode *getNodeById(int id) const;
void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, RemoteDeckList_TreeModel::DirectoryNode *parent);
void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder,
RemoteDeckList_TreeModel::DirectoryNode *parent);
void addFolderToTree(const QString &name, RemoteDeckList_TreeModel::DirectoryNode *parent);
void removeNode(RemoteDeckList_TreeModel::Node *node);
void refreshTree();
};
#endif

View file

@ -0,0 +1,319 @@
#include "remote_replay_list_tree_widget.h"
#include "../../client/game_logic/abstract_client.h"
#include "../pending_command.h"
#include "pb/command_replay_list.pb.h"
#include "pb/response_replay_list.pb.h"
#include "pb/serverinfo_replay.pb.h"
#include <QFileIconProvider>
#include <QHeaderView>
#include <QSortFilterProxyModel>
const int RemoteReplayList_TreeModel::numberOfColumns = 6;
RemoteReplayList_TreeModel::MatchNode::MatchNode(const ServerInfo_ReplayMatch &_matchInfo)
: RemoteReplayList_TreeModel::Node(QString::fromStdString(_matchInfo.game_name())), matchInfo(_matchInfo)
{
for (int i = 0; i < matchInfo.replay_list_size(); ++i)
append(new ReplayNode(matchInfo.replay_list(i), this));
}
RemoteReplayList_TreeModel::MatchNode::~MatchNode()
{
for (int i = 0; i < size(); ++i)
delete at(i);
}
void RemoteReplayList_TreeModel::MatchNode::updateMatchInfo(const ServerInfo_ReplayMatch &_matchInfo)
{
matchInfo.MergeFrom(_matchInfo);
}
RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client, QObject *parent)
: QAbstractItemModel(parent), client(_client)
{
QFileIconProvider fip;
dirIcon = fip.icon(QFileIconProvider::Folder);
fileIcon = fip.icon(QFileIconProvider::File);
lockIcon = QPixmap("theme:icons/lock");
refreshTree();
}
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()
{
clearTree();
}
int RemoteReplayList_TreeModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid())
return replayMatches.size();
MatchNode *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(parent.internalPointer()));
if (matchNode)
return matchNode->size();
else
return 0;
}
QVariant RemoteReplayList_TreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.column() >= numberOfColumns)
return QVariant();
ReplayNode *replayNode = dynamic_cast<ReplayNode *>(static_cast<Node *>(index.internalPointer()));
if (replayNode) {
const ServerInfo_Replay &replayInfo = replayNode->getReplayInfo();
switch (role) {
case Qt::TextAlignmentRole:
return index.column() == 0 ? Qt::AlignRight : Qt::AlignLeft;
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return replayInfo.replay_id();
case 1:
return QString::fromStdString(replayInfo.replay_name());
case 5:
return replayInfo.duration();
default:
return QVariant();
}
}
case Qt::DecorationRole:
return index.column() == 0 ? fileIcon : QVariant();
}
} else {
MatchNode *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(index.internalPointer()));
const ServerInfo_ReplayMatch &matchInfo = matchNode->getMatchInfo();
switch (role) {
case Qt::TextAlignmentRole:
switch (index.column()) {
case 0:
case 5:
return Qt::AlignRight;
default:
return Qt::AlignLeft;
}
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return matchInfo.game_id();
case 1:
return QString::fromStdString(matchInfo.game_name());
case 2: {
QStringList playerList;
for (int i = 0; i < matchInfo.player_names_size(); ++i)
playerList.append(QString::fromStdString(matchInfo.player_names(i)));
return playerList.join(", ");
}
case 4:
return QDateTime::fromSecsSinceEpoch(matchInfo.time_started());
case 5:
return matchInfo.length();
default:
return QVariant();
}
}
case Qt::DecorationRole:
switch (index.column()) {
case 0:
return dirIcon;
case 3:
return matchInfo.do_not_hide() ? lockIcon : QVariant();
default:
return QVariant();
}
}
}
return QVariant();
}
QVariant RemoteReplayList_TreeModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal)
return QVariant();
switch (role) {
case Qt::TextAlignmentRole:
switch (section) {
case 0:
case 5:
return Qt::AlignRight;
default:
return Qt::AlignLeft;
}
case Qt::DisplayRole: {
switch (section) {
case 0:
return tr("ID");
case 1:
return tr("Name");
case 2:
return tr("Players");
case 3:
return tr("Keep");
case 4:
return tr("Time started");
case 5:
return tr("Duration (sec)");
default:
return QVariant();
}
}
default:
return QVariant();
}
}
QModelIndex RemoteReplayList_TreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
MatchNode *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(parent.internalPointer()));
if (matchNode) {
if (row >= matchNode->size())
return QModelIndex();
return createIndex(row, column, (void *)matchNode->at(row));
} else {
if (row >= replayMatches.size())
return QModelIndex();
return createIndex(row, column, (void *)replayMatches[row]);
}
}
QModelIndex RemoteReplayList_TreeModel::parent(const QModelIndex &ind) const
{
MatchNode const *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(ind.internalPointer()));
if (matchNode)
return QModelIndex();
else {
ReplayNode *replayNode = dynamic_cast<ReplayNode *>(static_cast<Node *>(ind.internalPointer()));
return createIndex(replayNode->getParent()->indexOf(replayNode), 0, replayNode);
}
}
Qt::ItemFlags RemoteReplayList_TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
ServerInfo_Replay const *RemoteReplayList_TreeModel::getReplay(const QModelIndex &index) const
{
if (!index.isValid())
return 0;
ReplayNode *node = dynamic_cast<ReplayNode *>(static_cast<Node *>(index.internalPointer()));
if (!node)
return 0;
return &node->getReplayInfo();
}
ServerInfo_ReplayMatch const *RemoteReplayList_TreeModel::getReplayMatch(const QModelIndex &index) const
{
if (!index.isValid())
return nullptr;
auto *node = dynamic_cast<MatchNode *>(static_cast<Node *>(index.internalPointer()));
if (!node) {
auto *_node = dynamic_cast<ReplayNode *>(static_cast<Node *>(index.internalPointer()));
if (!_node)
return nullptr;
return &_node->getParent()->getMatchInfo();
}
return &node->getMatchInfo();
}
void RemoteReplayList_TreeModel::clearTree()
{
for (int i = 0; i < replayMatches.size(); ++i)
delete replayMatches[i];
replayMatches.clear();
}
void RemoteReplayList_TreeModel::refreshTree()
{
PendingCommand *pend = client->prepareSessionCommand(Command_ReplayList());
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(replayListFinished(const Response &)));
client->sendCommand(pend);
}
void RemoteReplayList_TreeModel::addMatchInfo(const ServerInfo_ReplayMatch &matchInfo)
{
beginInsertRows(QModelIndex(), replayMatches.size(), replayMatches.size());
replayMatches.append(new MatchNode(matchInfo));
endInsertRows();
emit treeRefreshed();
}
void RemoteReplayList_TreeModel::updateMatchInfo(int gameId, const ServerInfo_ReplayMatch &matchInfo)
{
for (int i = 0; i < replayMatches.size(); ++i)
if (replayMatches[i]->getMatchInfo().game_id() == gameId) {
replayMatches[i]->updateMatchInfo(matchInfo);
emit dataChanged(createIndex(i, 0, (void *)replayMatches[i]),
createIndex(i, numberOfColumns - 1, (void *)replayMatches[i]));
break;
}
}
void RemoteReplayList_TreeModel::removeMatchInfo(int gameId)
{
for (int i = 0; i < replayMatches.size(); ++i)
if (replayMatches[i]->getMatchInfo().game_id() == gameId) {
beginRemoveRows(QModelIndex(), i, i);
replayMatches.removeAt(i);
endRemoveRows();
break;
}
}
void RemoteReplayList_TreeModel::replayListFinished(const Response &r)
{
const Response_ReplayList &resp = r.GetExtension(Response_ReplayList::ext);
beginResetModel();
clearTree();
for (int i = 0; i < resp.match_list_size(); ++i)
replayMatches.append(new MatchNode(resp.match_list(i)));
endResetModel();
emit treeRefreshed();
}
RemoteReplayList_TreeWidget::RemoteReplayList_TreeWidget(AbstractClient *_client, QWidget *parent) : QTreeView(parent)
{
treeModel = new RemoteReplayList_TreeModel(_client, this);
proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(treeModel);
proxyModel->setDynamicSortFilter(true);
proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
setModel(proxyModel);
header()->setSectionResizeMode(QHeaderView::ResizeToContents);
header()->setStretchLastSection(false);
setUniformRowHeights(true);
setSortingEnabled(true);
proxyModel->sort(0, Qt::AscendingOrder);
header()->setSortIndicator(0, Qt::AscendingOrder);
}
ServerInfo_Replay const *RemoteReplayList_TreeWidget::getCurrentReplay() const
{
return treeModel->getReplay(proxyModel->mapToSource(selectionModel()->currentIndex()));
}
ServerInfo_ReplayMatch const *RemoteReplayList_TreeWidget::getCurrentReplayMatch() const
{
return treeModel->getReplayMatch(proxyModel->mapToSource(selectionModel()->currentIndex()));
}

View file

@ -0,0 +1,131 @@
#ifndef REMOTEREPLAYLIST_TREEWIDGET_H
#define REMOTEREPLAYLIST_TREEWIDGET_H
#include "pb/serverinfo_replay.pb.h"
#include "pb/serverinfo_replay_match.pb.h"
#include <QAbstractItemModel>
#include <QDateTime>
#include <QTreeView>
class Response;
class AbstractClient;
class QSortFilterProxyModel;
class RemoteReplayList_TreeModel : public QAbstractItemModel
{
Q_OBJECT
private:
class MatchNode;
class ReplayNode;
class Node
{
protected:
QString name;
public:
Node(const QString &_name) : name(_name)
{
}
virtual ~Node(){};
QString getName() const
{
return name;
}
};
class MatchNode : public Node, public QList<ReplayNode *>
{
private:
ServerInfo_ReplayMatch matchInfo;
public:
MatchNode(const ServerInfo_ReplayMatch &_matchInfo);
~MatchNode();
void clearTree();
const ServerInfo_ReplayMatch &getMatchInfo()
{
return matchInfo;
}
void updateMatchInfo(const ServerInfo_ReplayMatch &_matchInfo);
};
class ReplayNode : public Node
{
private:
MatchNode *parent;
ServerInfo_Replay replayInfo;
public:
ReplayNode(const ServerInfo_Replay &_replayInfo, MatchNode *_parent)
: Node(QString::fromStdString(_replayInfo.replay_name())), parent(_parent), replayInfo(_replayInfo)
{
}
MatchNode *getParent() const
{
return parent;
}
const ServerInfo_Replay &getReplayInfo()
{
return replayInfo;
}
};
AbstractClient *client;
QList<MatchNode *> replayMatches;
QIcon dirIcon, fileIcon, lockIcon;
void clearTree();
static const int numberOfColumns;
signals:
void treeRefreshed();
private slots:
void replayListFinished(const Response &r);
public:
RemoteReplayList_TreeModel(AbstractClient *_client, QObject *parent = nullptr);
~RemoteReplayList_TreeModel();
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const
{
return numberOfColumns;
}
QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
QModelIndex parent(const QModelIndex &index) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
void refreshTree();
ServerInfo_Replay const *getReplay(const QModelIndex &index) const;
ServerInfo_ReplayMatch const *getReplayMatch(const QModelIndex &index) const;
void addMatchInfo(const ServerInfo_ReplayMatch &matchInfo);
void updateMatchInfo(int gameId, const ServerInfo_ReplayMatch &matchInfo);
void removeMatchInfo(int gameId);
};
class RemoteReplayList_TreeWidget : public QTreeView
{
private:
RemoteReplayList_TreeModel *treeModel;
QSortFilterProxyModel *proxyModel;
ServerInfo_Replay const *getNode(const QModelIndex &ind) const;
public:
RemoteReplayList_TreeWidget(AbstractClient *_client, QWidget *parent = nullptr);
ServerInfo_Replay const *getCurrentReplay() const;
ServerInfo_ReplayMatch const *getCurrentReplayMatch() const;
void refreshTree();
void addMatchInfo(const ServerInfo_ReplayMatch &matchInfo)
{
treeModel->addMatchInfo(matchInfo);
}
void updateMatchInfo(int gameId, const ServerInfo_ReplayMatch &matchInfo)
{
treeModel->updateMatchInfo(gameId, matchInfo);
}
void removeMatchInfo(int gameId)
{
treeModel->removeMatchInfo(gameId);
}
};
#endif

View file

@ -0,0 +1,489 @@
#include "user_context_menu.h"
#include "../../client/game_logic/abstract_client.h"
#include "../../client/tabs/tab_account.h"
#include "../../client/tabs/tab_game.h"
#include "../../client/tabs/tab_supervisor.h"
#include "../../game/game_selector.h"
#include "../chat_view/chat_view.h"
#include "../pending_command.h"
#include "pb/command_kick_from_game.pb.h"
#include "pb/commands.pb.h"
#include "pb/moderator_commands.pb.h"
#include "pb/response_ban_history.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"
#include "pb/response_warn_list.pb.h"
#include "pb/session_commands.pb.h"
#include "user_info_box.h"
#include "user_list.h"
#include <QAction>
#include <QMenu>
#include <QMessageBox>
#include <QSignalMapper>
#include <QtGui>
#include <QtWidgets>
UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, TabGame *_game)
: QObject(parent), client(_tabSupervisor->getClient()), tabSupervisor(_tabSupervisor), game(_game)
{
aUserName = new QAction(QString(), this);
aUserName->setEnabled(false);
aDetails = new QAction(QString(), this);
aChat = new QAction(QString(), this);
aShowGames = new QAction(QString(), this);
aAddToBuddyList = new QAction(QString(), this);
aRemoveFromBuddyList = new QAction(QString(), this);
aAddToIgnoreList = new QAction(QString(), this);
aRemoveFromIgnoreList = new QAction(QString(), this);
aKick = new QAction(QString(), this);
aWarnUser = new QAction(QString(), this);
aWarnHistory = new QAction(QString(), this);
aBan = new QAction(QString(), this);
aBanHistory = new QAction(QString(), this);
aPromoteToMod = new QAction(QString(), this);
aDemoteFromMod = new QAction(QString(), this);
aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this);
retranslateUi();
}
void UserContextMenu::retranslateUi()
{
aDetails->setText(tr("User &details"));
aChat->setText(tr("Private &chat"));
aShowGames->setText(tr("Show this user's &games"));
aAddToBuddyList->setText(tr("Add to &buddy list"));
aRemoveFromBuddyList->setText(tr("Remove from &buddy list"));
aAddToIgnoreList->setText(tr("Add to &ignore list"));
aRemoveFromIgnoreList->setText(tr("Remove from &ignore list"));
aKick->setText(tr("Kick from &game"));
aWarnUser->setText(tr("Warn user"));
aWarnHistory->setText(tr("View user's war&n history"));
aBan->setText(tr("Ban from &server"));
aBanHistory->setText(tr("View user's &ban history"));
aPromoteToMod->setText(tr("&Promote user to moderator"));
aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
aPromoteToJudge->setText(tr("Promote user to &judge"));
aDemoteFromJudge->setText(tr("Demote user from judge"));
}
void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer)
{
const Response_GetGamesOfUser &response = resp.GetExtension(Response_GetGamesOfUser::ext);
const Command_GetGamesOfUser &cmd = commandContainer.session_command(0).GetExtension(Command_GetGamesOfUser::ext);
QMap<int, GameTypeMap> gameTypeMap;
QMap<int, QString> roomMap;
const int roomListSize = response.room_list_size();
for (int i = 0; i < roomListSize; ++i) {
const ServerInfo_Room &roomInfo = response.room_list(i);
roomMap.insert(roomInfo.room_id(), QString::fromStdString(roomInfo.name()));
GameTypeMap tempMap;
const int gameTypeListSize = roomInfo.gametype_list_size();
for (int j = 0; j < gameTypeListSize; ++j) {
const ServerInfo_GameType &gameTypeInfo = roomInfo.gametype_list(j);
tempMap.insert(gameTypeInfo.game_type_id(), QString::fromStdString(gameTypeInfo.description()));
}
gameTypeMap.insert(roomInfo.room_id(), tempMap);
}
GameSelector *selector = new GameSelector(client, tabSupervisor, nullptr, roomMap, gameTypeMap, false, false);
selector->setParent(static_cast<QWidget *>(parent()), Qt::Window);
const int gameListSize = response.game_list_size();
for (int i = 0; i < gameListSize; ++i) {
selector->processGameInfo(response.game_list(i));
}
selector->setWindowTitle(tr("%1's games").arg(QString::fromStdString(cmd.user_name())));
selector->setMinimumWidth(800);
selector->setAttribute(Qt::WA_DeleteOnClose);
selector->show();
}
void UserContextMenu::banUser_processUserInfoResponse(const Response &r)
{
const Response_GetUserInfo &response = r.GetExtension(Response_GetUserInfo::ext);
// The dialog needs to be non-modal in order to not block the event queue of the client.
BanDialog *dlg = new BanDialog(response.user_info(), static_cast<QWidget *>(parent()));
connect(dlg, SIGNAL(accepted()), this, SLOT(banUser_dialogFinished()));
dlg->show();
}
void UserContextMenu::warnUser_processGetWarningsListResponse(const Response &r)
{
const Response_WarnList &response = r.GetExtension(Response_WarnList::ext);
QString user = QString::fromStdString(response.user_name()).simplified();
QString clientid = QString::fromStdString(response.user_clientid()).simplified();
// The dialog needs to be non-modal in order to not block the event queue of the client.
WarningDialog *dlg = new WarningDialog(user, clientid, static_cast<QWidget *>(parent()));
connect(dlg, SIGNAL(accepted()), this, SLOT(warnUser_dialogFinished()));
if (response.warning_size() > 0) {
for (int i = 0; i < response.warning_size(); ++i) {
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified());
}
}
dlg->show();
}
void UserContextMenu::warnUser_processUserInfoResponse(const Response &resp)
{
const Response_GetUserInfo &response = resp.GetExtension(Response_GetUserInfo::ext);
ServerInfo_User userInfo = response.user_info();
Command_GetWarnList cmd;
cmd.set_user_name(userInfo.name());
cmd.set_user_clientid(userInfo.clientid());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(warnUser_processGetWarningsListResponse(Response)));
client->sendCommand(pend);
}
void UserContextMenu::banUserHistory_processResponse(const Response &resp)
{
const Response_BanHistory &response = resp.GetExtension(Response_BanHistory::ext);
if (resp.response_code() == Response::RespOk) {
if (response.ban_list_size() > 0) {
QTableWidget *table = new QTableWidget();
table->setWindowTitle(tr("Ban History"));
table->setRowCount(response.ban_list_size());
table->setColumnCount(5);
table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
table->setHorizontalHeaderLabels(
QString(tr("Ban Time;Moderator;Ban Length;Ban Reason;Visible Reason")).split(";"));
ServerInfo_Ban ban;
for (int i = 0; i < response.ban_list_size(); ++i) {
ban = response.ban_list(i);
table->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(ban.ban_time())));
table->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(ban.admin_name())));
table->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(ban.ban_length())));
table->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(ban.ban_reason())));
table->setItem(i, 4, new QTableWidgetItem(QString::fromStdString(ban.visible_reason())));
}
table->resizeColumnsToContents();
table->setMinimumSize(table->horizontalHeader()->length() + (table->columnCount() * 5),
table->verticalHeader()->length() + (table->rowCount() * 3));
table->show();
} else
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Ban History"),
tr("User has never been banned."));
} else
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Ban History"),
tr("Failed to collect ban information."));
}
void UserContextMenu::warnUserHistory_processResponse(const Response &resp)
{
const Response_WarnHistory &response = resp.GetExtension(Response_WarnHistory::ext);
if (resp.response_code() == Response::RespOk) {
if (response.warn_list_size() > 0) {
QTableWidget *table = new QTableWidget();
table->setWindowTitle(tr("Warning History"));
table->setRowCount(response.warn_list_size());
table->setColumnCount(4);
table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
table->setHorizontalHeaderLabels(QString(tr("Warning Time;Moderator;User Name;Reason")).split(";"));
ServerInfo_Warning warn;
for (int i = 0; i < response.warn_list_size(); ++i) {
warn = response.warn_list(i);
table->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(warn.time_of())));
table->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(warn.admin_name())));
table->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(warn.user_name())));
table->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(warn.reason())));
}
table->resizeColumnsToContents();
table->setMinimumSize(table->horizontalHeader()->length() + (table->columnCount() * 5),
table->verticalHeader()->length() + (table->rowCount() * 3));
table->show();
} else
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Warning History"),
tr("User has never been warned."));
} else
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Warning History"),
tr("Failed to collect warning information."));
}
void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const CommandContainer &commandContainer)
{
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
if (resp.response_code() == Response::RespOk) {
if (cmd.should_be_mod() || cmd.should_be_judge()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"),
tr("Successfully promoted user."));
} else {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"), tr("Successfully demoted user."));
}
} else {
if (cmd.should_be_mod() || cmd.should_be_judge()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user."));
} else {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user."));
}
}
}
void UserContextMenu::banUser_dialogFinished()
{
BanDialog *dlg = static_cast<BanDialog *>(sender());
Command_BanFromServer cmd;
cmd.set_user_name(dlg->getBanName().toStdString());
cmd.set_address(dlg->getBanIP().toStdString());
cmd.set_minutes(dlg->getMinutes());
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));
}
void UserContextMenu::warnUser_dialogFinished()
{
WarningDialog *dlg = static_cast<WarningDialog *>(sender());
if (dlg->getName().isEmpty() || tabSupervisor->getOwnUsername().simplified().isEmpty())
return;
Command_WarnUser cmd;
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));
}
void UserContextMenu::showContextMenu(const QPoint &pos,
const QString &userName,
UserLevelFlags userLevel,
bool online,
int playerId)
{
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,
const QString &userName,
UserLevelFlags userLevel,
bool online,
int playerId,
const QString &deckHash,
ChatView *chatView)
{
QAction *aCopyToClipBoard = nullptr, *aRemoveMessages = nullptr;
aUserName->setText(userName);
auto *menu = new QMenu(static_cast<QWidget *>(parent()));
menu->addAction(aUserName);
menu->addSeparator();
if (!deckHash.isEmpty()) {
aCopyToClipBoard = new QAction(tr("Copy hash to clipboard"), this);
menu->addAction(aCopyToClipBoard);
}
menu->addAction(aDetails);
menu->addAction(aShowGames);
menu->addAction(aChat);
if (userLevel.testFlag(ServerInfo_User::IsRegistered) && tabSupervisor->isOwnUserRegistered()) {
menu->addSeparator();
if (tabSupervisor->isUserBuddy(userName)) {
menu->addAction(aRemoveFromBuddyList);
} else {
menu->addAction(aAddToBuddyList);
}
if (tabSupervisor->isUserIgnored(userName)) {
menu->addAction(aRemoveFromIgnoreList);
} 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();
menu->addAction(aKick);
}
if (!tabSupervisor->getAdminLocked()) {
menu->addSeparator();
menu->addAction(aWarnUser);
menu->addAction(aWarnHistory);
menu->addSeparator();
menu->addAction(aBan);
menu->addAction(aBanHistory);
menu->addSeparator();
if (userLevel.testFlag(ServerInfo_User::IsModerator) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aDemoteFromMod);
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToMod);
}
if (userLevel.testFlag(ServerInfo_User::IsJudge) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aDemoteFromJudge);
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToJudge);
}
}
bool anotherUser = userName != tabSupervisor->getOwnUsername();
aDetails->setEnabled(true);
aChat->setEnabled(anotherUser && online);
aShowGames->setEnabled(online);
aAddToBuddyList->setEnabled(anotherUser);
aRemoveFromBuddyList->setEnabled(anotherUser);
aAddToIgnoreList->setEnabled(anotherUser);
aRemoveFromIgnoreList->setEnabled(anotherUser);
aKick->setEnabled(anotherUser);
aWarnUser->setEnabled(anotherUser);
aWarnHistory->setEnabled(anotherUser);
aBan->setEnabled(anotherUser);
aBanHistory->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser);
QAction *actionClicked = menu->exec(pos);
if (actionClicked == nullptr) {
} else if (actionClicked == aDetails) {
UserInfoBox *infoWidget =
new UserInfoBox(client, false, static_cast<QWidget *>(parent()),
Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint);
infoWidget->setAttribute(Qt::WA_DeleteOnClose);
infoWidget->updateInfo(userName);
} else if (actionClicked == aChat) {
emit openMessageDialog(userName, true);
} else if (actionClicked == aShowGames) {
Command_GetGamesOfUser cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(gamesOfUserReceived(Response, CommandContainer)));
client->sendCommand(pend);
} else if (actionClicked == aAddToBuddyList) {
Command_AddToList cmd;
cmd.set_list("buddy");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aRemoveFromBuddyList) {
Command_RemoveFromList cmd;
cmd.set_list("buddy");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aAddToIgnoreList) {
Command_AddToList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aRemoveFromIgnoreList) {
Command_RemoveFromList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aKick) {
Command_KickFromGame cmd;
cmd.set_player_id(playerId);
game->sendGameCommand(cmd);
} else if (actionClicked == aBan) {
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(banUser_processUserInfoResponse(Response)));
client->sendCommand(pend);
} else if (actionClicked == aPromoteToMod || actionClicked == aDemoteFromMod) {
Command_AdjustMod cmd;
cmd.set_user_name(userName.toStdString());
cmd.set_should_be_mod(actionClicked == aPromoteToMod);
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(adjustMod_processUserResponse(Response, CommandContainer)));
client->sendCommand(pend);
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
Command_AdjustMod cmd;
cmd.set_user_name(userName.toStdString());
cmd.set_should_be_judge(actionClicked == aPromoteToJudge);
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(adjustMod_processUserResponse(Response, CommandContainer)));
client->sendCommand(pend);
} else if (actionClicked == aBanHistory) {
Command_GetBanHistory cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(banUserHistory_processResponse(Response)));
client->sendCommand(pend);
} else if (actionClicked == aWarnUser) {
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(warnUser_processUserInfoResponse(Response)));
client->sendCommand(pend);
} else if (actionClicked == aWarnHistory) {
Command_GetWarnHistory cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(warnUserHistory_processResponse(Response)));
client->sendCommand(pend);
} else if (actionClicked == aCopyToClipBoard) {
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setText(deckHash);
} else if (actionClicked == aRemoveMessages) {
chatView->redactMessages(userName, -1);
}
delete menu;
}

View file

@ -0,0 +1,69 @@
#ifndef USER_CONTEXT_MENU_H
#define USER_CONTEXT_MENU_H
#include "user_level.h"
#include <QObject>
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
{
Q_OBJECT
private:
AbstractClient *client;
TabSupervisor *tabSupervisor;
TabGame *game;
QAction *aUserName;
QAction *aDetails;
QAction *aShowGames;
QAction *aChat;
QAction *aAddToBuddyList, *aRemoveFromBuddyList;
QAction *aAddToIgnoreList, *aRemoveFromIgnoreList;
QAction *aKick;
QAction *aBan, *aBanHistory;
QAction *aPromoteToMod, *aDemoteFromMod;
QAction *aPromoteToJudge, *aDemoteFromJudge;
QAction *aWarnUser, *aWarnHistory;
signals:
void openMessageDialog(const QString &userName, bool focus);
private slots:
void banUser_processUserInfoResponse(const Response &resp);
void warnUser_processGetWarningsListResponse(const Response &r);
void warnUser_processUserInfoResponse(const Response &resp);
void banUserHistory_processResponse(const Response &resp);
void warnUserHistory_processResponse(const Response &resp);
void adjustMod_processUserResponse(const Response &resp, const CommandContainer &commandContainer);
void banUser_dialogFinished();
void warnUser_dialogFinished();
void gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer);
public:
UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *_parent, TabGame *_game = 0);
void retranslateUi();
void showContextMenu(const QPoint &pos,
const QString &userName,
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,
ChatView *chatView = nullptr);
};
#endif

View file

@ -0,0 +1,368 @@
#include "user_info_box.h"
#include "../../client/game_logic/abstract_client.h"
#include "../../client/get_text_with_max.h"
#include "../../client/ui/pixel_map_generator.h"
#include "../../dialogs/dlg_edit_avatar.h"
#include "../../dialogs/dlg_edit_password.h"
#include "../../dialogs/dlg_edit_user.h"
#include "../pending_command.h"
#include "passwordhasher.h"
#include "pb/response_get_user_info.pb.h"
#include "pb/session_commands.pb.h"
#include <QDateTime>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *parent, Qt::WindowFlags flags)
: QWidget(parent, flags), client(_client), editable(_editable)
{
QFont nameFont = nameLabel.font();
nameFont.setBold(true);
nameFont.setPointSizeF(nameFont.pointSizeF() * 1.5);
nameLabel.setFont(nameFont);
auto *userIconAndNameLayout = new QHBoxLayout;
userIconAndNameLayout->addWidget(&userLevelIcon, 0, Qt::AlignCenter);
userIconAndNameLayout->addWidget(&nameLabel, 1);
userIconAndNameLayout->addStretch();
avatarPic.setMinimumSize(200, 200);
avatarPic.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
avatarPic.setAlignment(Qt::AlignCenter);
auto *avatarLayout = new QHBoxLayout;
avatarLayout->setContentsMargins(0, 0, 0, 0);
avatarLayout->addStretch(1);
avatarLayout->addWidget(&avatarPic, 3);
avatarLayout->addStretch(1);
auto *mainLayout = new QGridLayout;
mainLayout->addLayout(avatarLayout, 0, 0, 1, 3);
mainLayout->addLayout(userIconAndNameLayout, 1, 0, 1, 3);
mainLayout->addWidget(&realNameLabel1, 2, 0, 1, 1);
mainLayout->addWidget(&realNameLabel2, 2, 1, 1, 2);
mainLayout->addWidget(&countryLabel1, 3, 0, 1, 1);
mainLayout->addWidget(&countryLabel2, 3, 1, 1, 1, Qt::AlignCenter);
mainLayout->addWidget(&countryLabel3, 3, 2, 1, 1);
mainLayout->addWidget(&userLevelLabel1, 4, 0, 1, 1);
mainLayout->addWidget(&userLevelLabel2, 4, 1, 1, 2);
mainLayout->addWidget(&accountAgeLabel1, 5, 0, 1, 1);
mainLayout->addWidget(&accountAgeLabel2, 5, 1, 1, 2);
mainLayout->setColumnStretch(2, 10);
if (editable) {
auto *buttonsLayout = new QHBoxLayout;
buttonsLayout->addWidget(&editButton);
buttonsLayout->addWidget(&passwordButton);
buttonsLayout->addWidget(&avatarButton);
mainLayout->addLayout(buttonsLayout, 7, 0, 1, 3);
connect(&editButton, SIGNAL(clicked()), this, SLOT(actEdit()));
connect(&passwordButton, SIGNAL(clicked()), this, SLOT(actPassword()));
connect(&avatarButton, SIGNAL(clicked()), this, SLOT(actAvatar()));
}
setWindowTitle(tr("User Information"));
setLayout(mainLayout);
retranslateUi();
}
void UserInfoBox::retranslateUi()
{
realNameLabel1.setText(tr("Real Name:"));
countryLabel1.setText(tr("Location:"));
userLevelLabel1.setText(tr("User Level:"));
accountAgeLabel1.setText(tr("Account Age:"));
editButton.setText(tr("Edit"));
passwordButton.setText(tr("Change password"));
avatarButton.setText(tr("Change avatar"));
}
void UserInfoBox::updateInfo(const ServerInfo_User &user)
{
const UserLevelFlags userLevel(user.user_level());
const std::string &bmp = user.avatar_bmp();
if (!avatarPixmap.loadFromData((const uchar *)bmp.data(), static_cast<uint>(bmp.size()))) {
avatarPixmap =
UserLevelPixmapGenerator::generatePixmap(64, userLevel, false, QString::fromStdString(user.privlevel()));
}
nameLabel.setText(QString::fromStdString(user.name()));
realNameLabel2.setText(QString::fromStdString(user.real_name()));
QString country = QString::fromStdString(user.country());
if (country.length() != 0) {
countryLabel2.setPixmap(CountryPixmapGenerator::generatePixmap(13, country));
countryLabel3.setText(QString("%1").arg(country.toUpper()));
} else {
countryLabel2.setText("");
countryLabel3.setText("");
}
userLevelIcon.setPixmap(
UserLevelPixmapGenerator::generatePixmap(15, userLevel, false, QString::fromStdString(user.privlevel())));
QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin))
userLevelText = tr("Administrator");
else if (userLevel.testFlag(ServerInfo_User::IsModerator))
userLevelText = tr("Moderator");
else if (userLevel.testFlag(ServerInfo_User::IsRegistered))
userLevelText = tr("Registered user");
else
userLevelText = tr("Unregistered user");
if (userLevel.testFlag(ServerInfo_User::IsJudge))
userLevelText += " | " + tr("Judge");
if (user.has_privlevel() && user.privlevel() != "NONE") {
userLevelText += " | " + QString("%1").arg(user.privlevel().c_str());
}
userLevelLabel2.setText(userLevelText);
QString accountAgeString = tr("Unregistered user");
if (userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsModerator) ||
userLevel.testFlag(ServerInfo_User::IsRegistered)) {
accountAgeString = getAgeString(user.accountage_secs());
}
accountAgeLabel2.setText(accountAgeString);
}
QString UserInfoBox::getAgeString(int ageSeconds)
{
QString accountAgeString = tr("Unknown");
if (ageSeconds == 0)
return accountAgeString;
auto date = QDateTime::fromSecsSinceEpoch(QDateTime::currentSecsSinceEpoch() - ageSeconds).date();
if (!date.isValid())
return accountAgeString;
QString dateString = QLocale().toString(date, QLocale::ShortFormat);
auto now = QDate::currentDate();
auto daysAndYears = getDaysAndYearsBetween(date, now);
QString yearString;
if (daysAndYears.second > 0) {
yearString = tr("%n Year(s), ", "amount of years (only shown if more than 0)", daysAndYears.second);
}
accountAgeString =
tr("%10%n Day(s) %20", "amount of years (if more than 0), amount of days, date in local short format",
daysAndYears.first)
.arg(yearString)
.arg(dateString);
return accountAgeString;
}
void UserInfoBox::updateInfo(const QString &userName)
{
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(processResponse(const Response &)));
client->sendCommand(pend);
}
void UserInfoBox::processResponse(const Response &r)
{
const Response_GetUserInfo &response = r.GetExtension(Response_GetUserInfo::ext);
updateInfo(response.user_info());
resize(sizeHint());
show();
}
void UserInfoBox::actEdit()
{
Command_GetUserInfo cmd;
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(actEditInternal(const Response &)));
client->sendCommand(pend);
}
void UserInfoBox::actEditInternal(const Response &r)
{
const Response_GetUserInfo &response = r.GetExtension(Response_GetUserInfo::ext);
const ServerInfo_User &user = response.user_info();
QString email = QString::fromStdString(user.email());
QString country = QString::fromStdString(user.country());
QString realName = QString::fromStdString(user.real_name());
DlgEditUser dlg(this, email, country, realName);
if (!dlg.exec())
return;
Command_AccountEdit cmd;
cmd.set_real_name(dlg.getRealName().toStdString());
if (client->getServerSupportsPasswordHash()) {
if (email != dlg.getEmail()) {
// real password is required to change email
bool ok = false;
QString password =
getTextWithMax(this, tr("Enter Password"),
tr("Password verification is required in order to change your email address"),
QLineEdit::Password, "", &ok);
if (!ok)
return;
cmd.set_password_check(password.toStdString());
cmd.set_email(dlg.getEmail().toStdString());
} // servers that support password hash do not require all fields to be filled anymore
} else {
cmd.set_email(dlg.getEmail().toStdString());
}
cmd.set_country(dlg.getCountry().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(processEditResponse(const Response &)));
client->sendCommand(pend);
}
void UserInfoBox::actPassword()
{
DlgEditPassword dlg(this);
if (!dlg.exec())
return;
auto oldPassword = dlg.getOldPassword();
auto newPassword = dlg.getNewPassword();
if (client->getServerSupportsPasswordHash()) {
Command_RequestPasswordSalt cmd;
cmd.set_user_name(client->getUserName().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend,
// we need qoverload here in order to select the right version of this function
QOverload<const Response &, const CommandContainer &, const QVariant &>::of(&PendingCommand::finished),
this, [=](const Response &response, const CommandContainer &, const QVariant &) {
if (response.response_code() == Response::RespOk) {
changePassword(oldPassword, newPassword);
} else {
QMessageBox::critical(this, tr("Error"),
tr("An error occurred while trying to update your user information."));
}
});
client->sendCommand(pend);
} else {
changePassword(oldPassword, newPassword);
}
}
void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword)
{
Command_AccountPassword cmd;
cmd.set_old_password(oldPassword.toStdString());
if (client->getServerSupportsPasswordHash()) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
QString hashedPassword = PasswordHasher::computeHash(newPassword, passwordSalt);
cmd.set_hashed_new_password(hashedPassword.toStdString());
} else {
cmd.set_new_password(newPassword.toStdString());
}
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(processPasswordResponse(const Response &)));
client->sendCommand(pend);
}
void UserInfoBox::actAvatar()
{
DlgEditAvatar dlg(this);
if (!dlg.exec())
return;
Command_AccountImage cmd;
cmd.set_image(dlg.getImage().data(), dlg.getImage().size());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(processAvatarResponse(const Response &)));
client->sendCommand(pend);
}
void UserInfoBox::processEditResponse(const Response &r)
{
switch (r.response_code()) {
case Response::RespOk:
updateInfo(nameLabel.text());
QMessageBox::information(this, tr("Information"), tr("User information updated."));
break;
case Response::RespFunctionNotAllowed:
QMessageBox::critical(this, tr("Error"),
tr("This server does not permit you to update your user informations."));
break;
case Response::RespWrongPassword:
QMessageBox::critical(this, tr("Error"), tr("The entered password does not match your account."));
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"),
tr("An error occurred while trying to update your user information."));
break;
}
}
void UserInfoBox::processPasswordResponse(const Response &r)
{
switch (r.response_code()) {
case Response::RespOk:
QMessageBox::information(this, tr("Information"), tr("Password changed."));
break;
case Response::RespFunctionNotAllowed:
QMessageBox::critical(this, tr("Error"), tr("This server does not permit you to change your password."));
break;
case Response::RespPasswordTooShort:
QMessageBox::critical(this, tr("Error"), tr("The new password is too short."));
break;
case Response::RespWrongPassword:
QMessageBox::critical(this, tr("Error"), tr("The old password is incorrect."));
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"),
tr("An error occurred while trying to update your user information."));
break;
}
}
void UserInfoBox::processAvatarResponse(const Response &r)
{
switch (r.response_code()) {
case Response::RespOk:
updateInfo(nameLabel.text());
QMessageBox::information(this, tr("Information"), tr("Avatar updated."));
break;
case Response::RespFunctionNotAllowed:
QMessageBox::critical(this, tr("Error"), tr("This server does not permit you to update your avatar."));
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to updater your avatar."));
break;
}
}
void UserInfoBox::resizeEvent(QResizeEvent *event)
{
QPixmap resizedPixmap = avatarPixmap.scaled(avatarPic.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
avatarPic.setPixmap(resizedPixmap);
QWidget::resizeEvent(event);
}

View file

@ -0,0 +1,55 @@
#ifndef USERINFOBOX_H
#define USERINFOBOX_H
#include <QDateTime>
#include <QLabel>
#include <QPushButton>
#include <QWidget>
class ServerInfo_User;
class AbstractClient;
class Response;
class UserInfoBox : public QWidget
{
Q_OBJECT
private:
AbstractClient *client;
bool editable;
QLabel avatarPic, userLevelIcon, nameLabel, realNameLabel1, realNameLabel2, countryLabel1, countryLabel2,
countryLabel3, userLevelLabel1, userLevelLabel2, accountAgeLabel1, accountAgeLabel2;
QPushButton editButton, passwordButton, avatarButton;
QPixmap avatarPixmap;
static QString getAgeString(int ageSeconds);
public:
UserInfoBox(AbstractClient *_client, bool editable, QWidget *parent = nullptr, Qt::WindowFlags flags = {});
void retranslateUi();
inline static QPair<int, int> getDaysAndYearsBetween(const QDate &then, const QDate &now)
{
int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo
int days = then.addYears(years).daysTo(now);
return {days, years};
}
private slots:
void processResponse(const Response &r);
void processEditResponse(const Response &r);
void processPasswordResponse(const Response &r);
void processAvatarResponse(const Response &r);
void actEdit();
void actEditInternal(const Response &r);
void actPassword();
void actAvatar();
public slots:
void updateInfo(const ServerInfo_User &user);
void updateInfo(const QString &userName);
private:
void resizeEvent(QResizeEvent *event) override;
void changePassword(const QString &oldPassword, const QString &newPassword);
};
#endif

View file

@ -0,0 +1,80 @@
#include "user_info_connection.h"
#include "../../settings/cache_settings.h"
#include <QDebug>
#include <utility>
UserConnection_Information::UserConnection_Information() = default;
UserConnection_Information::UserConnection_Information(QString _saveName,
QString _serverName,
QString _portNum,
QString _userName,
QString _pass,
bool _savePass,
QString _site)
: saveName(std::move(_saveName)), server(std::move(_serverName)), port(std::move(_portNum)),
username(std::move(_userName)), password(std::move(_pass)), savePassword(_savePass), site(std::move(_site))
{
}
QMap<QString, std::pair<QString, UserConnection_Information>> UserConnection_Information::getServerInfo()
{
QMap<QString, std::pair<QString, UserConnection_Information>> serverList;
ServersSettings &servers = SettingsCache::instance().servers();
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
for (int i = 0; i < size; i++) {
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
UserConnection_Information userInfo(_saveName, serverName, portNum, userName, pass, savePass, _site);
serverList.insert(_saveName, std::make_pair(serverName, userInfo));
}
return serverList;
}
QStringList UserConnection_Information::getServerInfo(const QString &find)
{
QStringList _server;
ServersSettings &servers = SettingsCache::instance().servers();
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
for (int i = 0; i < size; i++) {
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
if (find != _saveName)
continue;
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
_server.append(_saveName);
_server.append(serverName);
_server.append(portNum);
_server.append(userName);
_server.append(pass);
_server.append(savePass ? "1" : "0");
_server.append(_site);
break;
}
if (_server.empty())
qDebug() << "There was a problem!";
return _server;
}

View file

@ -0,0 +1,55 @@
#ifndef USERCONNECTION_INFORMATION_H
#define USERCONNECTION_INFORMATION_H
#include <QApplication>
#include <QDir>
#include <QFile>
#include <QSettings>
#include <QStandardPaths>
class UserConnection_Information
{
private:
QString saveName;
QString server;
QString port;
QString username;
QString password;
bool savePassword;
QString site;
public:
UserConnection_Information();
UserConnection_Information(QString, QString, QString, QString, QString, bool, QString);
QString getSaveName() const
{
return saveName;
}
QString getServer() const
{
return server;
}
QString getPort() const
{
return port;
}
QString getUsername() const
{
return username;
}
QString getPassword() const
{
return password;
}
bool getSavePassword() const
{
return savePassword;
}
QString getSite() const
{
return site;
}
QMap<QString, std::pair<QString, UserConnection_Information>> getServerInfo();
QStringList getServerInfo(const QString &find);
};
#endif

View file

@ -0,0 +1,464 @@
#include "user_list.h"
#include "../../client/game_logic/abstract_client.h"
#include "../../client/tabs/tab_account.h"
#include "../../client/tabs/tab_supervisor.h"
#include "../../client/ui/pixel_map_generator.h"
#include "../../game/game_selector.h"
#include "../pending_command.h"
#include "pb/moderator_commands.pb.h"
#include "pb/response_get_games_of_user.pb.h"
#include "pb/response_get_user_info.pb.h"
#include "pb/session_commands.pb.h"
#include "trice_limits.h"
#include "user_context_menu.h"
#include <QApplication>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QVBoxLayout>
#include <QWidget>
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
{
setAttribute(Qt::WA_DeleteOnClose);
nameBanCheckBox = new QCheckBox(tr("ban &user name"));
nameBanCheckBox->setChecked(true);
nameBanEdit = new QLineEdit(QString::fromStdString(info.name()));
nameBanEdit->setMaxLength(MAX_NAME_LENGTH);
ipBanCheckBox = new QCheckBox(tr("ban &IP address"));
ipBanCheckBox->setChecked(true);
ipBanEdit = new QLineEdit(QString::fromStdString(info.address()));
ipBanEdit->setMaxLength(MAX_NAME_LENGTH);
idBanCheckBox = new QCheckBox(tr("ban client I&D"));
idBanCheckBox->setChecked(true);
idBanEdit = new QLineEdit(QString::fromStdString(info.clientid()));
idBanEdit->setMaxLength(MAX_NAME_LENGTH);
if (QString::fromStdString(info.clientid()).isEmpty())
idBanCheckBox->setChecked(false);
QGridLayout *banTypeGrid = new QGridLayout;
banTypeGrid->addWidget(nameBanCheckBox, 0, 0);
banTypeGrid->addWidget(nameBanEdit, 0, 1);
banTypeGrid->addWidget(ipBanCheckBox, 1, 0);
banTypeGrid->addWidget(ipBanEdit, 1, 1);
banTypeGrid->addWidget(idBanCheckBox, 2, 0);
banTypeGrid->addWidget(idBanEdit, 2, 1);
QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type"));
banTypeGroupBox->setLayout(banTypeGrid);
permanentRadio = new QRadioButton(tr("&permanent ban"));
temporaryRadio = new QRadioButton(tr("&temporary ban"));
temporaryRadio->setChecked(true);
connect(temporaryRadio, SIGNAL(toggled(bool)), this, SLOT(enableTemporaryEdits(bool)));
daysLabel = new QLabel(tr("&Days:"));
daysEdit = new QSpinBox;
daysEdit->setMinimum(0);
daysEdit->setValue(0);
daysEdit->setMaximum(10000);
daysLabel->setBuddy(daysEdit);
hoursLabel = new QLabel(tr("&Hours:"));
hoursEdit = new QSpinBox;
hoursEdit->setMinimum(0);
hoursEdit->setValue(0);
hoursEdit->setMaximum(24);
hoursLabel->setBuddy(hoursEdit);
minutesLabel = new QLabel(tr("&Minutes:"));
minutesEdit = new QSpinBox;
minutesEdit->setMinimum(0);
minutesEdit->setValue(5);
minutesEdit->setMaximum(60);
minutesLabel->setBuddy(minutesEdit);
QGridLayout *durationLayout = new QGridLayout;
durationLayout->addWidget(permanentRadio, 0, 0, 1, 6);
durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6);
durationLayout->addWidget(daysLabel, 2, 0);
durationLayout->addWidget(daysEdit, 2, 1);
durationLayout->addWidget(hoursLabel, 2, 2);
durationLayout->addWidget(hoursEdit, 2, 3);
durationLayout->addWidget(minutesLabel, 2, 4);
durationLayout->addWidget(minutesEdit, 2, 5);
QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban"));
durationGroupBox->setLayout(durationLayout);
QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\nThis is only saved for moderators and "
"cannot be seen by the banned person."));
reasonEdit = new QPlainTextEdit;
QLabel *visibleReasonLabel =
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()));
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(banTypeGroupBox);
vbox->addWidget(durationGroupBox);
vbox->addWidget(reasonLabel);
vbox->addWidget(reasonEdit);
vbox->addWidget(visibleReasonLabel);
vbox->addWidget(visibleReasonEdit);
vbox->addWidget(deleteMessages);
vbox->addLayout(buttonLayout);
setLayout(vbox);
setWindowTitle(tr("Ban user from server"));
}
WarningDialog::WarningDialog(const QString userName, const QString clientID, QWidget *parent) : QDialog(parent)
{
setAttribute(Qt::WA_DeleteOnClose);
descriptionLabel = new QLabel(tr("Which warning would you like to send?"));
nameWarning = new QLineEdit(userName);
nameWarning->setMaxLength(MAX_NAME_LENGTH);
warnClientID = new QLineEdit(clientID);
warnClientID->setMaxLength(MAX_NAME_LENGTH);
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()));
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(descriptionLabel);
vbox->addWidget(nameWarning);
vbox->addWidget(warningOption);
vbox->addWidget(deleteMessages);
vbox->addLayout(buttonLayout);
setLayout(vbox);
setWindowTitle(tr("Warn user for misconduct"));
}
void WarningDialog::okClicked()
{
if (nameWarning->text().simplified().isEmpty()) {
QMessageBox::critical(this, tr("Error"),
tr("User name to send a warning to can not be blank, please specify a user to warn."));
return;
}
if (warningOption->currentText().simplified().isEmpty()) {
QMessageBox::critical(this, tr("Error"),
tr("Warning to use can not be blank, please select a valid warning to send."));
return;
}
accept();
}
QString WarningDialog::getName() const
{
return nameWarning->text().simplified();
}
QString WarningDialog::getWarnID() const
{
return warnClientID->text().simplified();
}
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);
}
void BanDialog::okClicked()
{
if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) {
QMessageBox::critical(this, tr("Error"),
tr("You have to select a name-based, IP-based, clientId based, or some combination of "
"the three to place a ban."));
return;
}
if (nameBanCheckBox->isChecked())
if (nameBanEdit->text().simplified() == "") {
QMessageBox::critical(this, tr("Error"),
tr("You must have a value in the name ban when selecting the name ban checkbox."));
return;
}
if (ipBanCheckBox->isChecked())
if (ipBanEdit->text().simplified() == "") {
QMessageBox::critical(this, tr("Error"),
tr("You must have a value in the ip ban when selecting the ip ban checkbox."));
return;
}
if (idBanCheckBox->isChecked())
if (idBanEdit->text().simplified() == "") {
QMessageBox::critical(
this, tr("Error"),
tr("You must have a value in the clientid ban when selecting the clientid ban checkbox."));
return;
}
accept();
}
void BanDialog::enableTemporaryEdits(bool enabled)
{
daysLabel->setEnabled(enabled);
daysEdit->setEnabled(enabled);
hoursLabel->setEnabled(enabled);
hoursEdit->setEnabled(enabled);
minutesLabel->setEnabled(enabled);
minutesEdit->setEnabled(enabled);
}
QString BanDialog::getBanId() const
{
return idBanCheckBox->isChecked() ? idBanEdit->text() : QString();
}
QString BanDialog::getBanName() const
{
return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString();
}
QString BanDialog::getBanIP() const
{
return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString();
}
int BanDialog::getMinutes() const
{
return permanentRadio->isChecked() ? 0
: (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value());
}
QString BanDialog::getReason() const
{
return reasonEdit->toPlainText();
}
QString BanDialog::getVisibleReason() const
{
return visibleReasonEdit->toPlainText();
}
int BanDialog::getDeleteMessages() const
{
return deleteMessages->isChecked() ? -1 : 0;
}
UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent)
{
}
bool UserListItemDelegate::editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index)
{
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
QMouseEvent *const mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::RightButton) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
static_cast<UserList *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
#else
static_cast<UserList *>(parent())->showContextMenu(mouseEvent->globalPos(), index);
#endif
return true;
}
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type)
{
setUserInfo(_userInfo);
}
void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo)
{
userInfo = _userInfo;
setData(0, Qt::UserRole, userInfo.user_level());
setIcon(0, QIcon(UserLevelPixmapGenerator::generatePixmap(12, UserLevelFlags(userInfo.user_level()), false,
QString::fromStdString(userInfo.privlevel()))));
setIcon(1, QIcon(CountryPixmapGenerator::generatePixmap(12, QString::fromStdString(userInfo.country()))));
setData(2, Qt::UserRole, QString::fromStdString(userInfo.name()));
setData(2, Qt::DisplayRole, QString::fromStdString(userInfo.name()));
}
void UserListTWI::setOnline(bool online)
{
setData(0, Qt::UserRole + 1, online);
setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray));
}
bool UserListTWI::operator<(const QTreeWidgetItem &other) const
{
// Sort by online/offline
if (data(0, Qt::UserRole + 1) != other.data(0, Qt::UserRole + 1))
return data(0, Qt::UserRole + 1).toBool();
// Sort by user level
if ((data(0, Qt::UserRole).toInt() & 15) != (other.data(0, Qt::UserRole).toInt() & 15))
return (data(0, Qt::UserRole).toInt() & 15) > (other.data(0, Qt::UserRole).toInt() & 15);
// Sort by name
return QString::localeAwareCompare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
}
UserList::UserList(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, QWidget *parent)
: QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0)
{
itemDelegate = new UserListItemDelegate(this);
userContextMenu = new UserContextMenu(tabSupervisor, this);
connect(userContextMenu, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool)));
userTree = new QTreeWidget;
userTree->setColumnCount(3);
userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
userTree->setHeaderHidden(true);
userTree->setRootIsDecorated(false);
userTree->setIconSize(QSize(20, 12));
userTree->setItemDelegate(itemDelegate);
userTree->setAlternatingRowColors(true);
connect(userTree, SIGNAL(itemActivated(QTreeWidgetItem *, int)), this, SLOT(userClicked(QTreeWidgetItem *, int)));
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(userTree);
setLayout(vbox);
retranslateUi();
}
void UserList::retranslateUi()
{
userContextMenu->retranslateUi();
switch (type) {
case AllUsersList:
titleStr = tr("Users connected to server: %1");
break;
case RoomList:
titleStr = tr("Users in this room: %1");
break;
case BuddyList:
titleStr = tr("Buddies online: %1 / %2");
break;
case IgnoreList:
titleStr = tr("Ignored users online: %1 / %2");
break;
}
updateCount();
}
void UserList::processUserInfo(const ServerInfo_User &user, bool online)
{
const QString userName = QString::fromStdString(user.name());
UserListTWI *item = users.value(userName);
if (item)
item->setUserInfo(user);
else {
item = new UserListTWI(user);
users.insert(userName, item);
userTree->addTopLevelItem(item);
if (online)
++onlineCount;
updateCount();
}
item->setOnline(online);
}
bool UserList::deleteUser(const QString &userName)
{
UserListTWI *twi = users.value(userName);
if (twi) {
users.remove(userName);
userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi));
if (twi->data(0, Qt::UserRole + 1).toBool())
--onlineCount;
delete twi;
updateCount();
return true;
}
return false;
}
void UserList::setUserOnline(const QString &userName, bool online)
{
UserListTWI *twi = users.value(userName);
if (!twi)
return;
twi->setOnline(online);
if (online)
++onlineCount;
else
--onlineCount;
updateCount();
}
void UserList::updateCount()
{
QString str = titleStr;
if ((type == BuddyList) || (type == IgnoreList))
str = str.arg(onlineCount);
setTitle(str.arg(userTree->topLevelItemCount()));
}
void UserList::userClicked(QTreeWidgetItem *item, int /*column*/)
{
emit openMessageDialog(item->data(2, Qt::UserRole).toString(), true);
}
void UserList::showContextMenu(const QPoint &pos, const QModelIndex &index)
{
const ServerInfo_User &userInfo = static_cast<UserListTWI *>(userTree->topLevelItem(index.row()))->getUserInfo();
bool online = index.sibling(index.row(), 0).data(Qt::UserRole + 1).toBool();
userContextMenu->showContextMenu(pos, QString::fromStdString(userInfo.name()),
UserLevelFlags(userInfo.user_level()), online);
}
void UserList::sortItems()
{
userTree->sortItems(1, Qt::AscendingOrder);
}

View file

@ -0,0 +1,142 @@
#ifndef USERLIST_H
#define USERLIST_H
#include "pb/moderator_commands.pb.h"
#include "user_level.h"
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QStyledItemDelegate>
#include <QTreeWidgetItem>
class QTreeWidget;
class ServerInfo_User;
class AbstractClient;
class TabSupervisor;
class QLabel;
class QCheckBox;
class QSpinBox;
class QRadioButton;
class QPlainTextEdit;
class Response;
class CommandContainer;
class UserContextMenu;
class BanDialog : public QDialog
{
Q_OBJECT
private:
QLabel *daysLabel, *hoursLabel, *minutesLabel;
QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages;
QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit;
QSpinBox *daysEdit, *hoursEdit, *minutesEdit;
QRadioButton *permanentRadio, *temporaryRadio;
QPlainTextEdit *reasonEdit, *visibleReasonEdit;
private slots:
void okClicked();
void enableTemporaryEdits(bool enabled);
public:
BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr);
QString getBanName() const;
QString getBanIP() const;
QString getBanId() const;
int getMinutes() const;
QString getReason() const;
QString getVisibleReason() const;
int getDeleteMessages() const;
};
class WarningDialog : public QDialog
{
Q_OBJECT
private:
QLabel *descriptionLabel;
QLineEdit *nameWarning;
QComboBox *warningOption;
QLineEdit *warnClientID;
QCheckBox *deleteMessages;
private slots:
void okClicked();
public:
WarningDialog(const QString userName, const QString clientID, QWidget *parent = nullptr);
QString getName() const;
QString getWarnID() const;
QString getReason() const;
int getDeleteMessages() const;
void addWarningOption(const QString warning);
};
class UserListItemDelegate : public QStyledItemDelegate
{
public:
UserListItemDelegate(QObject *const parent);
bool
editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index);
};
class UserListTWI : public QTreeWidgetItem
{
private:
ServerInfo_User userInfo;
public:
UserListTWI(const ServerInfo_User &_userInfo);
const ServerInfo_User &getUserInfo() const
{
return userInfo;
}
void setUserInfo(const ServerInfo_User &_userInfo);
void setOnline(bool online);
bool operator<(const QTreeWidgetItem &other) const;
};
class UserList : public QGroupBox
{
Q_OBJECT
public:
enum UserListType
{
AllUsersList,
RoomList,
BuddyList,
IgnoreList
};
private:
QMap<QString, UserListTWI *> users;
TabSupervisor *tabSupervisor;
AbstractClient *client;
UserListType type;
QTreeWidget *userTree;
UserListItemDelegate *itemDelegate;
UserContextMenu *userContextMenu;
int onlineCount;
QString titleStr;
void updateCount();
private slots:
void userClicked(QTreeWidgetItem *item, int column);
signals:
void openMessageDialog(const QString &userName, bool focus);
void addBuddy(const QString &userName);
void removeBuddy(const QString &userName);
void addIgnore(const QString &userName);
void removeIgnore(const QString &userName);
public:
UserList(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, QWidget *parent = nullptr);
void retranslateUi();
void processUserInfo(const ServerInfo_User &user, bool online);
bool deleteUser(const QString &userName);
void setUserOnline(const QString &userName, bool online);
const QMap<QString, UserListTWI *> &getUsers() const
{
return users;
}
void showContextMenu(const QPoint &pos, const QModelIndex &index);
void sortItems();
};
#endif