diff --git a/README.md b/README.md index a94926ad..d1177ca0 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,29 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* +

-- [Cockatrice](#cockatrice) -- [Get Involved [![Gitter chat](https://badges.gitter.im/Cockatrice/Cockatrice.png)](https://gitter.im/Cockatrice/Cockatrice)](#get-involved-) -- [Community Resources](#community-resources) -- [Translation Status [![Cockatrice on Transiflex](https://ds0k0en9abmn1.cloudfront.net/static/charts/images/tx-logo-micro.646b0065fce6.png)](https://www.transifex.com/projects/p/cockatrice/)](#translation-status-) -- [Building [![Build Status](https://travis-ci.org/Cockatrice/Cockatrice.svg?branch=master)](https://travis-ci.org/Cockatrice/Cockatrice)](#building-) -- [Building servatrice Docker container](#building-servatrice-docker-container) -- [Running](#running) -- [License](#license) +--- - +**Table of Contents**    [Cockatrice](#cockatrice) | [Get Involved] (#get-involved-) | [Community](#community-resources) | [Translation](#translation-status-) | [Building](#building-) | [Running](#running) | [License](#license) + +--- # Cockatrice Cockatrice is an open-source multiplatform software for playing card games, such as Magic: The Gathering, over a network. It is fully client-server based to prevent any kind of cheating, though it supports single-player games without -a network interface as well. Both client and server are written in Qt, supporting both Qt4 and Qt5. +a network interface as well. Both client and server are written in Qt, supporting both Qt4 and Qt5.
+ # Get Involved [![Gitter chat](https://badges.gitter.im/Cockatrice/Cockatrice.png)](https://gitter.im/Cockatrice/Cockatrice) -Chat with the Cockatrice developers on Gitter. Come here to talk about the application, features, or just to hang out. For support regarding specific servers, please contact that server's admin or forum for support rather than asking here. +Chat with the Cockatrice developers on Gitter. Come here to talk about the application, features, or just to hang out. For support regarding specific servers, please contact that server's admin or forum for support rather than asking here.
+ # Community Resources -- [reddit r/Cockatrice](http://reddit.com/r/cockatrice) -- [Woogerworks Server & Forums](http://www.woogerworks.com) - [Cockatrice Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) +- [reddit r/Cockatrice](http://reddit.com/r/cockatrice) +- [Woogerworks](http://www.woogerworks.com) / [Chickatrice] (http://www.chickatrice.net/) / [Poixen](http://www.poixen.com/) (incomplete Serverlist)
+ # Translation Status [![Cockatrice on Transiflex](https://ds0k0en9abmn1.cloudfront.net/static/charts/images/tx-logo-micro.646b0065fce6.png)](https://www.transifex.com/projects/p/cockatrice/) @@ -38,26 +34,25 @@ Language statistics for `Cockatrice` *(on the left)* and `Oracle` *(on the right [![Cockatrice translations](https://www.transifex.com/projects/p/cockatrice/resource/cockatrice/chart/image_png)](https://www.transifex.com/projects/p/cockatrice/resource/cockatrice/)      [![Oracle translations](https://www.transifex.com/projects/p/cockatrice/resource/oracle/chart/image_png)](https://www.transifex.com/projects/p/cockatrice/resource/oracle/) -Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information! +Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information!
+ # Building [![Build Status](https://travis-ci.org/Cockatrice/Cockatrice.svg?branch=master)](https://travis-ci.org/Cockatrice/Cockatrice) **Detailed compiling instructions are on the Cockatrice wiki under [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice)** Dependencies: - - [Qt](http://qt-project.org/) - [protobuf](http://code.google.com/p/protobuf/) - [CMake](http://www.cmake.org/) Oracle can optionally use zlib to load zipped files: - - [zlib](http://www.zlib.net/) The server requires an additional dependency when compiled under Qt4: - - [libgcrypt](http://www.gnu.org/software/libgcrypt/) + To compile: mkdir build @@ -75,15 +70,18 @@ The following flags can be passed to `cmake`: - `-DCMAKE_BUILD_TYPE=Debug` Compile in debug mode. Enables extra logging output, debug symbols, and much more verbose compiler warnings. - `-DUPDATE_TRANSLATIONS=1` Configure `make` to update the translation .ts files for new strings in the source code. Note: Running `make clean` will remove the .ts files. -# Building servatrice Docker container -`docker build -t servatrice .` +#### Building servatrice Docker container +`docker build -t servatrice .`
+ # Running `oracle` fetches card data `cockatrice` is the game client -`servatrice` is the server +`servatrice` is the server
+ # License -Cockatrice is free software, licensed under the GPLv2; see COPYING for details. +Cockatrice is free software, licensed under the GPLv2; see COPYING for details.
+ diff --git a/cockatrice/src/dlg_connect.cpp b/cockatrice/src/dlg_connect.cpp index d3bc4027..c97e4e6d 100644 --- a/cockatrice/src/dlg_connect.cpp +++ b/cockatrice/src/dlg_connect.cpp @@ -1,10 +1,14 @@ #include #include #include +#include +#include #include #include #include #include +#include +#include #include #include "dlg_connect.h" @@ -14,8 +18,24 @@ DlgConnect::DlgConnect(QWidget *parent) QSettings settings; settings.beginGroup("server"); + previousHostButton = new QRadioButton(tr("Previous Host"), this); + + previousHosts = new QComboBox(this); + previousHosts->installEventFilter(new DeleteHighlightedItemWhenShiftDelPressedEventFilter); + QStringList previousHostList = settings.value("previoushosts").toStringList(); + if (previousHostList.isEmpty()) { + previousHostList << "cockatrice.woogerworks.com"; + previousHostList << "vps.poixen.com"; + previousHostList << "chickatrice.net"; + } + previousHosts->addItems(previousHostList); + previousHosts->setCurrentIndex(settings.value("previoushostindex").toInt()); + + newHostButton = new QRadioButton(tr("New Host"), this); + hostLabel = new QLabel(tr("&Host:")); - hostEdit = new QLineEdit(settings.value("hostname", "cockatrice.woogerworks.com").toString()); + hostEdit = new QLineEdit(); + hostEdit->setPlaceholderText(tr("Enter host name")); hostLabel->setBuddy(hostEdit); portLabel = new QLabel(tr("&Port:")); @@ -48,16 +68,19 @@ DlgConnect::DlgConnect(QWidget *parent) connect(savePasswordCheckBox, SIGNAL(stateChanged(int)), this, SLOT(passwordSaved(int))); QGridLayout *grid = new QGridLayout; - grid->addWidget(hostLabel, 0, 0); - grid->addWidget(hostEdit, 0, 1); - grid->addWidget(portLabel, 1, 0); - grid->addWidget(portEdit, 1, 1); - grid->addWidget(playernameLabel, 2, 0); - grid->addWidget(playernameEdit, 2, 1); - grid->addWidget(passwordLabel, 3, 0); - grid->addWidget(passwordEdit, 3, 1); - grid->addWidget(savePasswordCheckBox, 4, 0, 1, 2); - grid->addWidget(autoConnectCheckBox, 5, 0, 1, 2); + grid->addWidget(previousHostButton, 0, 1); + grid->addWidget(previousHosts, 1, 1); + grid->addWidget(newHostButton, 2, 1); + grid->addWidget(hostLabel, 3, 0); + grid->addWidget(hostEdit, 3, 1); + grid->addWidget(portLabel, 4, 0); + grid->addWidget(portEdit, 4, 1); + grid->addWidget(playernameLabel, 5, 0); + grid->addWidget(playernameEdit, 5, 1); + grid->addWidget(passwordLabel, 6, 0); + grid->addWidget(passwordEdit, 6, 1); + grid->addWidget(savePasswordCheckBox, 7, 0, 1, 2); + grid->addWidget(autoConnectCheckBox, 8, 0, 1, 2); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, SIGNAL(accepted()), this, SLOT(actOk())); @@ -71,8 +94,32 @@ DlgConnect::DlgConnect(QWidget *parent) setWindowTitle(tr("Connect to server")); setFixedHeight(sizeHint().height()); setMinimumWidth(300); + + connect(previousHostButton, SIGNAL(toggled(bool)), this, SLOT(previousHostSelected(bool))); + connect(newHostButton, SIGNAL(toggled(bool)), this, SLOT(newHostSelected(bool))); + + if (settings.value("previoushostlogin", 1).toInt()) + previousHostButton->setChecked(true); + else + newHostButton->setChecked(true); } + +void DlgConnect::previousHostSelected(bool state) { + if (state) { + hostEdit->setDisabled(true); + previousHosts->setDisabled(false); + } +} + +void DlgConnect::newHostSelected(bool state) { + if (state) { + hostEdit->setDisabled(false); + previousHosts->setDisabled(true); + } +} + + void DlgConnect::passwordSaved(int state) { Q_UNUSED(state); @@ -88,17 +135,34 @@ void DlgConnect::actOk() { QSettings settings; settings.beginGroup("server"); - settings.setValue("hostname", hostEdit->text()); settings.setValue("port", portEdit->text()); settings.setValue("playername", playernameEdit->text()); settings.setValue("password", savePasswordCheckBox->isChecked() ? passwordEdit->text() : QString()); settings.setValue("save_password", savePasswordCheckBox->isChecked() ? 1 : 0); settings.setValue("auto_connect", autoConnectCheckBox->isChecked() ? 1 : 0); + settings.setValue("previoushostlogin", previousHostButton->isChecked() ? 1 : 0); + + QStringList hostList; + if (newHostButton->isChecked()) + if (!hostEdit->text().trimmed().isEmpty()) + hostList << hostEdit->text(); + + for (int i = 0; i < previousHosts->count(); i++) + if(!previousHosts->itemText(i).trimmed().isEmpty()) + hostList << previousHosts->itemText(i); + + settings.setValue("previoushosts", hostList); + settings.setValue("previoushostindex", previousHosts->currentIndex()); settings.endGroup(); accept(); } + +QString DlgConnect::getHost() const { + return previousHostButton->isChecked() ? previousHosts->currentText() : hostEdit->text(); +} + void DlgConnect::actCancel() { QSettings settings; @@ -109,3 +173,17 @@ void DlgConnect::actCancel() reject(); } + + +bool DeleteHighlightedItemWhenShiftDelPressedEventFilter::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Delete) { + QComboBox* combobox = reinterpret_cast(obj); + combobox->removeItem(combobox->currentIndex()); + return true; + } + } + return QObject::eventFilter(obj, event); +} diff --git a/cockatrice/src/dlg_connect.h b/cockatrice/src/dlg_connect.h index 6f135aa0..43cb319e 100644 --- a/cockatrice/src/dlg_connect.h +++ b/cockatrice/src/dlg_connect.h @@ -7,12 +7,22 @@ class QLabel; class QPushButton; class QCheckBox; +class QComboBox; +class QRadioButton; + +class DeleteHighlightedItemWhenShiftDelPressedEventFilter : public QObject +{ + Q_OBJECT +protected: + bool eventFilter(QObject *obj, QEvent *event); +}; + class DlgConnect : public QDialog { Q_OBJECT public: DlgConnect(QWidget *parent = 0); - QString getHost() const { return hostEdit->text(); } + QString getHost() const; int getPort() const { return portEdit->text().toInt(); } QString getPlayerName() const { return playernameEdit->text(); } QString getPassword() const { return passwordEdit->text(); } @@ -20,10 +30,14 @@ private slots: void actOk(); void actCancel(); void passwordSaved(int state); + void previousHostSelected(bool state); + void newHostSelected(bool state); private: QLabel *hostLabel, *portLabel, *playernameLabel, *passwordLabel; QLineEdit *hostEdit, *portEdit, *playernameEdit, *passwordEdit; QCheckBox *savePasswordCheckBox, *autoConnectCheckBox; + QComboBox *previousHosts; + QRadioButton *newHostButton, *previousHostButton; }; #endif diff --git a/cockatrice/src/dlg_settings.cpp b/cockatrice/src/dlg_settings.cpp index 0ea5b5b6..da47aa8d 100644 --- a/cockatrice/src/dlg_settings.cpp +++ b/cockatrice/src/dlg_settings.cpp @@ -574,6 +574,9 @@ MessagesSettingsPage::MessagesSettingsPage() { chatMentionCheckBox.setChecked(settingsCache->getChatMention()); connect(&chatMentionCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setChatMention(int))); + + chatMentionCompleterCheckbox.setChecked(settingsCache->getChatMentionCompleter()); + connect(&chatMentionCompleterCheckbox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setChatMentionCompleter(int))); ignoreUnregUsersMainChat.setChecked(settingsCache->getIgnoreUnregisteredUsers()); ignoreUnregUserMessages.setChecked(settingsCache->getIgnoreUnregisteredUserMessages()); @@ -606,11 +609,12 @@ MessagesSettingsPage::MessagesSettingsPage() chatGrid->addWidget(&chatMentionCheckBox, 0, 0); chatGrid->addWidget(&invertMentionForeground, 0, 1); chatGrid->addWidget(mentionColor, 0, 2); - chatGrid->addWidget(&ignoreUnregUsersMainChat, 1, 0); + chatGrid->addWidget(&chatMentionCompleterCheckbox, 1, 0); + chatGrid->addWidget(&ignoreUnregUsersMainChat, 2, 0); chatGrid->addWidget(&hexLabel, 1, 2); - chatGrid->addWidget(&ignoreUnregUserMessages, 2, 0); - chatGrid->addWidget(&messagePopups, 3, 0); - chatGrid->addWidget(&mentionPopups, 4, 0); + chatGrid->addWidget(&ignoreUnregUserMessages, 3, 0); + chatGrid->addWidget(&messagePopups, 4, 0); + chatGrid->addWidget(&mentionPopups, 5, 0); chatGroupBox = new QGroupBox; chatGroupBox->setLayout(chatGrid); @@ -735,6 +739,7 @@ void MessagesSettingsPage::retranslateUi() chatGroupBox->setTitle(tr("Chat settings")); highlightGroupBox->setTitle(tr("Custom alert words")); chatMentionCheckBox.setText(tr("Enable chat mentions")); + chatMentionCompleterCheckbox.setText(tr("Enable mention completer")); messageShortcuts->setTitle(tr("In-game message macros")); ignoreUnregUsersMainChat.setText(tr("Ignore chat room messages sent by unregistered users")); ignoreUnregUserMessages.setText(tr("Ignore private messages sent by unregistered users")); diff --git a/cockatrice/src/dlg_settings.h b/cockatrice/src/dlg_settings.h index d95ce606..208c9b63 100644 --- a/cockatrice/src/dlg_settings.h +++ b/cockatrice/src/dlg_settings.h @@ -168,6 +168,7 @@ private: QAction *aAdd; QAction *aRemove; QCheckBox chatMentionCheckBox; + QCheckBox chatMentionCompleterCheckbox; QCheckBox invertMentionForeground; QCheckBox invertHighlightForeground; QCheckBox ignoreUnregUsersMainChat; diff --git a/cockatrice/src/localserver.cpp b/cockatrice/src/localserver.cpp index a7eda3d1..613964fd 100644 --- a/cockatrice/src/localserver.cpp +++ b/cockatrice/src/localserver.cpp @@ -33,7 +33,7 @@ ServerInfo_User LocalServer_DatabaseInterface::getUserData(const QString &name, return result; } -AuthenticationResult LocalServer_DatabaseInterface::checkUserPassword(Server_ProtocolHandler * /* handler */, const QString & /* user */, const QString & /* password */, QString & /* reasonStr */, int & /* secondsLeft */) +AuthenticationResult LocalServer_DatabaseInterface::checkUserPassword(Server_ProtocolHandler * /* handler */, const QString & /* user */, const QString & /* password */, const QString & /* clientId */, QString & /* reasonStr */, int & /* secondsLeft */) { return UnknownUser; } diff --git a/cockatrice/src/localserver.h b/cockatrice/src/localserver.h index 276ebf90..221318c7 100644 --- a/cockatrice/src/localserver.h +++ b/cockatrice/src/localserver.h @@ -24,7 +24,7 @@ protected: ServerInfo_User getUserData(const QString &name, bool withId = false); public: LocalServer_DatabaseInterface(LocalServer *_localServer); - AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, QString &reasonStr, int &secondsLeft); + AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, const QString &clientId, QString &reasonStr, int &secondsLeft); int getNextGameId() { return localServer->getNextLocalGameId(); } int getNextReplayId() { return -1; } int getActiveUserCount() { return 0; } diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index cdeee600..5bd07ef9 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -222,8 +222,8 @@ int main(int argc, char *argv[]) settingsCache->setClientID(generateClientID()); qDebug() << "ClientID In Cache: " << settingsCache->getClientID(); - ui.showMaximized(); - qDebug("main(): ui.showMaximized() finished"); + ui.show(); + qDebug("main(): ui.show() finished"); app.exec(); } diff --git a/cockatrice/src/remoteclient.cpp b/cockatrice/src/remoteclient.cpp index 68122d2a..96ee1eea 100644 --- a/cockatrice/src/remoteclient.cpp +++ b/cockatrice/src/remoteclient.cpp @@ -11,6 +11,7 @@ #include "pb/server_message.pb.h" #include "pb/event_server_identification.pb.h" #include "settingscache.h" +#include "main.h" static const unsigned int protocolVersion = 14; @@ -78,6 +79,7 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica cmdRegister.set_gender((ServerInfo_User_Gender) gender); cmdRegister.set_country(country.toStdString()); cmdRegister.set_real_name(realName.toStdString()); + cmdRegister.set_clientid(settingsCache->getClientID().toStdString()); PendingCommand *pend = prepareSessionCommand(cmdRegister); connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(registerResponse(Response))); diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index 33141680..462001c6 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -78,6 +78,7 @@ SettingsCache::SettingsCache() minPlayersForMultiColumnLayout = settings->value("interface/min_players_multicolumn", 5).toInt(); tapAnimation = settings->value("cards/tapanimation", true).toBool(); chatMention = settings->value("chat/mention", true).toBool(); + chatMentionCompleter = settings->value("chat/mentioncompleter", true).toBool(); chatMentionForeground = settings->value("chat/mentionforeground", true).toBool(); chatHighlightForeground = settings->value("chat/highlightforeground", true).toBool(); chatMentionColor = settings->value("chat/mentioncolor", "A6120D").toString(); @@ -361,6 +362,13 @@ void SettingsCache::setChatMention(int _chatMention) { settings->setValue("chat/mention", chatMention); } +void SettingsCache::setChatMentionCompleter(const int _enableMentionCompleter) +{ + chatMentionCompleter = _enableMentionCompleter; + settings->setValue("chat/mentioncompleter", chatMentionCompleter); + emit chatMentionCompleterChanged(); +} + void SettingsCache::setChatMentionForeground(int _chatMentionForeground) { chatMentionForeground = _chatMentionForeground; settings->setValue("chat/mentionforeground", chatMentionForeground); diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index bb275cbb..f8227efc 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -44,6 +44,7 @@ signals: void ignoreUnregisteredUserMessagesChanged(); void pixmapCacheSizeChanged(int newSizeInMBs); void masterVolumeChanged(int value); + void chatMentionCompleterChanged(); private: QSettings *settings; ShortcutsSettings *shortcutsSettings; @@ -66,6 +67,7 @@ private: int minPlayersForMultiColumnLayout; bool tapAnimation; bool chatMention; + bool chatMentionCompleter; QString chatMentionColor; QString chatHighlightColor; bool chatMentionForeground; @@ -137,6 +139,7 @@ public: int getMinPlayersForMultiColumnLayout() const { return minPlayersForMultiColumnLayout; } bool getTapAnimation() const { return tapAnimation; } bool getChatMention() const { return chatMention; } + bool getChatMentionCompleter() const { return chatMentionCompleter; } bool getChatMentionForeground() const { return chatMentionForeground; } bool getChatHighlightForeground() const { return chatHighlightForeground; } bool getZoneViewSortByName() const { return zoneViewSortByName; } @@ -220,6 +223,7 @@ public slots: void setMinPlayersForMultiColumnLayout(int _minPlayersForMultiColumnLayout); void setTapAnimation(int _tapAnimation); void setChatMention(int _chatMention); + void setChatMentionCompleter(int _chatMentionCompleter); void setChatMentionForeground(int _chatMentionForeground); void setChatHighlightForeground(int _chatHighlightForeground); void setZoneViewSortByName(int _zoneViewSortByName); diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/tab_deck_editor.cpp index 6e5f9270..a884dd3c 100644 --- a/cockatrice/src/tab_deck_editor.cpp +++ b/cockatrice/src/tab_deck_editor.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include "tab_deck_editor.h" #include "window_sets.h" #include "carddatabase.h" @@ -363,6 +365,7 @@ void TabDeckEditor::createMenus() #if defined(Q_OS_WIN) || defined(Q_OS_MAC) dbMenu->addSeparator(); dbMenu->addAction(aOpenCustomFolder); + dbMenu->addAction(aOpenCustomsetsFolder); #endif addTabMenu(dbMenu); } @@ -573,6 +576,7 @@ void TabDeckEditor::retranslateUi() aPrintDeck->setText(tr("&Print deck...")); aAnalyzeDeck->setText(tr("&Analyze deck on deckstats.net")); aOpenCustomFolder->setText(tr("Open custom image folder")); + aOpenCustomsetsFolder->setText(tr("Open custom sets folder")); aClose->setText(tr("&Close")); aClose->setShortcuts(settingsCache->shortcuts().getShortcut( "TabDeckEditor/aClose", @@ -825,6 +829,33 @@ void TabDeckEditor::actOpenCustomFolder() { } +void TabDeckEditor::actOpenCustomsetsFolder() { +#if QT_VERSION < 0x050000 + QString dataDir = QDesktopServices::storageLocation(QDesktopServices::DataLocation); +#else + QString dataDir = QStandardPaths::standardLocations(QStandardPaths::DataLocation).first(); +#endif + +#if defined(Q_OS_MAC) + + QStringList scriptArgs; + scriptArgs << QLatin1String("-e"); + scriptArgs << QString::fromLatin1("tell application \"Finder\" to open POSIX file \"%1\"").arg(dataDir + "/customsets/"); + scriptArgs << QLatin1String("-e"); + scriptArgs << QLatin1String("tell application \"Finder\" to activate"); + + QProcess::execute("/usr/bin/osascript", scriptArgs); +#endif +#if defined(Q_OS_WIN) + QStringList args; + dataDir.append("/customsets"); + args << QDir::toNativeSeparators(dataDir); + aOpenCustomsetsFolder->setText(dataDir); + QProcess::startDetached("explorer", args); +#endif + +} + void TabDeckEditor::actEditSets() { WndSets *w = new WndSets; diff --git a/cockatrice/src/tab_deck_editor.h b/cockatrice/src/tab_deck_editor.h index 1398bff8..19591e3f 100644 --- a/cockatrice/src/tab_deck_editor.h +++ b/cockatrice/src/tab_deck_editor.h @@ -54,6 +54,7 @@ class TabDeckEditor : public Tab { void actPrintDeck(); void actAnalyzeDeck(); void actOpenCustomFolder(); + void actOpenCustomsetsFolder(); void actEditSets(); void actEditTokens(); @@ -112,7 +113,7 @@ private: QWidget *filterBox; QMenu *deckMenu, *dbMenu; - QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard, *aPrintDeck, *aAnalyzeDeck, *aClose, *aOpenCustomFolder; + QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard, *aPrintDeck, *aAnalyzeDeck, *aClose, *aOpenCustomFolder, *aOpenCustomsetsFolder; QAction *aEditSets, *aEditTokens, *aClearFilterAll, *aClearFilterOne; QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement;// *aUpdatePrices; QAction *aResetLayout; diff --git a/cockatrice/src/tab_room.cpp b/cockatrice/src/tab_room.cpp index 53e4de7f..b87772a0 100644 --- a/cockatrice/src/tab_room.cpp +++ b/cockatrice/src/tab_room.cpp @@ -11,6 +11,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include "tab_supervisor.h" #include "tab_room.h" #include "tab_userlists.h" @@ -51,8 +59,9 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, AbstractClient *_client, ServerI connect(chatView, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString))); connect(chatView, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString))); connect(chatView, SIGNAL(addMentionTag(QString)), this, SLOT(addMentionTag(QString))); + connect(settingsCache, SIGNAL(chatMentionCompleterChanged()), this, SLOT(actCompleterChanged())); sayLabel = new QLabel; - sayEdit = new QLineEdit; + sayEdit = new CustomLineEdit; sayLabel->setBuddy(sayEdit); connect(sayEdit, SIGNAL(returnPressed()), this, SLOT(sendMessage())); @@ -103,13 +112,26 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, AbstractClient *_client, ServerI setLayout(hbox); const int userListSize = info.user_list_size(); - for (int i = 0; i < userListSize; ++i) + for (int i = 0; i < userListSize; ++i){ userList->processUserInfo(info.user_list(i), true); + autocompleteUserList.append("@" + QString::fromStdString(info.user_list(i).name())); + } userList->sortItems(); const int gameListSize = info.game_list_size(); for (int i = 0; i < gameListSize; ++i) gameSelector->processGameInfo(info.game_list(i)); + + completer = new QCompleter(autocompleteUserList, sayEdit); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setMaxVisibleItems(5); + + #if QT_VERSION >= 0x050000 + completer->setFilterMode(Qt::MatchStartsWith); + #endif + + sayEdit->setCompleter(completer); + actCompleterChanged(); } TabRoom::~TabRoom() @@ -119,7 +141,7 @@ TabRoom::~TabRoom() void TabRoom::retranslateUi() { - gameSelector->retranslateUi(); + gameSelector->retranslateUi(); chatView->retranslateUi(); userList->retranslateUi(); sayLabel->setText(tr("&Say:")); @@ -166,16 +188,20 @@ QString TabRoom::sanitizeHtml(QString dirty) const void TabRoom::sendMessage() { - if (sayEdit->text().isEmpty()) - return; + if (sayEdit->text().isEmpty()){ + return; + }else if (completer->popup()->isVisible()){ + completer->popup()->hide(); + return; + }else{ + Command_RoomSay cmd; + cmd.set_message(sayEdit->text().toStdString()); - Command_RoomSay cmd; - cmd.set_message(sayEdit->text().toStdString()); - - PendingCommand *pend = prepareRoomCommand(cmd); - connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(sayFinished(const Response &))); - sendRoomCommand(pend); - sayEdit->clear(); + PendingCommand *pend = prepareRoomCommand(cmd); + connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(sayFinished(const Response &))); + sendRoomCommand(pend); + sayEdit->clear(); + } } void TabRoom::sayFinished(const Response &response) @@ -200,6 +226,11 @@ void TabRoom::actOpenChatSettings() { settings.exec(); } +void TabRoom::actCompleterChanged() +{ + settingsCache->getChatMentionCompleter() ? completer->setCompletionRole(2) : completer->setCompletionRole(1); +} + void TabRoom::processRoomEvent(const RoomEvent &event) { switch (static_cast(getPbExtension(event))) { @@ -222,11 +253,17 @@ void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) { userList->processUserInfo(event.user_info(), true); userList->sortItems(); + if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))){ + autocompleteUserList << "@" + QString::fromStdString(event.user_info().name()); + sayEdit->updateCompleterModel(autocompleteUserList); + } } void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event) { userList->deleteUser(QString::fromStdString(event.name())); + autocompleteUserList.removeOne("@" + QString::fromStdString(event.name())); + sayEdit->updateCompleterModel(autocompleteUserList); } void TabRoom::processRoomSayEvent(const Event_RoomSay &event) @@ -259,3 +296,124 @@ void TabRoom::sendRoomCommand(PendingCommand *pend) { client->sendCommand(pend); } + +CustomLineEdit::CustomLineEdit(QWidget *parent) + : QLineEdit(parent) +{ +} + +void CustomLineEdit::focusOutEvent(QFocusEvent * e){ + QLineEdit::focusOutEvent(e); + if (c->popup()->isVisible()){ + //Remove Popup + c->popup()->hide(); + //Truncate the line to last space or whole string + QString textValue = text(); + int lastIndex = textValue.length(); + int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; + int leftShift = qMin(lastIndex, lastWordStartIndex); + setText(textValue.left(leftShift)); + //Insert highlighted line from popup + insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); + //Set focus back to the textbox since tab was pressed + setFocus(); + } +} + +void CustomLineEdit::keyPressEvent(QKeyEvent * event) +{ + switch (event->key()){ + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Escape: + if (c->popup()->isVisible()){ + event->ignore(); + //Remove Popup + c->popup()->hide(); + //Truncate the line to last space or whole string + QString textValue = text(); + int lastIndexof = textValue.lastIndexOf(" "); + QString finalString = textValue.left(lastIndexof); + //Add a space if there's a word + if (finalString != "") + finalString += " "; + setText(finalString); + return; + } + break; + case Qt::Key_Space: + if (c->popup()->isVisible()){ + event->ignore(); + //Remove Popup + c->popup()->hide(); + //Truncate the line to last space or whole string + QString textValue = text(); + int lastIndex = textValue.length(); + int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; + int leftShift = qMin(lastIndex, lastWordStartIndex); + setText(textValue.left(leftShift)); + //Insert highlighted line from popup + insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); + return; + } + break; + default: + break; + } + + QLineEdit::keyPressEvent(event); + //Wait until the first character after @ + if (!c || text().right(1).contains("@")) + return; + + //Set new completion prefix + c->setCompletionPrefix(cursorWord(text())); + if (c->completionPrefix().length() < 1){ + c->popup()->hide(); + return; + } + + //Draw completion box + QRect cr = cursorRect(); + cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); + c->complete(cr); + + //Select first item in the completion popup + QItemSelectionModel* sm = new QItemSelectionModel(c->completionModel()); + c->popup()->setSelectionModel(sm); + sm->select(c->completionModel()->index(0, 0), QItemSelectionModel::ClearAndSelect); + sm->setCurrentIndex(c->completionModel()->index(0, 0), QItemSelectionModel::NoUpdate); +} + +QString CustomLineEdit::cursorWord(const QString &line) const +{ + return line.mid(line.left(cursorPosition()).lastIndexOf(" ") + 1, + cursorPosition() - line.left(cursorPosition()).lastIndexOf(" ") - 1); +} + +void CustomLineEdit::insertCompletion(QString arg) +{ + QString s_arg = arg + " "; + setText(text().replace(text().left(cursorPosition()).lastIndexOf(" ") + 1, + cursorPosition() - text().left(cursorPosition()).lastIndexOf(" ") - 1, s_arg)); +} + +void CustomLineEdit::setCompleter(QCompleter* completer) +{ + c = completer; + c->setWidget(this); + connect(c, SIGNAL(activated(QString)),this, SLOT(insertCompletion(QString))); +} + +void CustomLineEdit::updateCompleterModel(QStringList completionList) +{ + if (!c || c->popup()->isVisible()) + return; + + QStringListModel *model; + model = (QStringListModel*)(c->model()); + if (model == NULL) + model = new QStringListModel(); + QStringList updatedList = completionList; + model->setStringList(updatedList); +} \ No newline at end of file diff --git a/cockatrice/src/tab_room.h b/cockatrice/src/tab_room.h index 377f357f..98a74d8f 100644 --- a/cockatrice/src/tab_room.h +++ b/cockatrice/src/tab_room.h @@ -4,6 +4,9 @@ #include "tab.h" #include #include +#include +#include +#include namespace google { namespace protobuf { class Message; } } class AbstractClient; @@ -13,6 +16,7 @@ class ChatView; class QLineEdit; class QPushButton; class QTextTable; +class QCompleter; class RoomEvent; class ServerInfo_Room; class ServerInfo_Game; @@ -24,6 +28,7 @@ class GameSelector; class Response; class PendingCommand; class ServerInfo_User; +class CustomLineEdit; class TabRoom : public Tab { Q_OBJECT @@ -38,14 +43,17 @@ private: UserList *userList; ChatView *chatView; QLabel *sayLabel; - QLineEdit *sayEdit; + CustomLineEdit *sayEdit; QGroupBox *chatGroupBox; QMenu *roomMenu; QAction *aLeaveRoom; QAction *aOpenChatSettings; - QAction * aClearChat; + QAction *aClearChat; QString sanitizeHtml(QString dirty) const; + + QStringList autocompleteUserList; + QCompleter *completer; signals: void roomClosing(TabRoom *tab); void openMessageDialog(const QString &userName, bool focus); @@ -59,7 +67,8 @@ private slots: void addMentionTag(QString mentionTag); void focusTab(); void actShowMentionPopup(QString &sender); - + void actCompleterChanged(); + void processListGamesEvent(const Event_ListGames &event); void processJoinRoomEvent(const Event_JoinRoom &event); void processLeaveRoomEvent(const Event_LeaveRoom &event); @@ -81,4 +90,21 @@ public: void sendRoomCommand(PendingCommand *pend); }; +class CustomLineEdit : public QLineEdit +{ + Q_OBJECT +private: + QString cursorWord(const QString& line) const; + QCompleter* c; +private slots: + void insertCompletion(QString); +protected: + void keyPressEvent(QKeyEvent * event); + void focusOutEvent(QFocusEvent * e); +public: + explicit CustomLineEdit(QWidget *parent = 0); + void setCompleter(QCompleter*); + void updateCompleterModel(QStringList); +}; + #endif diff --git a/cockatrice/src/user_context_menu.cpp b/cockatrice/src/user_context_menu.cpp index ace5a44b..758f99e1 100644 --- a/cockatrice/src/user_context_menu.cpp +++ b/cockatrice/src/user_context_menu.cpp @@ -99,6 +99,7 @@ void UserContextMenu::banUser_dialogFinished() cmd.set_minutes(dlg->getMinutes()); cmd.set_reason(dlg->getReason().toStdString()); cmd.set_visible_reason(dlg->getVisibleReason().toStdString()); + cmd.set_clientid(dlg->getBanId().toStdString()); client->sendCommand(client->prepareModeratorCommand(cmd)); } diff --git a/cockatrice/src/userlist.cpp b/cockatrice/src/userlist.cpp index 6afbc610..59717608 100644 --- a/cockatrice/src/userlist.cpp +++ b/cockatrice/src/userlist.cpp @@ -36,11 +36,19 @@ BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) ipBanCheckBox = new QCheckBox(tr("ban &IP address")); ipBanCheckBox->setChecked(true); ipBanEdit = new QLineEdit(QString::fromStdString(info.address())); + idBanCheckBox = new QCheckBox(tr("ban client I&D")); + idBanCheckBox->setChecked(true); + idBanEdit = new QLineEdit(QString::fromStdString(info.clientid())); + 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); @@ -110,8 +118,8 @@ BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) void BanDialog::okClicked() { - if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked()) { - QMessageBox::critical(this, tr("Error"), tr("You have to select a name-based or IP-based ban, or both.")); + 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; } accept(); @@ -127,6 +135,11 @@ void BanDialog::enableTemporaryEdits(bool enabled) minutesEdit->setEnabled(enabled); } +QString BanDialog::getBanId() const +{ + return idBanCheckBox->isChecked() ? idBanEdit->text() : QString(); +} + QString BanDialog::getBanName() const { return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString(); diff --git a/cockatrice/src/userlist.h b/cockatrice/src/userlist.h index fc043b1d..3ac47a08 100644 --- a/cockatrice/src/userlist.h +++ b/cockatrice/src/userlist.h @@ -24,8 +24,8 @@ class BanDialog : public QDialog { Q_OBJECT private: QLabel *daysLabel, *hoursLabel, *minutesLabel; - QCheckBox *nameBanCheckBox, *ipBanCheckBox; - QLineEdit *nameBanEdit, *ipBanEdit; + QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox; + QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit; QSpinBox *daysEdit, *hoursEdit, *minutesEdit; QRadioButton *permanentRadio, *temporaryRadio; QPlainTextEdit *reasonEdit, *visibleReasonEdit; @@ -36,6 +36,7 @@ public: BanDialog(const ServerInfo_User &info, QWidget *parent = 0); QString getBanName() const; QString getBanIP() const; + QString getBanId() const; int getMinutes() const; QString getReason() const; QString getVisibleReason() const; diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index 25198109..ae5edf3e 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -319,6 +319,9 @@ void MainWindow::loginError(Response::ResponseCode r, QString reasonStr, quint32 case Response::RespClientIdRequired: QMessageBox::critical(this, tr("Error"), tr("This server requires client ID's. Your client is either failing to generate an ID or you are running a modified client.\nPlease close and reopen your client to try again.")); break; + case Response::RespContextError: + QMessageBox::critical(this, tr("Error"), tr("An internal error has occurred, please try closing and reopening your client and try again. If the error persists try updating your client to the most recent build and if need be contact your software provider.")); + break; case Response::RespAccountNotActivated: { bool ok = false; QString token = QInputDialog::getText(this, tr("Account activation"), tr("Your account has not been activated yet.\nYou need to provide the activation token received in the activation email"), QLineEdit::Normal, QString(), &ok); diff --git a/common/pb/moderator_commands.proto b/common/pb/moderator_commands.proto index 18324ef1..5fe8a847 100644 --- a/common/pb/moderator_commands.proto +++ b/common/pb/moderator_commands.proto @@ -14,4 +14,5 @@ message Command_BanFromServer { optional uint32 minutes = 3; optional string reason = 4; optional string visible_reason = 5; + optional string clientid = 6; } diff --git a/common/pb/serverinfo_user.proto b/common/pb/serverinfo_user.proto index e93d09fe..71e5849e 100644 --- a/common/pb/serverinfo_user.proto +++ b/common/pb/serverinfo_user.proto @@ -23,4 +23,5 @@ message ServerInfo_User { optional uint64 session_id = 10; optional uint64 accountage_secs = 11; optional string email = 12; + optional string clientid = 13; } diff --git a/common/pb/session_commands.proto b/common/pb/session_commands.proto index fda1d8bc..d5564f3e 100644 --- a/common/pb/session_commands.proto +++ b/common/pb/session_commands.proto @@ -119,6 +119,7 @@ message Command_Register { // Country code of the user. 2 letter ISO format optional string country = 5; optional string real_name = 6; + optional string clientid = 7; } // User wants to activate an account diff --git a/common/server.cpp b/common/server.cpp index e07db95e..2fb064bc 100644 --- a/common/server.cpp +++ b/common/server.cpp @@ -46,7 +46,7 @@ Server::Server(bool _threaded, QObject *parent) qRegisterMetaType("GameEventContainer"); qRegisterMetaType("IslMessage"); qRegisterMetaType("Command_JoinGame"); - + connect(this, SIGNAL(sigSendIslMessage(IslMessage, int)), this, SLOT(doSendIslMessage(IslMessage, int)), Qt::QueuedConnection); } @@ -62,9 +62,9 @@ void Server::prepareDestroy() for (int i = 0; i < clients.size(); ++i) QMetaObject::invokeMethod(clients.at(i), "prepareDestroy", Qt::QueuedConnection); clientsLock.unlock(); - + bool done = false; - + class SleeperThread : public QThread { public: @@ -83,7 +83,7 @@ void Server::prepareDestroy() while (!clients.isEmpty()) clients.first()->prepareDestroy(); } - + roomsLock.lockForWrite(); QMapIterator roomIterator(rooms); while (roomIterator.hasNext()) @@ -107,21 +107,21 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString { if (name.size() > 35) name = name.left(35); - + Server_DatabaseInterface *databaseInterface = getDatabaseInterface(); - + QWriteLocker locker(&clientsLock); - - AuthenticationResult authState = databaseInterface->checkUserPassword(session, name, password, reasonStr, secondsLeft); + + AuthenticationResult authState = databaseInterface->checkUserPassword(session, name, password, clientid, reasonStr, secondsLeft); if (authState == NotLoggedIn || authState == UserIsBanned || authState == UsernameInvalid || authState == UserIsInactive) return authState; - + ServerInfo_User data = databaseInterface->getUserData(name, true); data.set_address(session->getAddress().toStdString()); name = QString::fromStdString(data.name()); // Compensate for case indifference - + databaseInterface->lockSessionTables(); - + if (authState == PasswordRight) { // verify that new session would not cause problems with older existing session @@ -133,8 +133,7 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString } else if (authState == UnknownUser) { // Change user name so that no two users have the same names, // don't interfere with registered user names though. - bool requireReg = databaseInterface->getRequireRegistration(); - if (requireReg) { + if (getRegOnlyServer()) { qDebug("Login denied: registration required"); databaseInterface->unlockSessionTables(); return RegistrationRequired; @@ -147,18 +146,18 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString name = tempName; data.set_name(name.toStdString()); } - + users.insert(name, session); qDebug() << "Server::loginUser:" << session << "name=" << name; - - data.set_session_id(databaseInterface->startSession(name, session->getAddress())); + + data.set_session_id(databaseInterface->startSession(name, session->getAddress(), clientid)); databaseInterface->unlockSessionTables(); - + usersBySessionId.insert(data.session_id(), session); - + qDebug() << "session id:" << data.session_id(); session->setUserInfo(data); - + Event_UserJoined event; event.mutable_user_info()->CopyFrom(session->copyUserInfo(false)); SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); @@ -166,10 +165,10 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString if (clients[i]->getAcceptsUserListChanges()) clients[i]->sendProtocolItem(*se); delete se; - + event.mutable_user_info()->CopyFrom(session->copyUserInfo(true, true, true)); locker.unlock(); - + if (clientid.isEmpty()){ // client id is empty, either out dated client or client has been modified if (getClientIdRequired()) @@ -180,10 +179,11 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, QString databaseInterface->updateUsersClientID(name, clientid); } + databaseInterface->updateUsersLastLoginTime(name); se = Server_ProtocolHandler::prepareSessionEvent(event); sendIsl_SessionEvent(*se); delete se; - + return authState; } @@ -208,7 +208,7 @@ QList Server::getPersistentPlayerReferences(const QString &user Server_AbstractUserInterface *Server::findUser(const QString &userName) const { // Call this only with clientsLock set. - + Server_AbstractUserInterface *userHandler = users.value(userName); if (userHandler) return userHandler; @@ -236,10 +236,10 @@ void Server::removeClient(Server_ProtocolHandler *client) clients[i]->sendProtocolItem(*se); sendIsl_SessionEvent(*se); delete se; - + users.remove(QString::fromStdString(data->name())); qDebug() << "Server::removeClient: name=" << QString::fromStdString(data->name()); - + if (data->has_session_id()) { const qint64 sessionId = data->session_id(); usersBySessionId.remove(sessionId); @@ -254,21 +254,21 @@ void Server::externalUserJoined(const ServerInfo_User &userInfo) { // This function is always called from the main thread via signal/slot. clientsLock.lockForWrite(); - + Server_RemoteUserInterface *newUser = new Server_RemoteUserInterface(this, ServerInfo_User_Container(userInfo)); externalUsers.insert(QString::fromStdString(userInfo.name()), newUser); externalUsersBySessionId.insert(userInfo.session_id(), newUser); - + Event_UserJoined event; event.mutable_user_info()->CopyFrom(userInfo); - + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); for (int i = 0; i < clients.size(); ++i) if (clients[i]->getAcceptsUserListChanges()) clients[i]->sendProtocolItem(*se); delete se; clientsLock.unlock(); - + ResponseContainer rc(-1); newUser->joinPersistentGames(rc); newUser->sendResponseContainer(rc, Response::RespNothing); @@ -277,12 +277,12 @@ void Server::externalUserJoined(const ServerInfo_User &userInfo) void Server::externalUserLeft(const QString &userName) { // This function is always called from the main thread via signal/slot. - + clientsLock.lockForWrite(); Server_AbstractUserInterface *user = externalUsers.take(userName); externalUsersBySessionId.remove(user->getUserInfo()->session_id()); clientsLock.unlock(); - + QMap > userGames(user->getGames()); QMapIterator > userGamesIterator(userGames); roomsLock.lockForRead(); @@ -291,26 +291,26 @@ void Server::externalUserLeft(const QString &userName) Server_Room *room = rooms.value(userGamesIterator.value().first); if (!room) continue; - + QReadLocker roomGamesLocker(&room->gamesLock); Server_Game *game = room->getGames().value(userGamesIterator.key()); if (!game) continue; - + QMutexLocker gameLocker(&game->gameMutex); Server_Player *player = game->getPlayers().value(userGamesIterator.value().second); if (!player) continue; - + player->disconnectClient(); } roomsLock.unlock(); - + delete user; - + Event_UserLeft event; event.set_name(userName.toStdString()); - + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); clientsLock.lockForRead(); for (int i = 0; i < clients.size(); ++i) @@ -324,7 +324,7 @@ void Server::externalRoomUserJoined(int roomId, const ServerInfo_User &userInfo) { // This function is always called from the main thread via signal/slot. QReadLocker locker(&roomsLock); - + Server_Room *room = rooms.value(roomId); if (!room) { qDebug() << "externalRoomUserJoined: room id=" << roomId << "not found"; @@ -337,7 +337,7 @@ void Server::externalRoomUserLeft(int roomId, const QString &userName) { // This function is always called from the main thread via signal/slot. QReadLocker locker(&roomsLock); - + Server_Room *room = rooms.value(roomId); if (!room) { qDebug() << "externalRoomUserLeft: room id=" << roomId << "not found"; @@ -350,7 +350,7 @@ void Server::externalRoomSay(int roomId, const QString &userName, const QString { // This function is always called from the main thread via signal/slot. QReadLocker locker(&roomsLock); - + Server_Room *room = rooms.value(roomId); if (!room) { qDebug() << "externalRoomSay: room id=" << roomId << "not found"; @@ -365,7 +365,7 @@ void Server::externalRoomGameListChanged(int roomId, const ServerInfo_Game &game { // This function is always called from the main thread via signal/slot. QReadLocker locker(&roomsLock); - + Server_Room *room = rooms.value(roomId); if (!room) { qDebug() << "externalRoomGameListChanged: room id=" << roomId << "not found"; @@ -377,11 +377,11 @@ void Server::externalRoomGameListChanged(int roomId, const ServerInfo_Game &game void Server::externalJoinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId) { // This function is always called from the main thread via signal/slot. - + try { QReadLocker roomsLocker(&roomsLock); QReadLocker clientsLocker(&clientsLock); - + Server_Room *room = rooms.value(roomId); if (!room) { qDebug() << "externalJoinGameCommandReceived: room id=" << roomId << "not found"; @@ -392,7 +392,7 @@ void Server::externalJoinGameCommandReceived(const Command_JoinGame &cmd, int cm qDebug() << "externalJoinGameCommandReceived: session id=" << sessionId << "not found"; throw Response::RespNotInRoom; } - + ResponseContainer responseContainer(cmdId); Response::ResponseCode responseCode = room->processJoinGameCommand(cmd, responseContainer, userInterface); userInterface->sendResponseContainer(responseContainer, responseCode); @@ -400,7 +400,7 @@ void Server::externalJoinGameCommandReceived(const Command_JoinGame &cmd, int cm Response response; response.set_cmd_id(cmdId); response.set_response_code(code); - + sendIsl_Response(response, serverId, sessionId); } } @@ -408,44 +408,44 @@ void Server::externalJoinGameCommandReceived(const Command_JoinGame &cmd, int cm void Server::externalGameCommandContainerReceived(const CommandContainer &cont, int playerId, int serverId, qint64 sessionId) { // This function is always called from the main thread via signal/slot. - + try { ResponseContainer responseContainer(cont.cmd_id()); Response::ResponseCode finalResponseCode = Response::RespOk; - + QReadLocker roomsLocker(&roomsLock); Server_Room *room = rooms.value(cont.room_id()); if (!room) { qDebug() << "externalGameCommandContainerReceived: room id=" << cont.room_id() << "not found"; throw Response::RespNotInRoom; } - + QReadLocker roomGamesLocker(&room->gamesLock); Server_Game *game = room->getGames().value(cont.game_id()); if (!game) { qDebug() << "externalGameCommandContainerReceived: game id=" << cont.game_id() << "not found"; throw Response::RespNotInRoom; } - + QMutexLocker gameLocker(&game->gameMutex); Server_Player *player = game->getPlayers().value(playerId); if (!player) { qDebug() << "externalGameCommandContainerReceived: player id=" << playerId << "not found"; throw Response::RespNotInRoom; } - + GameEventStorage ges; for (int i = cont.game_command_size() - 1; i >= 0; --i) { const GameCommand &sc = cont.game_command(i); qDebug() << "[ISL]" << QString::fromStdString(sc.ShortDebugString()); - + Response::ResponseCode resp = player->processGameCommand(sc, responseContainer, ges); - + if (resp != Response::RespOk) finalResponseCode = resp; } ges.sendToGame(game); - + if (finalResponseCode != Response::RespNothing) { player->playerMutex.lock(); player->getUserInterface()->sendResponseContainer(responseContainer, finalResponseCode); @@ -455,7 +455,7 @@ void Server::externalGameCommandContainerReceived(const CommandContainer &cont, Response response; response.set_cmd_id(cont.cmd_id()); response.set_response_code(code); - + sendIsl_Response(response, serverId, sessionId); } } @@ -463,9 +463,9 @@ void Server::externalGameCommandContainerReceived(const CommandContainer &cont, void Server::externalGameEventContainerReceived(const GameEventContainer &cont, qint64 sessionId) { // This function is always called from the main thread via signal/slot. - + QReadLocker usersLocker(&clientsLock); - + Server_ProtocolHandler *client = usersBySessionId.value(sessionId); if (!client) { qDebug() << "externalGameEventContainerReceived: session" << sessionId << "not found"; @@ -477,9 +477,9 @@ void Server::externalGameEventContainerReceived(const GameEventContainer &cont, void Server::externalResponseReceived(const Response &resp, qint64 sessionId) { // This function is always called from the main thread via signal/slot. - + QReadLocker usersLocker(&clientsLock); - + Server_ProtocolHandler *client = usersBySessionId.value(sessionId); if (!client) { qDebug() << "externalResponseReceived: session" << sessionId << "not found"; @@ -491,10 +491,10 @@ void Server::externalResponseReceived(const Response &resp, qint64 sessionId) void Server::broadcastRoomUpdate(const ServerInfo_Room &roomInfo, bool sendToIsl) { // This function is always called from the main thread via signal/slot. - + Event_ListRooms event; event.add_room_list()->CopyFrom(roomInfo); - + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); clientsLock.lockForRead(); @@ -502,10 +502,10 @@ void Server::broadcastRoomUpdate(const ServerInfo_Room &roomInfo, bool sendToIsl if (clients[i]->getAcceptsRoomListChanges()) clients[i]->sendProtocolItem(*se); clientsLock.unlock(); - + if (sendToIsl) sendIsl_SessionEvent(*se); - + delete se; } @@ -543,7 +543,7 @@ void Server::sendIsl_Response(const Response &item, int serverId, qint64 session if (sessionId != -1) msg.set_session_id(sessionId); msg.mutable_response()->CopyFrom(item); - + emit sigSendIslMessage(msg, serverId); } @@ -554,7 +554,7 @@ void Server::sendIsl_SessionEvent(const SessionEvent &item, int serverId, qint64 if (sessionId != -1) msg.set_session_id(sessionId); msg.mutable_session_event()->CopyFrom(item); - + emit sigSendIslMessage(msg, serverId); } @@ -565,7 +565,7 @@ void Server::sendIsl_GameEventContainer(const GameEventContainer &item, int serv if (sessionId != -1) msg.set_session_id(sessionId); msg.mutable_game_event_container()->CopyFrom(item); - + emit sigSendIslMessage(msg, serverId); } @@ -576,7 +576,7 @@ void Server::sendIsl_RoomEvent(const RoomEvent &item, int serverId, qint64 sessi if (sessionId != -1) msg.set_session_id(sessionId); msg.mutable_room_event()->CopyFrom(item); - + emit sigSendIslMessage(msg, serverId); } @@ -586,11 +586,11 @@ void Server::sendIsl_GameCommand(const CommandContainer &item, int serverId, qin msg.set_message_type(IslMessage::GAME_COMMAND_CONTAINER); msg.set_session_id(sessionId); msg.set_player_id(playerId); - + CommandContainer *cont = msg.mutable_game_command(); cont->CopyFrom(item); cont->set_room_id(roomId); - + emit sigSendIslMessage(msg, serverId); } @@ -599,10 +599,10 @@ void Server::sendIsl_RoomCommand(const CommandContainer &item, int serverId, qin IslMessage msg; msg.set_message_type(IslMessage::ROOM_COMMAND_CONTAINER); msg.set_session_id(sessionId); - + CommandContainer *cont = msg.mutable_room_command(); cont->CopyFrom(item); cont->set_room_id(roomId); - + emit sigSendIslMessage(msg, serverId); } diff --git a/common/server.h b/common/server.h index 46008fb0..46113a50 100644 --- a/common/server.h +++ b/common/server.h @@ -57,6 +57,7 @@ public: virtual bool getGameShouldPing() const { return false; } virtual bool getClientIdRequired() const { return false; } + virtual bool getRegOnlyServer() const { return false; } virtual int getPingClockInterval() const { return 0; } virtual int getMaxGameInactivityTime() const { return 9999999; } virtual int getMaxPlayerInactivityTime() const { return 9999999; } diff --git a/common/server_database_interface.h b/common/server_database_interface.h index c132cb15..224ae2c7 100644 --- a/common/server_database_interface.h +++ b/common/server_database_interface.h @@ -12,8 +12,8 @@ public: Server_DatabaseInterface(QObject *parent = 0) : QObject(parent) { } - virtual AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, QString &reasonStr, int &secondsLeft) = 0; - virtual bool checkUserIsBanned(const QString & /* ipAddress */, const QString & /* userName */, QString & /* banReason */, int & /* banSecondsRemaining */) { return false; } + virtual AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, const QString &clientId, QString &reasonStr, int &secondsLeft) = 0; + virtual bool checkUserIsBanned(const QString & /* ipAddress */, const QString & /* userName */, const QString & /* clientId */, QString & /* banReason */, int & /* banSecondsRemaining */) { return false; } virtual bool activeUserExists(const QString & /* user */) { return false; } virtual bool userExists(const QString & /* user */) { return false; } virtual QMap getBuddyList(const QString & /* name */) { return QMap(); } @@ -24,7 +24,7 @@ public: virtual void storeGameInformation(const QString & /* roomName */, const QStringList & /* roomGameTypes */, const ServerInfo_Game & /* gameInfo */, const QSet & /* allPlayersEver */, const QSet & /* allSpectatorsEver */, const QList & /* replayList */) { } virtual DeckList *getDeckFromDatabase(int /* deckId */, int /* userId */) { return 0; } - virtual qint64 startSession(const QString & /* userName */, const QString & /* address */) { return 0; } + virtual qint64 startSession(const QString & /* userName */, const QString & /* address */, const QString & /* clientId */) { return 0; } virtual bool usernameIsValid(const QString & /*userName */, QString & /* error */) { return true; }; public slots: virtual void endSession(qint64 /* sessionId */ ) { } @@ -42,6 +42,7 @@ public: virtual bool registerUser(const QString & /* userName */, const QString & /* realName */, ServerInfo_User_Gender const & /* gender */, const QString & /* password */, const QString & /* emailAddress */, const QString & /* country */, bool /* active = false */) { return false; } virtual bool activateUser(const QString & /* userName */, const QString & /* token */) { return false; } virtual void updateUsersClientID(const QString & /* userName */, const QString & /* userClientID */) { } + virtual void updateUsersLastLoginTime(const QString & /* userName */) { } enum LogMessage_TargetType { MessageTargetRoom, MessageTargetGame, MessageTargetChat, MessageTargetIslRoom }; virtual void logMessage(const int /* senderId */, const QString & /* senderName */, const QString & /* senderIp */, const QString & /* logMessage */, LogMessage_TargetType /* targetType */, const int /* targetId */, const QString & /* targetName */) { }; diff --git a/common/server_protocolhandler.cpp b/common/server_protocolhandler.cpp index 073a3d95..272b8177 100644 --- a/common/server_protocolhandler.cpp +++ b/common/server_protocolhandler.cpp @@ -385,8 +385,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd if (userName.isEmpty() || (userInfo != 0)) return Response::RespContextError; - if (clientId.isEmpty()) - return Response::RespContextError; QString reasonStr; int banSecondsLeft = 0; diff --git a/common/serverinfo_user_container.cpp b/common/serverinfo_user_container.cpp index bf53a3f0..a0fd05c9 100644 --- a/common/serverinfo_user_container.cpp +++ b/common/serverinfo_user_container.cpp @@ -36,6 +36,7 @@ ServerInfo_User &ServerInfo_User_Container::copyUserInfo(ServerInfo_User &result if (!sessionInfo) { result.clear_session_id(); result.clear_address(); + result.clear_clientid(); } if (!internalInfo) { diff --git a/servatrice/migrations/servatrice_0002_to_0003.sql b/servatrice/migrations/servatrice_0002_to_0003.sql new file mode 100644 index 00000000..4a87bea4 --- /dev/null +++ b/servatrice/migrations/servatrice_0002_to_0003.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 2 to version 3 + +alter table cockatrice_users add clientid varchar(15) not null; + +UPDATE cockatrice_schema_version SET version=3 WHERE version=2; diff --git a/servatrice/migrations/servatrice_0003_to_0004.sql b/servatrice/migrations/servatrice_0003_to_0004.sql new file mode 100644 index 00000000..bbfce750 --- /dev/null +++ b/servatrice/migrations/servatrice_0003_to_0004.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 3 to version 4 + +alter table cockatrice_sessions add clientid varchar(15) not null; + +UPDATE cockatrice_schema_version SET version=4 WHERE version=3; diff --git a/servatrice/migrations/servatrice_0004_to_0005.sql b/servatrice/migrations/servatrice_0004_to_0005.sql new file mode 100644 index 00000000..2d4ecf00 --- /dev/null +++ b/servatrice/migrations/servatrice_0004_to_0005.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 4 to version 5 + +alter table cockatrice_bans add clientid varchar(15) not null; + +UPDATE cockatrice_schema_version SET version=5 WHERE version=4; diff --git a/servatrice/migrations/servatrice_0005_to_0006.sql b/servatrice/migrations/servatrice_0005_to_0006.sql new file mode 100644 index 00000000..3f9552b2 --- /dev/null +++ b/servatrice/migrations/servatrice_0005_to_0006.sql @@ -0,0 +1,5 @@ +-- Servatrice db migration from version 5 to version 6 + +alter table cockatrice_users add last_login datetime not null; + +UPDATE cockatrice_schema_version SET version=6 WHERE version=5; diff --git a/servatrice/scripts/linux/maint_countrycodes b/servatrice/scripts/linux/maint_countrycodes new file mode 100644 index 00000000..32d78628 --- /dev/null +++ b/servatrice/scripts/linux/maint_countrycodes @@ -0,0 +1,36 @@ +#!/bin/bash + +# THIS SCRIPT EXPECTS TO BE EXECUTED FROM THE GITHUB SOURCE FOLDER PATH STRUCTURE +# OTHERWISE, UPDATE THE 'COUNTRYCODEIMAGEPATH' TO POINT TO THE FOLDER CONTAINING THE COUNTRY CODE IMAGES +# USE THIS SCRIPT TO COMPARE EXISTING USER ACCOUNTS TO VALID COUNTRY CODES AND CLEAR INVALID COUNTRY CODE DATA + +MODE="report" #set this to correct to fix invalid country codes, otherwise it only reports +DBNAME="servatrice" #set this to the database name used +TABLEPREFIX="cockatrice" #set this to the prefix used for the table names in the database (do not inclue the _) +SQLCONFFILE="./mysql.cnf" #set this to the path that contains the mysql.cnf file +COUNTRYCODEIMAGEPATH='../../../cockatrice/resources/countries' +VALIDCOUNT=0 +INVALIDCOUNT=0 + +for i in `mysql --defaults-file=$SQLCONFFILE -h localhost -e "select distinct(country) from ""$DBNAME"".""$TABLEPREFIX""_users;"` +do + if [ "$i" != "country" ]; then + if [ -f "$COUNTRYCODEIMAGEPATH/$i.svg" ]; then + ((VALIDCOUNT++)) + else + ((INVALIDCOUNT++)) + + if [ "$MODE" == "correct" ]; then + echo "$i COUNTRY CODE INVALID, ATTEMPTING TO CORRECT" + mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = '' where country = '$i';" + fi + fi + fi +done + +if [ "$MODE" == "correct" ]; then + mysql --defaults-file=$SQLCONFFILE -h localhost -e "update ""$DBNAME"".""$TABLEPREFIX""_users set country = lower(country);" +fi + +echo "INVALID: $INVALIDCOUNT" +echo "VALID: $VALIDCOUNT" diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index 313001c7..f8aeffa7 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -62,8 +62,8 @@ method=none ; if the chosen authentication method is password, here you can define the password your users will use to log in password=123456 -; Accept only registered users? default is 0 (accept unregistered users) -regonly=0 +; Accept only registered users? default is false (accept unregistered users) +regonly=false [users] @@ -193,6 +193,11 @@ roomlist\1\game_types\3\name="GameType3" ; default is 120 max_game_inactivity_time=120 +; All actions during a game are recorded and stored in the database as a replay that all participants of +; the game can go back to and review after the game is closed. This can require a fairly large amount of +; storage to save all the information. Disable this option to prevent the storing of replay data in +; the database. Default value is true. +store_replays=true [security] ; You may want to restrict the number of users that can connect to your server at any given time. diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index b33f7de5..27280167 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( PRIMARY KEY (`version`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -INSERT INTO cockatrice_schema_version VALUES(3); +INSERT INTO cockatrice_schema_version VALUES(6); CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, @@ -84,6 +84,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_users` ( `active` tinyint(1) NOT NULL, `token` binary(16) NOT NULL, `clientid` varchar(15) NOT NULL, + `last_login` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`), KEY `token` (`token`), @@ -132,6 +133,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_bans` ( `minutes` int(6) NOT NULL, `reason` text NOT NULL, `visible_reason` text NOT NULL, + `clientid` varchar(15) NOT NULL, PRIMARY KEY (`user_name`,`time_from`), KEY `time_from` (`time_from`,`ip_address`), KEY `ip_address` (`ip_address`) @@ -144,6 +146,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_sessions` ( `ip_address` char(15) COLLATE utf8_unicode_ci NOT NULL, `start_time` datetime NOT NULL, `end_time` datetime DEFAULT NULL, + `clientid` varchar(15) NOT NULL, PRIMARY KEY (`id`), KEY `username` (`user_name`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index d218cfac..0f56003c 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -143,7 +143,7 @@ bool Servatrice::initServer() serverName = settingsCache->value("server/name", "My Cockatrice server").toString(); serverId = settingsCache->value("server/id", 0).toInt(); clientIdRequired = settingsCache->value("server/requireclientid",0).toBool(); - bool regServerOnly = settingsCache->value("authentication/regonly", 0).toBool(); + regServerOnly = settingsCache->value("authentication/regonly", 0).toBool(); const QString authenticationMethodStr = settingsCache->value("authentication/method").toString(); if (authenticationMethodStr == "sql") { @@ -161,7 +161,8 @@ bool Servatrice::initServer() qDebug() << "Authenticating method: none"; authenticationMethod = AuthenticationNone; } - + + qDebug() << "Store Replays: " << settingsCache->value("game/store_replays", true).toBool(); qDebug() << "Client ID Required: " << clientIdRequired; bool maxUserLimitEnabled = settingsCache->value("security/enable_max_user_limit", false).toBool(); qDebug() << "Maximum user limit enabled: " << maxUserLimitEnabled; @@ -174,7 +175,7 @@ bool Servatrice::initServer() bool registrationEnabled = settingsCache->value("registration/enabled", false).toBool(); bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool(); - qDebug() << "Registration enabled: " << registrationEnabled; + qDebug() << "Registration enabled: " << regServerOnly; if (registrationEnabled) qDebug() << "Require email address to register: " << requireEmailForRegistration; diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index b097022c..500df92c 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -120,7 +120,7 @@ private: QString shutdownReason; int shutdownMinutes; QTimer *shutdownTimer; - bool isFirstShutdownMessage, clientIdRequired; + bool isFirstShutdownMessage, clientIdRequired, regServerOnly; mutable QMutex serverListMutex; QList serverList; @@ -138,6 +138,7 @@ public: QString getLoginMessage() const { QMutexLocker locker(&loginMessageMutex); return loginMessage; } bool getGameShouldPing() const { return true; } bool getClientIdRequired() const { return clientIdRequired; } + bool getRegOnlyServer() const { return regServerOnly; } int getPingClockInterval() const { return pingClockInterval; } int getMaxGameInactivityTime() const { return maxGameInactivityTime; } int getMaxPlayerInactivityTime() const { return maxPlayerInactivityTime; } diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index e2b681a0..ae8279fa 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -44,7 +44,7 @@ bool Servatrice_DatabaseInterface::initDatabase(const QString &type, const QStri sqlDatabase.setDatabaseName(databaseName); sqlDatabase.setUserName(userName); sqlDatabase.setPassword(password); - + return openDatabase(); } @@ -52,7 +52,7 @@ bool Servatrice_DatabaseInterface::openDatabase() { if (sqlDatabase.isOpen()) sqlDatabase.close(); - + const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); qDebug() << QString("[%1] Opening database...").arg(poolStr); if (!sqlDatabase.open()) { @@ -92,7 +92,7 @@ bool Servatrice_DatabaseInterface::checkSql() { if (!sqlDatabase.isValid()) return false; - + if (!sqlDatabase.exec("select 1").isActive()) return openDatabase(); return true; @@ -152,12 +152,6 @@ bool Servatrice_DatabaseInterface::usernameIsValid(const QString &user, QString return re.exactMatch(user); } -// TODO move this to Server -bool Servatrice_DatabaseInterface::getRequireRegistration() -{ - return settingsCache->value("authentication/regonly", 0).toBool(); -} - bool Servatrice_DatabaseInterface::registerUser(const QString &userName, const QString &realName, ServerInfo_User_Gender const &gender, const QString &password, const QString &emailAddress, const QString &country, QString &token, bool active) { if (!checkSql()) @@ -235,7 +229,7 @@ QChar Servatrice_DatabaseInterface::getGenderChar(ServerInfo_User_Gender const & } } -AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, QString &reasonStr, int &banSecondsLeft) +AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, const QString &clientId, QString &reasonStr, int &banSecondsLeft) { switch (server->getAuthenticationMethod()) { case Servatrice::AuthenticationNone: return UnknownUser; @@ -252,23 +246,23 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot if (!usernameIsValid(user, reasonStr)) return UsernameInvalid; - - if (checkUserIsBanned(handler->getAddress(), user, reasonStr, banSecondsLeft)) + + if (checkUserIsBanned(handler->getAddress(), user, clientId, reasonStr, banSecondsLeft)) return UserIsBanned; - + QSqlQuery *passwordQuery = prepareQuery("select password_sha512, active from {prefix}_users where name = :name"); passwordQuery->bindValue(":name", user); if (!execSqlQuery(passwordQuery)) { qDebug("Login denied: SQL error"); return NotLoggedIn; } - + if (passwordQuery->next()) { const QString correctPassword = passwordQuery->value(0).toString(); const bool userIsActive = passwordQuery->value(1).toBool(); if(!userIsActive) { qDebug("Login denied: user not active"); - return UserIsInactive; + return UserIsInactive; } if (correctPassword == PasswordHasher::computeHash(password, correctPassword.left(16))) { qDebug("Login accepted: password right"); @@ -286,7 +280,7 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot return UnknownUser; } -bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress, const QString &userName, QString &banReason, int &banSecondsRemaining) +bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress, const QString &userName, const QString &clientId, QString &banReason, int &banSecondsRemaining) { if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql) return false; @@ -297,11 +291,48 @@ bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress, c } return - checkUserIsIpBanned(ipAddress, banReason, banSecondsRemaining) - || checkUserIsNameBanned(userName, banReason, banSecondsRemaining); + checkUserIsIpBanned(ipAddress, banReason, banSecondsRemaining) || checkUserIsNameBanned(userName, banReason, banSecondsRemaining) || checkUserIsIdBanned(clientId, banReason, banSecondsRemaining); } +bool Servatrice_DatabaseInterface::checkUserIsIdBanned(const QString &clientId, QString &banReason, int &banSecondsRemaining) +{ + if (clientId.isEmpty()) + return false; + + QSqlQuery *idBanQuery = prepareQuery( + "select" + " timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute))," + " b.minutes <=> 0," + " b.visible_reason" + " from {prefix}_bans b" + " where" + " b.time_from = (select max(c.time_from)" + " from {prefix}_bans c" + " where c.clientid = :id)" + " and b.clientid = :id2"); + + idBanQuery->bindValue(":id", clientId); + idBanQuery->bindValue(":id2", clientId); + if (!execSqlQuery(idBanQuery)) { + qDebug() << "Id ban check failed: SQL error." << idBanQuery->lastError(); + return false; + } + + if (idBanQuery->next()) { + const int secondsLeft = idBanQuery->value(0).toInt(); + const bool permanentBan = idBanQuery->value(1).toInt(); + if ((secondsLeft > 0) || permanentBan) { + banReason = idBanQuery->value(2).toString(); + banSecondsRemaining = permanentBan ? 0 : secondsLeft; + qDebug() << "User is banned by client id" << clientId; + return true; + } + } + return false; +} + + bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName, QString &banReason, int &banSecondsRemaining) { QSqlQuery *nameBanQuery = prepareQuery("select timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute)), b.minutes <=> 0, b.visible_reason from {prefix}_bans b where b.time_from = (select max(c.time_from) from {prefix}_bans c where c.user_name = :name2) and b.user_name = :name1"); @@ -363,7 +394,7 @@ bool Servatrice_DatabaseInterface::activeUserExists(const QString &user) { if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { checkSql(); - + QSqlQuery *query = prepareQuery("select 1 from {prefix}_users where name = :name and active = 1"); query->bindValue(":name", user); if (!execSqlQuery(query)) @@ -377,7 +408,7 @@ bool Servatrice_DatabaseInterface::userExists(const QString &user) { if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { checkSql(); - + QSqlQuery *query = prepareQuery("select 1 from {prefix}_users where name = :name"); query->bindValue(":name", user); if (!execSqlQuery(query)) @@ -405,13 +436,13 @@ bool Servatrice_DatabaseInterface::isInBuddyList(const QString &whoseList, const { if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) return false; - + if (!checkSql()) return false; - + int id1 = getUserIdInDB(whoseList); int id2 = getUserIdInDB(who); - + QSqlQuery *query = prepareQuery("select 1 from {prefix}_buddylist where id_user1 = :id_user1 and id_user2 = :id_user2"); query->bindValue(":id_user1", id1); query->bindValue(":id_user2", id2); @@ -424,13 +455,13 @@ bool Servatrice_DatabaseInterface::isInIgnoreList(const QString &whoseList, cons { if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) return false; - + if (!checkSql()) return false; - + int id1 = getUserIdInDB(whoseList); int id2 = getUserIdInDB(who); - + QSqlQuery *query = prepareQuery("select 1 from {prefix}_ignorelist where id_user1 = :id_user1 and id_user2 = :id_user2"); query->bindValue(":id_user1", id1); query->bindValue(":id_user2", id2); @@ -442,11 +473,11 @@ bool Servatrice_DatabaseInterface::isInIgnoreList(const QString &whoseList, cons ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuery *query, bool complete, bool withId) { ServerInfo_User result; - + if (withId) result.set_id(query->value(0).toInt()); result.set_name(query->value(1).toString().toStdString()); - + const int is_admin = query->value(2).toInt(); int userLevel = ServerInfo_User::IsUser | ServerInfo_User::IsRegistered; if (is_admin == 1) @@ -483,6 +514,10 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer const QString email = query->value(8).toString(); if (!email.isEmpty()) result.set_email(email.toStdString()); + + const QString clientid = query->value(9).toString(); + if (!clientid.isEmpty()) + result.set_clientid(clientid.toStdString()); } return result; } @@ -492,16 +527,16 @@ ServerInfo_User Servatrice_DatabaseInterface::getUserData(const QString &name, b ServerInfo_User result; result.set_name(name.toStdString()); result.set_user_level(ServerInfo_User::IsUser); - + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { if (!checkSql()) return result; - - QSqlQuery *query = prepareQuery("select id, name, admin, country, gender, realname, avatar_bmp, registrationDate, email from {prefix}_users where name = :name and active = 1"); + + QSqlQuery *query = prepareQuery("select id, name, admin, country, gender, realname, avatar_bmp, registrationDate, email, clientid from {prefix}_users where name = :name and active = 1"); query->bindValue(":name", name); if (!execSqlQuery(query)) return result; - + if (query->next()) return evalUserQueryResult(query, true, withId); else @@ -534,7 +569,7 @@ void Servatrice_DatabaseInterface::unlockSessionTables() bool Servatrice_DatabaseInterface::userSessionExists(const QString &userName) { // Call only after lockSessionTables(). - + QSqlQuery *query = prepareQuery("select 1 from {prefix}_sessions where user_name = :user_name and id_server = :id_server and end_time is null"); query->bindValue(":id_server", server->getServerId()); query->bindValue(":user_name", userName); @@ -542,18 +577,19 @@ bool Servatrice_DatabaseInterface::userSessionExists(const QString &userName) return query->next(); } -qint64 Servatrice_DatabaseInterface::startSession(const QString &userName, const QString &address) +qint64 Servatrice_DatabaseInterface::startSession(const QString &userName, const QString &address, const QString &clientId) { if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) return -1; - + if (!checkSql()) return -1; - - QSqlQuery *query = prepareQuery("insert into {prefix}_sessions (user_name, id_server, ip_address, start_time) values(:user_name, :id_server, :ip_address, NOW())"); + + QSqlQuery *query = prepareQuery("insert into {prefix}_sessions (user_name, id_server, ip_address, start_time, clientid) values(:user_name, :id_server, :ip_address, NOW(), :client_id)"); query->bindValue(":user_name", userName); query->bindValue(":id_server", server->getServerId()); query->bindValue(":ip_address", address); + query->bindValue(":client_id", clientId); if (execSqlQuery(query)) return query->lastInsertId().toInt(); return -1; @@ -563,13 +599,13 @@ void Servatrice_DatabaseInterface::endSession(qint64 sessionId) { if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone) return; - + if (!checkSql()) return; QSqlQuery *query = prepareQuery("lock tables {prefix}_sessions write"); execSqlQuery(query); - + query = prepareQuery("update {prefix}_sessions set end_time=NOW() where id = :id_session"); query->bindValue(":id_session", sessionId); execSqlQuery(query); @@ -581,7 +617,7 @@ void Servatrice_DatabaseInterface::endSession(qint64 sessionId) QMap Servatrice_DatabaseInterface::getBuddyList(const QString &name) { QMap result; - + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { checkSql(); @@ -589,7 +625,7 @@ QMap Servatrice_DatabaseInterface::getBuddyList(const query->bindValue(":name", name); if (!execSqlQuery(query)) return result; - + while (query->next()) { const ServerInfo_User &temp = evalUserQueryResult(query, false); result.insert(QString::fromStdString(temp.name()), temp); @@ -601,7 +637,7 @@ QMap Servatrice_DatabaseInterface::getBuddyList(const QMap Servatrice_DatabaseInterface::getIgnoreList(const QString &name) { QMap result; - + if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) { checkSql(); @@ -609,7 +645,7 @@ QMap Servatrice_DatabaseInterface::getIgnoreList(const query->bindValue(":name", name); if (!execSqlQuery(query)) return result; - + while (query->next()) { ServerInfo_User temp = evalUserQueryResult(query, false); result.insert(QString::fromStdString(temp.name()), temp); @@ -622,13 +658,13 @@ int Servatrice_DatabaseInterface::getNextGameId() { if (!sqlDatabase.isValid()) return server->getNextLocalGameId(); - + if (!checkSql()) return -1; - + QSqlQuery *query = prepareQuery("insert into {prefix}_games (time_started) values (now())"); execSqlQuery(query); - + return query->lastInsertId().toInt(); } @@ -636,10 +672,10 @@ int Servatrice_DatabaseInterface::getNextReplayId() { if (!checkSql()) return -1; - + QSqlQuery *query = prepareQuery("insert into {prefix}_replays () values ()"); execSqlQuery(query); - + return query->lastInsertId().toInt(); } @@ -647,7 +683,10 @@ void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, { if (!checkSql()) return; - + + if (!settingsCache->value("game/store_replays", 1).toBool() ) + return; + QVariantList gameIds1, playerNames, gameIds2, userIds, replayNames; QSetIterator playerIterator(allPlayersEver); while (playerIterator.hasNext()) { @@ -665,20 +704,20 @@ void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, userIds.append(id); replayNames.append(QString::fromStdString(gameInfo.description())); } - + QVariantList replayIds, replayGameIds, replayDurations, replayBlobs; for (int i = 0; i < replayList.size(); ++i) { QByteArray blob; const unsigned int size = replayList[i]->ByteSize(); blob.resize(size); replayList[i]->SerializeToArray(blob.data(), size); - + replayIds.append(QVariant((qulonglong) replayList[i]->replay_id())); replayGameIds.append(gameInfo.game_id()); replayDurations.append(replayList[i]->duration_seconds()); replayBlobs.append(blob); } - + { QSqlQuery *query = prepareQuery("update {prefix}_games set room_name=:room_name, descr=:descr, creator_name=:creator_name, password=:password, game_types=:game_types, player_count=:player_count, time_finished=now() where id=:id_game"); query->bindValue(":room_name", roomName); @@ -717,17 +756,17 @@ void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int userId) { checkSql(); - + QSqlQuery *query = prepareQuery("select content from {prefix}_decklist_files where id = :id and id_user = :id_user"); query->bindValue(":id", deckId); query->bindValue(":id_user", userId); execSqlQuery(query); if (!query->next()) throw Response::RespNameNotFound; - + DeckList *deck = new DeckList; deck->loadFromString_Native(query->value(0).toString()); - + return deck; } @@ -789,7 +828,7 @@ bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, const qDebug("Change password denied: SQL error"); return true; } - + if (!passwordQuery->next()) return true; @@ -831,7 +870,7 @@ int Servatrice_DatabaseInterface::getActiveUserCount() void Servatrice_DatabaseInterface::updateUsersClientID(const QString &userName, const QString &userClientID) { - + if (!checkSql()) return; @@ -839,5 +878,16 @@ void Servatrice_DatabaseInterface::updateUsersClientID(const QString &userName, query->bindValue(":clientid", userClientID); query->bindValue(":username", userName); execSqlQuery(query); - + +} + +void Servatrice_DatabaseInterface::updateUsersLastLoginTime(const QString &userName) +{ + if (!checkSql()) + return; + + QSqlQuery *query = prepareQuery("update {prefix}_users set last_login = NOW() where name = :user_name"); + query->bindValue(":user_name", userName); + execSqlQuery(query); + } diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index c5bb08a5..d082b2f3 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -9,7 +9,7 @@ #include "server.h" #include "server_database_interface.h" -#define DATABASE_SCHEMA_VERSION 3 +#define DATABASE_SCHEMA_VERSION 6 class Servatrice; @@ -22,13 +22,14 @@ private: Servatrice *server; ServerInfo_User evalUserQueryResult(const QSqlQuery *query, bool complete, bool withId = false); /** Must be called after checkSql and server is known to be in auth mode. */ + bool checkUserIsIdBanned(const QString &clientId, QString &banReason, int &banSecondsRemaining); + /** Must be called after checkSql and server is known to be in auth mode. */ bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining); /** Must be called after checkSql and server is known to be in auth mode. */ bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining); protected: - AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, - const QString &password, QString &reasonStr, int &secondsLeft); + AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, const QString &user, const QString &password, const QString &clientId, QString &reasonStr, int &secondsLeft); public slots: void initDatabase(const QSqlDatabase &_sqlDatabase); @@ -36,7 +37,7 @@ public slots: public: Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server); ~Servatrice_DatabaseInterface(); - bool initDatabase(const QString &type, const QString &hostName, const QString &databaseName, + bool initDatabase(const QString &type, const QString &hostName, const QString &databaseName, const QString &userName, const QString &password); bool openDatabase(); bool checkSql(); @@ -52,28 +53,28 @@ public: bool isInBuddyList(const QString &whoseList, const QString &who); bool isInIgnoreList(const QString &whoseList, const QString &who); ServerInfo_User getUserData(const QString &name, bool withId = false); - void storeGameInformation(const QString &roomName, const QStringList &roomGameTypes, const ServerInfo_Game &gameInfo, + void storeGameInformation(const QString &roomName, const QStringList &roomGameTypes, const ServerInfo_Game &gameInfo, const QSet &allPlayersEver, const QSet&allSpectatorsEver, const QList &replayList); DeckList *getDeckFromDatabase(int deckId, int userId); int getNextGameId(); int getNextReplayId(); int getActiveUserCount(); - qint64 startSession(const QString &userName, const QString &address); + qint64 startSession(const QString &userName, const QString &address, const QString &clientId); void endSession(qint64 sessionId); void clearSessionTables(); void lockSessionTables(); void unlockSessionTables(); bool userSessionExists(const QString &userName); bool usernameIsValid(const QString &user, QString & error); - bool checkUserIsBanned(const QString &ipAddress, const QString &userName, QString &banReason, int &banSecondsRemaining); + bool checkUserIsBanned(const QString &ipAddress, const QString &userName, const QString &clientId, QString &banReason, int &banSecondsRemaining); - bool getRequireRegistration(); - bool registerUser(const QString &userName, const QString &realName, ServerInfo_User_Gender const &gender, + bool registerUser(const QString &userName, const QString &realName, ServerInfo_User_Gender const &gender, const QString &password, const QString &emailAddress, const QString &country, QString &token, bool active = false); bool activateUser(const QString &userName, const QString &token); void updateUsersClientID(const QString &userName, const QString &userClientID); - void logMessage(const int senderId, const QString &senderName, const QString &senderIp, const QString &logMessage, + void updateUsersLastLoginTime(const QString &userName); + void logMessage(const int senderId, const QString &senderName, const QString &senderIp, const QString &logMessage, LogMessage_TargetType targetType, const int targetId, const QString &targetName); bool changeUserPassword(const QString &user, const QString &oldPassword, const QString &newPassword); QChar getGenderChar(ServerInfo_User_Gender const &gender); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index abb2997a..db87c70d 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -756,20 +756,40 @@ Response::ResponseCode ServerSocketInterface::cmdBanFromServer(const Command_Ban if (trustedSources.contains(address,Qt::CaseInsensitive)) address = ""; - QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_bans (user_name, ip_address, id_admin, time_from, minutes, reason, visible_reason) values(:user_name, :ip_address, :id_admin, NOW(), :minutes, :reason, :visible_reason)"); + QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_bans (user_name, ip_address, id_admin, time_from, minutes, reason, visible_reason, clientid) values(:user_name, :ip_address, :id_admin, NOW(), :minutes, :reason, :visible_reason, :client_id)"); query->bindValue(":user_name", userName); query->bindValue(":ip_address", address); query->bindValue(":id_admin", userInfo->id()); query->bindValue(":minutes", minutes); query->bindValue(":reason", QString::fromStdString(cmd.reason())); query->bindValue(":visible_reason", QString::fromStdString(cmd.visible_reason())); + query->bindValue(":client_id", QString::fromStdString(cmd.clientid())); sqlInterface->execSqlQuery(query); servatrice->clientsLock.lockForRead(); QList userList = servatrice->getUsersWithAddressAsList(QHostAddress(address)); - ServerSocketInterface *user = static_cast(server->getUsers().value(userName)); - if (user && !userList.contains(user)) + + if (!userName.isEmpty()) { + ServerSocketInterface *user = static_cast(server->getUsers().value(userName)); userList.append(user); + } + + if (userName.isEmpty() && address.isEmpty()) { + QSqlQuery *query = sqlInterface->prepareQuery("select name from {prefix}_users where clientid = :client_id"); + query->bindValue(":client_id", QString::fromStdString(cmd.clientid())); + sqlInterface->execSqlQuery(query); + if (!sqlInterface->execSqlQuery(query)){ + qDebug("ClientID username ban lookup failed: SQL Error"); + } else { + while (query->next()) { + userName = query->value(0).toString(); + ServerSocketInterface *user = static_cast(server->getUsers().value(userName)); + if (user && !userList.contains(user)) + userList.append(user); + } + } + } + if (!userList.isEmpty()) { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::BANNED); @@ -792,6 +812,7 @@ Response::ResponseCode ServerSocketInterface::cmdBanFromServer(const Command_Ban Response::ResponseCode ServerSocketInterface::cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc) { QString userName = QString::fromStdString(cmd.user_name()); + QString clientId = QString::fromStdString(cmd.clientid()); qDebug() << "Got register command: " << userName; bool registrationEnabled = settingsCache->value("registration/enabled", false).toBool(); @@ -822,7 +843,7 @@ Response::ResponseCode ServerSocketInterface::cmdRegisterAccount(const Command_R QString banReason; int banSecondsRemaining; - if (sqlInterface->checkUserIsBanned(this->getAddress(), userName, banReason, banSecondsRemaining)) + if (sqlInterface->checkUserIsBanned(this->getAddress(), userName, clientId, banReason, banSecondsRemaining)) { Response_Register *re = new Response_Register; re->set_denied_reason_str(banReason.toStdString()); diff --git a/servatrice/src/smtp/qxtglobal.h b/servatrice/src/smtp/qxtglobal.h index a5744d88..51d374a7 100644 --- a/servatrice/src/smtp/qxtglobal.h +++ b/servatrice/src/smtp/qxtglobal.h @@ -30,6 +30,7 @@ #define QXT_VERSION 0x000602 #define QXT_VERSION_STR "0.6.2" +#define QXT_STATIC //--------------------------global macros------------------------------ diff --git a/servatrice/src/smtp/qxtmailattachment.h b/servatrice/src/smtp/qxtmailattachment.h index 8084bbec..23955ff9 100644 --- a/servatrice/src/smtp/qxtmailattachment.h +++ b/servatrice/src/smtp/qxtmailattachment.h @@ -34,7 +34,7 @@ #include #include -class QxtMailAttachmentPrivate; +struct QxtMailAttachmentPrivate; class QXT_NETWORK_EXPORT QxtMailAttachment { public: