mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[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:
parent
be38e8d429
commit
17d2e8d86e
11 changed files with 281 additions and 34 deletions
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue