diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp index 73b13e23e..d0fb67446 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp @@ -451,7 +451,7 @@ void Server::externalRoomSay(int roomId, const QString &userName, const QString qDebug() << "externalRoomSay: room id=" << roomId << "not found"; return; } - room->say(userName, message, false); + room->say(userName, message, QString(), false); getDatabaseInterface()->logMessage(0, userName, "ISL", message, Server_DatabaseInterface::MessageTargetIslRoom, room->getId(), room->getName()); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index fbca9589a..0a91bbe77 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -13,6 +13,7 @@ #include "libcockatrice/protocol/pb/server_message.pb.h" #include "libcockatrice/protocol/pb/serverinfo_chat_message.pb.h" #include "libcockatrice/protocol/pb/serverinfo_game.pb.h" +#include "libcockatrice/protocol/pb/serverinfo_room_channel.pb.h" #include "libcockatrice/protocol/pb/session_commands.pb.h" #include "libcockatrice/protocol/pb/session_event.pb.h" #include "server_database_interface.h" @@ -40,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -252,6 +254,9 @@ Response::ResponseCode Server_ProtocolHandler::processRoomCommandContainer(const case RoomCommand::ROOM_SAY: resp = cmdRoomSay(sc.GetExtension(Command_RoomSay::ext), room, rc); break; + case RoomCommand::ROOM_CHANNEL_SAY: + resp = cmdRoomChannelSay(sc.GetExtension(Command_RoomChannelSay::ext), room, rc); + break; case RoomCommand::CREATE_GAME: resp = cmdCreateGame(sc.GetExtension(Command_CreateGame::ext), room, rc); break; @@ -856,10 +861,8 @@ Response::ResponseCode Server_ProtocolHandler::cmdJoinRoom(const Command_JoinRoo rooms.insert(room->getId(), room); QReadLocker chatHistoryLocker(&room->historyLock); - QList chatHistory = room->getChatHistory(); - ServerInfo_ChatMessage chatMessage; - for (int i = 0; i < chatHistory.size(); ++i) { - chatMessage = chatHistory.at(i); + const QList mainChatHistory = room->getChatHistory(Server_Room::MAIN_CHANNEL_KEY); + for (const ServerInfo_ChatMessage &chatMessage : mainChatHistory) { Event_RoomSay roomChatHistory; roomChatHistory.set_message(chatMessage.sender_name() + ": " + chatMessage.message()); roomChatHistory.set_message_type(Event_RoomSay::ChatHistory); @@ -868,6 +871,20 @@ Response::ResponseCode Server_ProtocolHandler::cmdJoinRoom(const Command_JoinRoo rc.enqueuePostResponseItem(ServerMessage::ROOM_EVENT, room->prepareRoomEvent(roomChatHistory)); } + // Replay the history of every catalog channel. Privilege filtering for + // restricted channels is applied on top of this in the moderator stack. + for (const ServerInfo_RoomChannel &channel : room->getChannels()) { + const QList channelHistory = room->getChatHistory(QString::fromStdString(channel.id())); + for (const ServerInfo_ChatMessage &chatMessage : channelHistory) { + Event_RoomChannelSay channelChatHistory; + channelChatHistory.set_channel_id(channel.id()); + channelChatHistory.set_message(chatMessage.sender_name() + ": " + chatMessage.message()); + channelChatHistory.set_time_of( + QDateTime::fromString(QString::fromStdString(chatMessage.time())).toMSecsSinceEpoch()); + rc.enqueuePostResponseItem(ServerMessage::ROOM_EVENT, room->prepareRoomEvent(channelChatHistory)); + } + } + Event_RoomSay joinMessageEvent; joinMessageEvent.set_message(room->getJoinMessage().toStdString()); joinMessageEvent.set_message_type(Event_RoomSay::Welcome); @@ -959,6 +976,29 @@ Server_ProtocolHandler::cmdRoomSay(const Command_RoomSay &cmd, Server_Room *room return Response::RespOk; } +Response::ResponseCode Server_ProtocolHandler::cmdRoomChannelSay(const Command_RoomChannelSay &cmd, + Server_Room *room, + ResponseContainer & /*rc*/) +{ + const QString channelId = QString::fromStdString(cmd.channel_id()); + const ServerInfo_RoomChannel *channel = room->findChannel(channelId); + if (channel == nullptr) { + // The client only ever offers catalog ids; a crafted or stale id is rejected. + return Response::RespInvalidCommand; + } + + if (!addSaidMessageSize(static_cast(cmd.message().size()))) { + return Response::RespChatFlood; + } + QString msg = QString::fromStdString(cmd.message()); + + msg.replace(QChar('\n'), QChar(' ')); + + room->say(QString::fromStdString(userInfo->name()), msg, channelId); + + return Response::RespOk; +} + Response::ResponseCode Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room *room, ResponseContainer &rc) { diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h index a12706757..65dab99ed 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h @@ -30,6 +30,7 @@ class Command_ListRooms; class Command_JoinRoom; class Command_LeaveRoom; class Command_RoomSay; +class Command_RoomChannelSay; class Command_CreateGame; class Command_JoinGame; @@ -66,6 +67,8 @@ private: Response::ResponseCode cmdListUsers(const Command_ListUsers &cmd, ResponseContainer &rc); Response::ResponseCode cmdLeaveRoom(const Command_LeaveRoom &cmd, Server_Room *room, ResponseContainer &rc); Response::ResponseCode cmdRoomSay(const Command_RoomSay &cmd, Server_Room *room, ResponseContainer &rc); + Response::ResponseCode + cmdRoomChannelSay(const Command_RoomChannelSay &cmd, Server_Room *room, ResponseContainer &rc); Response::ResponseCode cmdCreateGame(const Command_CreateGame &cmd, Server_Room *room, ResponseContainer &rc); Response::ResponseCode cmdJoinGame(const Command_JoinGame &cmd, Server_Room *room, ResponseContainer &rc); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 614de8d63..7a8c59536 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -26,16 +26,20 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include #include +const QString Server_Room::MAIN_CHANNEL_KEY = QStringLiteral("__main__"); + Server_Room::Server_Room(int _id, int _chatHistorySize, const QString &_name, @@ -45,10 +49,11 @@ Server_Room::Server_Room(int _id, bool _autoJoin, const QString &_joinMessage, const QStringList &_gameTypes, - Server *parent) + Server *parent, + const QList &_channels) : QObject(parent), id(_id), chatHistorySize(_chatHistorySize), name(_name), description(_description), permissionLevel(_permissionLevel), privilegeLevel(_privilegeLevel), autoJoin(_autoJoin), - joinMessage(_joinMessage), gameTypes(_gameTypes), gamesLock(QReadWriteLock::Recursive) + joinMessage(_joinMessage), gameTypes(_gameTypes), channels(_channels), gamesLock(QReadWriteLock::Recursive) { connect( this, &Server_Room::gameListChanged, this, [this](auto gameInfo) { broadcastGameListUpdate(gameInfo); }, @@ -152,9 +157,33 @@ Server_Room::getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes, } } + for (const ServerInfo_RoomChannel &channel : channels) { + result.add_channel_list()->CopyFrom(channel); + } + return result; } +bool Server_Room::isMainChannel(const QString &channelId) +{ + return channelId.isEmpty() || channelId == MAIN_CHANNEL_KEY; +} + +const ServerInfo_RoomChannel *Server_Room::findChannel(const QString &channelId) const +{ + for (const ServerInfo_RoomChannel &channel : channels) { + if (QString::fromStdString(channel.id()) == channelId) { + return &channel; + } + } + return nullptr; +} + +QList Server_Room::getChatHistory(const QString &channelId) const +{ + return channelChatHistory.value(isMainChannel(channelId) ? MAIN_CHANNEL_KEY : channelId); +} + RoomEvent *Server_Room::prepareRoomEvent(const ::google::protobuf::Message &roomEvent) { auto *event = new RoomEvent; @@ -310,12 +339,22 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam return result; } -void Server_Room::say(const QString &userName, const QString &userMessage, bool sendToIsl) +void Server_Room::say(const QString &userName, const QString &userMessage, const QString &channelId, bool sendToIsl) { - Event_RoomSay event; - event.set_name(userName.toStdString()); - event.set_message(userMessage.toStdString()); - sendRoomEvent(prepareRoomEvent(event), sendToIsl); + const bool isMain = isMainChannel(channelId); + + if (isMain) { + Event_RoomSay event; + event.set_name(userName.toStdString()); + event.set_message(userMessage.toStdString()); + sendRoomEvent(prepareRoomEvent(event), sendToIsl); + } else { + Event_RoomChannelSay event; + event.set_channel_id(channelId.toStdString()); + event.set_name(userName.toStdString()); + event.set_message(userMessage.toStdString()); + sendRoomEvent(prepareRoomEvent(event), sendToIsl); + } if (chatHistorySize != 0) { ServerInfo_ChatMessage chatMessage; @@ -324,13 +363,18 @@ void Server_Room::say(const QString &userName, const QString &userMessage, bool chatMessage.set_time(dateTimeString.toStdString()); chatMessage.set_sender_name(userName.toStdString()); chatMessage.set_message(userMessage.simplified().toStdString()); - - historyLock.lockForWrite(); - if (chatHistory.size() >= chatHistorySize) { - chatHistory.removeAt(0); + if (!isMain) { + chatMessage.set_channel(channelId.toStdString()); } - chatHistory.push_back(std::move(chatMessage)); + const QString storageKey = isMain ? MAIN_CHANNEL_KEY : channelId; + historyLock.lockForWrite(); + QList &history = channelChatHistory[storageKey]; + if (history.size() >= chatHistorySize) { + history.removeAt(0); + } + + history.push_back(std::move(chatMessage)); historyLock.unlock(); } } @@ -344,13 +388,17 @@ void Server_Room::removeSaidMessages(const QString &userName, int amount, bool s sendRoomEvent(prepareRoomEvent(event), sendToIsl); if (chatHistorySize != 0) { - int removed = 0; historyLock.lockForWrite(); - // redact [amount] of the most recent messages from this user from history - for (auto message = chatHistory.rbegin(); message != chatHistory.rend() && removed != amount; ++message) { - if (message->sender_name() == stdStringUserName) { - message->clear_message(); - ++removed; + // redact [amount] of the most recent messages from this user from every channel history, + // so removed content does not resurface for late joiners on any stream + for (auto historyIt = channelChatHistory.begin(); historyIt != channelChatHistory.end(); ++historyIt) { + QList &history = historyIt.value(); + int removed = 0; + for (auto message = history.rbegin(); message != history.rend() && removed != amount; ++message) { + if (message->sender_name() == stdStringUserName) { + message->clear_message(); + ++removed; + } } } historyLock.unlock(); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.h b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h index 5933894d5..e9227862f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h @@ -9,6 +9,7 @@ #include #include #include +#include #include class Server_ProtocolHandler; @@ -47,15 +48,18 @@ private: bool autoJoin; QString joinMessage; QStringList gameTypes; + QList channels; QMap games; QMap externalGames; QMap users; QMap externalUsers; - QList chatHistory; + QMap> channelChatHistory; private slots: void broadcastGameListUpdate(const ServerInfo_Game &gameInfo, bool sendToIsl = true); public: + static const QString MAIN_CHANNEL_KEY; + mutable QReadWriteLock usersLock; mutable QReadWriteLock gamesLock; mutable QReadWriteLock historyLock; @@ -68,7 +72,8 @@ public: bool _autoJoin, const QString &_joinMessage, const QStringList &_gameTypes, - Server *parent); + Server *parent, + const QList &_channels = {}); ~Server_Room() override; int getId() const { @@ -103,6 +108,17 @@ public: { return gameTypes; } + const QList &getChannels() const + { + return channels; + } + /** + * @brief Looks up a catalog channel by its stable id. + * @return The matching channel, or nullptr when the id is not in the catalog. + */ + const ServerInfo_RoomChannel *findChannel(const QString &channelId) const; + /** @brief True when @p channelId refers to the implicit Main channel. */ + static bool isMainChannel(const QString &channelId); const QMap &getGames() const { return games; @@ -116,10 +132,7 @@ public: getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes = false, bool includeExternalData = true) const; int getGamesCreatedByUser(const QString &name) const; QList getGamesOfUser(const QString &name) const; - QList &getChatHistory() - { - return chatHistory; - } + QList getChatHistory(const QString &channelId) const; void addClient(Server_ProtocolHandler *client); void removeClient(Server_ProtocolHandler *client); @@ -136,7 +149,7 @@ public: ResponseContainer &rc, Server_AbstractUserInterface *userInterface); - void say(const QString &userName, const QString &s, bool sendToIsl = true); + void say(const QString &userName, const QString &s, const QString &channelId = QString(), bool sendToIsl = true); void removeSaidMessages(const QString &userName, int amount, bool sendToIsl = true); void addGame(Server_Game *game); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto index a8c90ec6c..9d9c6d277 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto @@ -5,6 +5,7 @@ message RoomCommand { ROOM_SAY = 1001; CREATE_GAME = 1002; JOIN_GAME = 1003; + ROOM_CHANNEL_SAY = 1004; } extensions 100 to max; } @@ -22,6 +23,14 @@ message Command_RoomSay { optional string message = 1; } +message Command_RoomChannelSay { + extend RoomCommand { + optional Command_RoomChannelSay ext = 1004; + } + optional string channel_id = 1; + optional string message = 2; +} + // Create a new game in the room message Command_CreateGame { extend RoomCommand { diff --git a/servatrice/migrations/servatrice_0037_to_0038.sql b/servatrice/migrations/servatrice_0037_to_0038.sql new file mode 100644 index 000000000..6b8f155ab --- /dev/null +++ b/servatrice/migrations/servatrice_0037_to_0038.sql @@ -0,0 +1,21 @@ +-- Servatrice db migration from version 37 to version 38 + +-- Room chat channels. +-- +-- A room now offers a fixed catalog of named message streams in addition to the +-- implicit Main channel. Public channels are broadcast to every room member; +-- restricted channels are only delivered to (and sendable by) members whose +-- privilege level meets the channel's access level. + +-- 1. Catalog table, mirroring the config-driven channels of no-DB servers. +CREATE TABLE IF NOT EXISTS `cockatrice_rooms_channels` ( + `id_room` int(7) unsigned NOT NULL, + `id_server` tinyint(3) NOT NULL DEFAULT 1, + `channel_id` varchar(50) NOT NULL, + `display_name` varchar(50) NOT NULL, + `access_level` ENUM('public','registered','judge','moderator','admin','developer') NOT NULL DEFAULT 'public', + PRIMARY KEY (`id_room`, `id_server`, `channel_id`), + FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +UPDATE cockatrice_schema_version SET version=38 WHERE version=37; \ No newline at end of file diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index f802f0244..e45b38804 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -332,6 +332,22 @@ roomlist\1\game_types\1\name="GameType1" roomlist\1\game_types\2\name="GameType2" roomlist\1\game_types\3\name="GameType3" +; Optional chat channels for room number 1, in addition to the implicit Main +; channel every room has. Each channel needs a stable ASCII id, a display name +; (server data, like the room name) and an access level. Supported levels, from +; least to most restrictive, are: public, registered, judge, moderator, admin +; and developer. A channel is readable and writable by every member whose own +; privilege level is at least the channel's level -- so a moderator channel is +; also reachable by admins and developers, and an admin channel by developers. +; Public channels are visible to every room member. +roomlist\1\channels\size=2 +roomlist\1\channels\1\id=looking-for-players +roomlist\1\channels\1\displayname="Looking for players" +roomlist\1\channels\1\access=public +roomlist\1\channels\2\id=moderators +roomlist\1\channels\2\displayname="Moderators" +roomlist\1\channels\2\access=moderator + [game] diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index 4fbc1d8bd..c83f5bb06 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( PRIMARY KEY (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -INSERT INTO cockatrice_schema_version VALUES(37); +INSERT INTO cockatrice_schema_version VALUES(38); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( @@ -158,6 +158,16 @@ CREATE TABLE IF NOT EXISTS `cockatrice_rooms_gametypes` ( FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS `cockatrice_rooms_channels` ( + `id_room` int(7) unsigned NOT NULL, + `id_server` tinyint(3) NOT NULL DEFAULT 1, + `channel_id` varchar(50) NOT NULL, + `display_name` varchar(50) NOT NULL, + `access_level` ENUM('public','registered','judge','moderator','admin','developer') NOT NULL DEFAULT 'public', + PRIMARY KEY (`id_room`, `id_server`, `channel_id`), + FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + -- games CREATE TABLE IF NOT EXISTS `cockatrice_games` ( `room_name` varchar(255) NOT NULL, diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index a832c1652..9dd5a987c 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -249,6 +251,34 @@ Servatrice::~Servatrice() prepareDestroy(); } +/** + * @brief Maps a configured channel access level (database column or ini value) + * onto the ServerInfo_RoomChannel privilege ladder. + * + * Unknown keywords fall back to Public so a typo never hides a channel that was + * meant to be broadly readable. + */ +static ServerInfo_RoomChannel::AccessLevel roomChannelAccessFromKey(const QString &key) +{ + const QString level = key.trimmed().toLower(); + if (level == "developer") { + return ServerInfo_RoomChannel::Developer; + } + if (level == "admin") { + return ServerInfo_RoomChannel::Admin; + } + if (level == "moderator") { + return ServerInfo_RoomChannel::Moderator; + } + if (level == "judge") { + return ServerInfo_RoomChannel::Judge; + } + if (level == "registered") { + return ServerInfo_RoomChannel::Registered; + } + return ServerInfo_RoomChannel::Public; +} + bool Servatrice::initServer() { @@ -360,10 +390,41 @@ bool Servatrice::initServer() while (query2->next()) { gameTypes.append(query2->value(0).toString()); } + + QSqlQuery *query3 = servatriceDatabaseInterface->prepareQuery( + "select channel_id, display_name, access_level from {prefix}_rooms_channels where id_room = :id_room " + "AND id_server = :id_server order by channel_id asc"); + query3->bindValue(":id_server", serverId); + query3->bindValue(":id_room", query->value(0).toInt()); + servatriceDatabaseInterface->execSqlQuery(query3); + QList channels; + QSet seenChannelIds; + while (query3->next()) { + const QString channelId = query3->value(0).toString(); + if (channelId.isEmpty()) { + qWarning() << "Skipping room channel with empty id"; + continue; + } + if (Server_Room::isMainChannel(channelId)) { + qWarning() << "Skipping room channel with reserved id" << channelId; + continue; + } + if (seenChannelIds.contains(channelId)) { + qWarning() << "Skipping duplicate room channel id" << channelId; + continue; + } + seenChannelIds.insert(channelId); + ServerInfo_RoomChannel channel; + channel.set_id(channelId.toStdString()); + channel.set_display_name(query3->value(1).toString().toStdString()); + channel.set_access_level(roomChannelAccessFromKey(query3->value(2).toString())); + channels.append(channel); + } + addRoom(new Server_Room(query->value(0).toInt(), query->value(7).toInt(), query->value(1).toString(), query->value(2).toString(), query->value(3).toString().toLower(), query->value(4).toString().toLower(), static_cast(query->value(5).toInt()), - query->value(6).toString(), gameTypes, this)); + query->value(6).toString(), gameTypes, this, channels)); } } else { int size = settingsCache->beginReadArray("rooms/roomlist"); @@ -376,12 +437,38 @@ bool Servatrice::initServer() gameTypes.append(settingsCache->value("name").toString()); } settingsCache->endArray(); + QList channels; + QSet seenChannelIds; + int size3 = settingsCache->beginReadArray("channels"); + for (int k = 0; k < size3; ++k) { + settingsCache->setArrayIndex(k); + const QString channelId = settingsCache->value("id").toString(); + if (channelId.isEmpty()) { + qWarning() << "Skipping room channel with empty id"; + continue; + } + if (Server_Room::isMainChannel(channelId)) { + qWarning() << "Skipping room channel with reserved id" << channelId; + continue; + } + if (seenChannelIds.contains(channelId)) { + qWarning() << "Skipping duplicate room channel id" << channelId; + continue; + } + seenChannelIds.insert(channelId); + ServerInfo_RoomChannel channel; + channel.set_id(channelId.toStdString()); + channel.set_display_name(settingsCache->value("displayname").toString().toStdString()); + channel.set_access_level(roomChannelAccessFromKey(settingsCache->value("access").toString())); + channels.append(channel); + } + settingsCache->endArray(); Server_Room *newRoom = new Server_Room( i, settingsCache->value("chathistorysize").toInt(), settingsCache->value("name").toString(), settingsCache->value("description").toString(), settingsCache->value("permissionlevel").toString().toLower(), settingsCache->value("privilegelevel").toString().toLower(), settingsCache->value("autojoin").toBool(), - settingsCache->value("joinmessage").toString(), gameTypes, this); + settingsCache->value("joinmessage").toString(), gameTypes, this, channels); addRoom(newRoom); } diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 411360fa7..0bae67e6c 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -21,7 +21,7 @@ class QSqlQuery; -#define DATABASE_SCHEMA_VERSION 37 +#define DATABASE_SCHEMA_VERSION 38 class Servatrice;