[Server] Add room chat channels with per-channel fan-out and history (#7476)

* [Server] Add room chat channels with per-channel fan-out and history

* Fixup: redact all channel histories on message removal

removeSaidMessages only redacted the main history, so banned/warned
content stayed visible to late joiners on every other channel.
Drop the unused channelId parameter and redact up to amount messages
per channel across the whole channelChatHistory map.

* [Server] Validate room channel ids and fix catalog schema per review

- servatrice.sql: expand rooms_channels access_level enum to full set
  to match 0037->0038 migration
- servatrice.cpp: skip empty, reserved (__main__) and duplicate channel
  ids when loading from ini and DB with qWarning
- server_protocolhandler.cpp: clarify catalog replay comment (filtering
  happens in moderator stack)
- server_room: drop duplicate channelIndex, linear search preserves
  catalog order

* [Server] Fix server_room.cpp formatting per clang-format-18

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
This commit is contained in:
BruebachL
2026-10-16 22:46:38 +02:00
committed by GitHub
parent be38e8d429
commit 17d2e8d86e
11 changed files with 281 additions and 34 deletions

View File

@@ -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());

View File

@@ -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 <libcockatrice/protocol/pb/commands.pb.h>
#include <libcockatrice/protocol/pb/event_list_rooms.pb.h>
#include <libcockatrice/protocol/pb/event_notify_user.pb.h>
#include <libcockatrice/protocol/pb/event_room_channel_say.pb.h>
#include <libcockatrice/protocol/pb/event_room_say.pb.h>
#include <libcockatrice/protocol/pb/event_server_message.pb.h>
#include <libcockatrice/protocol/pb/event_user_message.pb.h>
@@ -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<ServerInfo_ChatMessage> chatHistory = room->getChatHistory();
ServerInfo_ChatMessage chatMessage;
for (int i = 0; i < chatHistory.size(); ++i) {
chatMessage = chatHistory.at(i);
const QList<ServerInfo_ChatMessage> 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<ServerInfo_ChatMessage> 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<int>(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)
{

View File

@@ -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);

View File

@@ -26,16 +26,20 @@
#include <libcockatrice/protocol/pb/event_leave_room.pb.h>
#include <libcockatrice/protocol/pb/event_list_games.pb.h>
#include <libcockatrice/protocol/pb/event_remove_messages.pb.h>
#include <libcockatrice/protocol/pb/event_room_channel_say.pb.h>
#include <libcockatrice/protocol/pb/event_room_say.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_chat_message.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room_channel.pb.h>
#include <libcockatrice/utility/string_limits.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <string>
#include <utility>
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<ServerInfo_RoomChannel> &_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<ServerInfo_ChatMessage> 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<ServerInfo_ChatMessage> &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<ServerInfo_ChatMessage> &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();

View File

@@ -9,6 +9,7 @@
#include <QStringList>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_chat_message.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room_channel.pb.h>
#include <qtmetamacros.h>
class Server_ProtocolHandler;
@@ -47,15 +48,18 @@ private:
bool autoJoin;
QString joinMessage;
QStringList gameTypes;
QList<ServerInfo_RoomChannel> channels;
QMap<int, Server_Game *> games;
QMap<int, ServerInfo_Game> externalGames;
QMap<QString, Server_ProtocolHandler *> users;
QMap<QString, ServerInfo_User_Container> externalUsers;
QList<ServerInfo_ChatMessage> chatHistory;
QMap<QString, QList<ServerInfo_ChatMessage>> 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<ServerInfo_RoomChannel> &_channels = {});
~Server_Room() override;
int getId() const
{
@@ -103,6 +108,17 @@ public:
{
return gameTypes;
}
const QList<ServerInfo_RoomChannel> &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<int, Server_Game *> &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<ServerInfo_Game> getGamesOfUser(const QString &name) const;
QList<ServerInfo_ChatMessage> &getChatHistory()
{
return chatHistory;
}
QList<ServerInfo_ChatMessage> 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);

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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]

View File

@@ -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,

View File

@@ -45,6 +45,7 @@
#include <QObject>
#include <QProcessEnvironment>
#include <QReadLocker>
#include <QSet>
#include <QSharedPointer>
#include <QSqlDatabase>
#include <QSqlQuery>
@@ -65,6 +66,7 @@
#include <libcockatrice/protocol/pb/event_connection_closed.pb.h>
#include <libcockatrice/protocol/pb/event_server_message.pb.h>
#include <libcockatrice/protocol/pb/event_server_shutdown.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room_channel.pb.h>
#include <new>
#include <qlogging.h>
#include <qnamespace.h>
@@ -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<ServerInfo_RoomChannel> channels;
QSet<QString> 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<bool>(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<ServerInfo_RoomChannel> channels;
QSet<QString> 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);
}

View File

@@ -21,7 +21,7 @@
class QSqlQuery;
#define DATABASE_SCHEMA_VERSION 37
#define DATABASE_SCHEMA_VERSION 38
class Servatrice;